Java
内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM
为程序中所有的操作定义了一个偏序关系,称之为Happens-Before
。要想保证执行操作B的线程看到操作A的结果,那么在A和B之间必须满足Happens-Before
关系。如果两个操作之间缺乏Happens-Before
关系,那么JVM
可以对它们任意地重排序。
Java并发编程实战———AbstractQueuedSynchronizer
在基于AQS
构建的同步器类中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态的操作,并且通常会阻塞。当使用锁或信号量时,“获取”操作的含义即获取的是锁或者许可,并且调用者可能会一直等待直到同步器类处于可被获取的状态。在使用CountDownLatch
时,“获取”操作意味着“等待并直到闭锁到达结束状态”,而在使用FutureTask
时,则意味着“等待并直到任务已经完成”。“释放”并不是一个可阻塞的操作,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行。
Java并发编程实战———显式锁
Lock与ReentrantLock
Lock
提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显示的。
1 | //Lock接口 |
ReentrantLock
实现了Lock
接口,并提供了与synchronized
相同的互斥性和内存可见性。
1 | //使用ReentrantLock来保护对象状态 |
Java并发编程实战———线程池的使用
在任务与执行策略之间的隐形耦合
在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。
线程饥饿死锁
在线程池中,如果任务依赖于其他任务,那么可能产生死锁。在单线程的Executor
中,如果一个任务将另一个任务提交到同一个Executor
,并且等待这个被提交任务的结果,那么通常会引发死锁。只要线程池中的任务需要无限期地等待一些必须由池中其他任务才能提供的资源或条件,就会发生线程饥饿死锁。
1 | //在单线程Executor中任务发生死锁 |
每当提交了一个有依赖性的
Executor
任务时,要清楚地知道可能会出现线程饥饿死锁,因此需要在代码或配置Executor
的配置文件中记录线程池的大小限制或配置限制。
Java并发编程实战———取消与关闭
要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断(Interruption
),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。
任务取消
如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就称为可取消的。在Java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占方式来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
其中一种协作机制能设置某个“已请求取消”标志,而任务将定期地查看该标志。如果设置了这个标记,那么任务将提前结束。
1 | //使用volatile类型的域来保存取消状态 |