本文探讨Java并发中的其它问题:线程安全、可见性、活跃性等等。html
在行文以前,我想先推荐如下两份资料,质量很高:
极客学院-Java并发编程
读书笔记-《Java并发编程实战》java
《Java并发编程实战》中提到了太多的术语,好比各类XX性。而安全性我以为这个概念并不妥。计算机术语中的线程安全你们一说就懂,但总是生造概念就很差了。又例如,活跃性,就是避免饥饿和死锁呗!
线程安全问题就是多线程时结果受执行顺序影响,要解决就要让相关操做具备原子性。这个上过操做系统原理的确定都知道。至于原子性,再也不解释。git
那么,Java给出了哪些工具来保证原子性和线程安全?github
即synchronized
关键字。内置锁能够做用在方法、代码块中,做用在方法时表示用该类的当前实例(this
)做为锁给方法体加锁。内置锁的实现是经过编译器加入monitor_enter和montior_exit指令,在虚拟机遇到前者时尝试获取锁,把锁的计数器加1;遇到后者时,将锁计数器减1,锁计数器为0时,锁被释放。编程
内置锁一度是java中进行同步的惟一方法,不少遗留方法仍是使用了内置锁进行同步,好比著名的Vector
,Collections
里面的同步包装器(如Collections.synchronizedMap(hashmap)
)等。安全
关于它和Lock
的比较,详见此文。结论是,建议优先使用synchronized
来进行同步。性能优化
显式锁的顶层接口为Lock
,提供了ReenterantLock
, ReadWriteLock
等实现。常见用法以下:多线程
Lock lock = new ReentrantLock(); ... lock.lock(); try { // 方法体 } ... finally { lock.unlock(); }
所谓可重入就是锁的得到是以线程为单位的,同一线程得到锁后能够重复进入锁。锁会保存被持有的计数。并发
信号量:Semaphore
,至关于容许进入数量大于1的锁。
闭锁:Latch
,实现类CountDownLatch
。至关于一个门,闭锁到达结束状态前,门一直关着,全部线程都不能经过。当闭锁到达结束状态时,门打开并容许全部线程经过。
栅栏:Barrier
,全部线程都等待时才打开放行。工具
现代CPU支持一种CAS(Compare And Swap
)指令,能够在一个指令内完成设置和冲突检测,从而实现了高效的原子性。CAS指令接受三个参数(v
, expectedValue
, newValue
)。若是变量v
的值和expectedValue
相等,那么就将v
赋值为newValue
;若是和expectedValue
不相等,就返回失败。
为什么CAS的效率更高?采用互斥同步策略的最主要问题就是进行线程阻塞和唤醒所带来的性能问题,于是这种同步又称为阻塞同步,它属于一种悲观的并发策略(悲观锁),即线程得到的是独占锁。独占锁意味着其余线程只能依靠阻塞来等待线程释放锁。而在 CPU 转换线程阻塞时会引发线程上下文切换,当有不少线程竞争锁的时候,会引发 CPU 频繁的上下文切换(由此致使内核态和用户态切换)致使效率很低。
而基于冲突检测(CAS)的乐观并发策略,通俗地讲就是先进性操做,若是没有其余线程争用共享数据,那操做就成功了,若是共享数据被争用,产生了冲突,那就再进行其余的补偿措施(最多见的补偿措施就是不断地重试,直到试成功为止),这种乐观的并发策略不须要把线程挂起,所以这种同步被称为非阻塞同步。
Java 5.0以后才支持CAS,并用它实现了一些原子变量类,如AtomicInteger//AtomicReference
等等。更重要的是,前面提到的全部锁机制几乎都使用了CAS来作性能优化。
这里的线程间协做是指经过一些机制使得线程能够彼此等待、唤醒,从而可以合做。例如,在生产者-消费者模型中,若是生产者向队列中放入一个新任务,能够马上唤醒一个等待在此的消费者,这即是协做。
首先是基于内置锁和Object
类的wait()
、notify()
、notifyAll()
方法。
在java中,每一个对象都有两个池,锁池和等待池。
synchronized
同步块的线程,若是此同步块的锁(是一个对象)被其余线程持有,则显然线程不能执行下去。线程将被放入该锁对象的锁池中,在锁池中的线程都在竞争这个锁。wait()
方法后,就进入了等待池。在等待池中的线程不去竞争锁,而是等待被锁对象的notify()
或notifyAll()
唤醒,以后再进入锁池,开始竞争锁。因此,锁池中的线程至关于睡着了,而等待池中的线程则进入了第二层睡眠!(若是你看过《盗梦空间》的话~)
再来说这三个方法就好理解了:
Object.wait()
notify()
方法或 notifyAll()
方法)或被中断。在调用 wait()
以前,线程必需要得到该对象锁,即只能在同步块中调用 wait()
方法。进入 wait()
方法后,当前线程释放锁。在从 wait()
返回时(被叫醒时),线程被放入锁池,与其余线程竞争从新得到锁。Object.notify()
Object.notifyAll()
说了这么多,这几个方法有什么用?仍是生产者-消费者问题,队列数据的正确性须要同步机制来确保,而两个线程什么时候生产,什么时候取走就须要线程间协做了。详见此文。
最后,实际上这几个方法已通过时了。若是想实现等待阻塞的功能,应该使用更好用的Lock
和Condition
,与前面的组合一模一样。
关于Lock
和Condition
的例子,见此回答。
可见性指的是,一个变量被一个线程更改后,另外一个线程在读取时因为时间顺序,可能获得的最新的有效值,也可能获得的是旧的无效值。
所以,同步的意义不只仅在于写,还在于读。只要在读的时候也进行同步操做(加锁),就确定能保证可见性。
另外一方面,使用volatile
关键字能够实现轻量级的可见性。volatile
关键字会禁止所修饰的变量被指令重排序和优化成寄存器值从而不对全部线程可见。因为Java保证最低可见性(CPU设置一个变量会是个原子操做,不会出现设置到一半就被读取,从而获得一个随机值的状况),于是volatile
能够实现很是高效的可见性。
可是volatile
的局限也是有的:它只能用于赋值操做,若是是i++这种组合操做,结果依赖于以前的值,就再也不能保证原子性了,于是没法保证准确。这时,只能采用加锁操做(或者CAS的冲突重试,总之要保证原子性)。