Java并发编程实践读书笔记--第一部分 基础知识

目前关于线程安全性没有一个统一的定义,作者自己总结了一个定义,如下:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

在并发编程中,由于不恰当的执行时序而出现不确定的结果的情况被称为竞态条件(Race Condition)。最常见的竞态条件就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能已经失效的观察来决定下一步的动作。比较简单的例子就是两个线程对同一个为0的int型变量进行n(n越大越能明显看出竞态条件的影响)次加一操作,最后结果很可能不是2n。选择一个点来说明此情况,假设目标变量时x,线程1读取x为12,进行加一得到13,这个时候,线程2也读取了x为12,进行加一操作得到13,然后线程1将13写入x,线程2也将13写回x,这样预期x为14的,结果x确实13。代码比较简单就不在此处贴出了,有兴趣的朋友可以试一下。要避免这个问题,就需要将先检查后执行操作以原子方式执行。比较简单的方式就是使用Synchronized关键字对方法或代码块进行同步,下一部分具体讲解。

Java提供了一种内置锁机制来支持原子性:同步代码块(Synchronized Block)。该机制包括两个部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
用法一:
Synchronized (lock){
//code block
}

用法二:
public synchronized void someMethod(){
//code
}
上述用法一,用法二分别展示了同步代码块和同步方法两种方式来让代码以原子方式执行。加锁之所以能让锁保护内的代码以原子方式执行,是因为锁只能被一个线程持有,当一个线程持有该锁后,其他线程只能等待持有锁的线程释放此锁才可以尝试获得此锁。所以当一个线程进入同步代码块获得该锁后,其他线程不会同时执行此代码,不会导致进入代码块的线程读取的数据失效。有两点需要注意
一、用法二没有具体指出使用什么锁,是因为同步方法默认以本对象为锁。
二、每个共享的可变的变量都应该只由一个锁来保护。
一个比较好的加锁约定:
将所有可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得再该对象上不会发生并发访问。

可见性是一种复杂的属性,因为可见性中的错误总会违背我们的直觉。
说到可见性就要提一下内存方面的内容,变量保存在内存中,线程执行到某个方法时会将需要的变量读进自己的工作空间中,在这种情况下,如果内存中的值发生变化,工作线程不一定能察觉。也就是说工作线程不一定能看到内存中变量的变化。这就是可见性的问题。解决可见性问题的办法和解决竞态条件的办法一样:加锁。加锁可以保证一次只有一个线程对变量进行处理,当前线程的工作空间中的值永远都是最新的有效值,线程处理完之后将最新值写回内存,然后其他线程再处理。这样,对于所有线程来说,内存中的变量就是可见的(所有线程中的变量值都是有效的)。

volatile变量是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程。换句话说,每次访问volatile类型的变量,都会从内存中读取变量的最新值。书中给出一个典型的用法

数绵羊程序
volatile boolean asleep;
...
while(!asleep)
countSomeSheep();

该程序在还没睡着时会一直数绵羊,睡着之后跳出循环。因为对asleep的更新是在其他线程中完成的,所以普通变量没办法及时得到asleep的值,这里使用volatile变量就非常合适。
要注意一点,volatile变量只能保证可见性,即该变量会一直与内存中的变量保持一致,但是没办法确保原子性,所以如果对volatile变量进行基于之前值的操作,比如+1操作,就会出现竞态条件。而加锁机制既可以确保可见性又可以确保原子性。

线程封闭是一种不需要使用同步也能保证数据安全的技术。竞态条件的发生是因为在多个线程中共享数据。如果把需要的对象都封闭在线程内部,那么,即使这个对象不是线程安全的也不会发生竞态条件。当觉定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。线程封闭需要在设计时规划,也比较容易理解,就不赘述了。

时间: 2025-01-01 12:50:52

Java并发编程实践读书笔记--第一部分 基础知识的相关文章

Java并发编程实践(读书笔记) 任务执行(未完)

任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元.   任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任务都要等待处理.响应性很糟糕,吞吐量低.系统资源利用率低. 2.显示的为任务创建线程 为每个任务创建对应一个线程,响应快,系统资源利用路高.缺点是资源消耗量大,如果有大量任务要执行的话,系统迟早会因为无限制创建过多的线程而造成内存耗尽.特别当创建的线程数量远远大于系统的CPU核数,由于每一个核同一时

Java并发编程实践读书笔记(5) 线程池的使用

Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到线程安全问题.如果你认为只会在单任务线程的Executor中运行的话,从设计上讲这就已经耦合了. 3,长时间的任务有可能会影响到其他任务的执行效率,可以让其他线程在等待的时候限定一下等待时间.不要无限制地等待下去. 确定线程池的大小 给出如下定义: 要使CPU达到期望的使用率,线程池的大小应设置为:

Java并发编程实践读书笔记(3)任务执行

类似于Web服务器这种多任务情况时,不可能只用一个线程来对外提供服务.这样效率和吞吐量都太低. 但是也不能来一个请求就创建一个线程,因为创建线程的成本很高,系统能创建的线程数量是有限的. 于是Executor就出现 了. Executor框架 线程池的意义 线程创建太少了浪费服务器资源,另外线程创建多了又搞得服务器很累.两个极端的结果都是对外的吞吐量上不去. 所以线程是需要统一管理的,不能随便new Thread().start(). Executor提供了四种线程池. ExecutorServ

JAVA并发编程实战 读书笔记(二)对象的共享

<java并发编程实战>读书摘要 birdhack 2015年1月2日 对象的共享 JAVA并发编程实战读书笔记 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键之synchronized只能用于实现原子性或者确定临界区.同步还有另一个重要的方面:内存可见性. 1.可见性 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 在没有同步的情况下,编译器.处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整.在缺乏足够同步的多线程程

JAVA并发编程实战 读书笔记(一)线程安全性

线程安全性   1.什么是线程安全 在线程安全的定义中,最核心的概念是正确性.正确性的含义是,某个类的行为与规范完全一致.当对正确性有了一个比较清晰的定义后,就可以定义线程安全性:当多个线程访问某个类时,这个类始终能表现出正确的行为,那这个类就是线程安全的. 举例:无状态对象一定是线程安全的. 大多数Servlet都是无状态的,当Servlet在处理请求时需要保存一些信息时,线程安全才会成为一个问题. 2.原子性 举个例子:语句 ++i:虽然递增操作++i是一种紧凑的语法,使其看上去是一个操作,

[Java 并发] Java并发编程实践 思维导图 - 第一章 简介

阅读<Java并发编程实践>一书后整理的思维导图.

[Java 并发] Java并发编程实践 思维导图 - 第一章 简单介绍

阅读<Java并发编程实践>一书后整理的思维导图.

[Java 并发] Java并发编程实践 思维导图 - 第六章 任务执行

根据<Java并发编程实践>一书整理的思维导图.希望能够有所帮助. 第一部分: 第二部分: 第三部分:

[Java 并发] Java并发编程实践 思维导图 - 第五章 基础构建模块

根据<Java并发编程实践>一书整理的思维导图.希望能够有所帮助. 第一部分: 第二部分: