深入理解JVM之线程安全的实现方法(七)

这篇文章算是对前面几篇文章的一个小结,从Java内存模型是如何来保证并发操作过程中的原子性、可见性以及有序性,到Happes-Before原则是如何保证内存模型中操作的有序性,再到我们特意分析了volatilesynchronized这两个JVM内置的实现同步机制关键字,在内存模型中的作用和他们的区别,但是这中间总是离不开「线程安全」几个字,那么实现线程安全的方法究竟有哪些,本文试图做一下总结。

线程安全的概念

周老师认为《Java Concurrency In Practice》的作者Brian Goetz对线程安全有个比较好的定义,如下:
当多个线程访问一个对象的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。

线程安全实现方法

互斥同步

互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
最基本的互斥手段是 synchronized 关键字,也是最简单的一种方式。
此关键字在经过编译之后,会在同步块前后形成monitorenter和monitorexit这两个字节码的指令,这两个字节码都需要一个reference来指定对象参数,来指明要锁定和解锁的对象。
除了synchronized之外,还有J.U.C中实现了Lock的ReentrantLock来实现同步,至于区别之前已经交代过,一个表现为API层面的互斥锁(Lock()和unlock()方法配合try/finally语句块来完成),另一个变现为语法层面的互斥锁。

非阻塞同步

除了上面说的互斥同步,还有一种方式就是非阻塞同步。互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(如加锁),那就肯定会出现问题,无论共享数据是否会发生竞争,都要进行加锁等操作。
随着硬件指令集的发展有了另外的选择:基于冲突检测的乐观并发策略。为什么说使用乐观并发策略需要“硬件指令集的发展”才行呢?因为我们需要「冲突检测和操作」这两个步骤具备原子性,再使用互斥同步就失去意义了,所以只能依靠硬件来完成这件事情,硬件能保证一个从语义上看起来需要多次操作的行为只通过一条处理器只能就能完成,比如最典型的;比较并交换(Compare-and-Swap,简称CAS)。
关于CAS的详细描述,参见该篇文章

同步只是保证共享数据争用时的正确性的手段,至于第三种「无同步方案」的意思就是不涉及共享数据,自然而然不需要任何同步的措施去保证正确性,因为有些代码天生就是线程安全的,比如可重入代码和线程本地存储。

参考

本文内容摘自周志明老师的深入理解Java虚拟机第十三章线程安全与锁优化。