再读《Java并发编程实践》
近日又看了一次《Java并发编程实践》这本书,以前看过这本书,并没有全部看完,只是选择性的看了几个章节。这一次有所不同,几乎将整本书都看完,而且还做了内容摘要和笔记,这样看下来,还是有不少的收获,一是对有些知识理解的更加深入了,另外发现不少的有意思的东西,比如条件队列,无界线程池的使用等,现在分享给大家。
条件队列
条件队列,其实就是线程队列,每个对象都有一个内置锁,与内置所关联的还有一个条件队列。当你线程进入同步方法或同步块中执行时,也就表示该线程已经获取了内置锁,然后因为某个线程某一条件没有满足时,线程调用wait方法,那么此时线程将进入条件队列中,等待其他的线程在同一个条件队列上调用notifyAll,notify方法。当线程从条件队列被唤醒了,那么线程将又去竞争对象锁,如果成功获取锁,那么将继续检测条件是否为真,如果为真,那么线程将继续执行,否则又将调用wait方法重新阻塞。调用的伪代码如下:
while(true){
if(!condition){//条件是否为真
wait();
continue;
}
//执行其他业务操作。
}
在面试的过程当中,也遇到与此相关的问题,就是问wait,sleep方法有何区别。其中一个重要区别就是调用wait方法需要持有锁,而sleep不需要。现在才彻底明白了,Object中的wait,notify,notifyAll方法是针对对象的条件队列上的线程间的协作方法。wait方法是阻塞线程,然后将线程加入条件队列,而notify,notifyAll则唤醒条件队列中一个或全部线程,让其去竞争锁。
Condition类
Condition类,一直以来,我都不知道这个类做什么用的,说出来还真有点好笑,无知。引入这个对象就是为了弥补一个对象上只能有一个条件队列的问题,也就是说在多个条件谓词上等待的线程都在同一个条件队列,所以在选择notify,notifyAll唤醒条件队列上的线程时,最好选择notifyAll,而不是notify。如果你选择notify唤醒某一个线程,而唤醒的线程并不在同一个条件谓词上等待,当唤醒的线程检查条件谓词时,并不能返回真,这将导致一次无效的线程唤醒操作。
通过Lock.newCondition方法可以在一个锁对象上创建多个条件队列,让在同一个条件谓词上等待的线程都在同一条件队列,这样你就可以安全的使用signal方法,而不是signalAll方法来唤醒线程了。
无界线程池
当线程池无限扩大时,势必耗尽内存,就是没有耗尽内存,也会因为线程过多导致频繁的线程上下文切换,性能也影响,所以在使用无界线程池需要特别的注意。另外使用无界线程池也能够解决一些问题,比如加入队列中的任务有依赖,或者任务在线程池的执行过程当中时,将子任务又加入队列并等待子任务的执行完成,如果单线程的线程池或只有少量线程的线程池,那么极容易出现死锁的问题,将有可能出现运行中的任务都在等待队列中任务的执行完成,而线程池中已经没有多余的线程执行队列中的任务了,那么程序将不能继续往下执行了。如果是无界线程池,就不存在这个问题,因为可以通过创建的新的线程去执行队列中的任务。
另外当使用无界队列时,可以考虑将队列的长度设置短一些,或者设置为同步队列,这样就可以让任务快速得到执行,提供程序的响应性。
ABA的问题
以前我觉得这不是一个问题,使用CAS(比较并交换)方式更新变量时候(乐观锁修改数据记录也一样),从读取变量到变量通过CAS设置的过程当中,如果变量值已经发生了变化(其他线程修改此变量值),那么CAS操作将会失败,如果变量没有发生变化,那么CAS操作将会成功。ABA的问题就是某一线程已经修改变量值,但在之后又被修改回原来的值,也就说变量原值为A,修改为值B后,又被修改值A,变量的值没有变化,但是变量确实已经修改过了,在这种情况下,CAS操作也会成功,对于大多数情况,这都是没有问题。假设这样一个情况,内存地址10000开始的两个字节存放某一变量的开始地址20000,修改为30000后,又修改为20000,但是内存地址20000所指向地址空间的变量已经被修改为其他值,如果根据地址值作为修改依据,那么将会覆盖此变量值。其实解决的办法也很简单,就是加入版本号,每次修改后,版本后都会增加,这样就解决了ABA问题的出现。另外JDK中也提供AtomicStampedReference以及AtomicMarkableReference支持原子性的更新两个变量。
不可中断阻塞的处理
JDK中有些操作是可以响应中断的,比如BlockingQueue中的put,take方法。有些则不能,比如对象的内置锁阻塞等,但是可以通过某些方式来中断阻塞中线程。
最为常见的不可中断的阻塞有套接字上的读取和写入操作,但是可以通过关闭套接字的方式间接中断线程,当关闭底层的套接字时,在其上读取或写入的线程将收到一个SocketException,这样就可以让阻塞的线程提前返回。
另外还有Selector.select方法上的阻塞,可以通过调用其上的close或wakeup方式,让其抛出一个异常就可以提前返回。至于在对象的内置锁阻塞的线程,并没有一种中断阻塞的线程。可通过换用Lock类来解决这个问题。
全文完。