12 Java内存模型与线程
12.3 Java内存模型
《Java虚拟机规范》中曾视图定义一种“Java内存模型”来屏蔽各种硬件和操作系统的内存访问差异,已实现让Java程序在各种平台下都能达到一致的内存访问效果。
12.3.1 主内存与工作内存
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量这样的底层细节,此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构造数组对象的元素,但是不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,自然就不存在竞争问题。为了更好地执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器是否要进行调整代码执行顺序这类优化措施。
Java内存模型规定了所有的变量都存储在主内存中。每个线程还有自己的工作内存,线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
12.3.2 内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节。Java内存模型中定义了一下8种操作来完成。Java虚拟机实现是必须保证下面提及的每一种操作都是原子的、不可再分的(对于double 和long类型来说,load,store、read和write操作在某些平台上有例外。)
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存总得到的变量放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接受的值赋值给工作内存,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
12.3.3 对应volatile型变量的特殊规则
当一个变量被定义成volatile之后,它将具备两项特性:第一项特性保证此变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量做不到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 运算不需要与其他状态变量共同参与不可变约束。
13.2.2 线程安全的实现方法
1 同步互斥
重入锁是Lock接口最常见的一种实现,它与synchronized一样是可重入的。在基本用法上,ReentrantLock与Synchronized很相似,只是代码写法上稍有区别而已。不过,ReentrantLock与synchronized相比在增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁以及锁可以绑定多个条件。
- 等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择释放等待,该为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
- 公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来一次获得锁,而非公平锁则不保证这一点,在锁被释放时,任何一个等待的线程都有机会获得锁,synchronized中的锁默认是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过布尔值构造函数要求使用公平锁。不过一旦使用公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
- 绑定多个条件:
ReentrantLock在功能上是synchronized的超集,在性能上又至少不会弱于synchronized。基于以下理由,笔者仍然推荐在synchronized与ReentrantLock都满足需要时优先使用synchronized:
- synchronized是Java语法层面的同步,足够清晰,也足够简单。每个java程序员都熟悉synchronized,但JUC的Lock接口并非如此,因此只需要在基础的同步功能是,更推荐synchronized。
- Lock应该确保finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则可能永远不会释放持有的锁。这一点需要程序员自己来保证,而使用synchronized的话,则可以由Java虚拟机来确保即使出现异常,锁也能被自动释放。
- 尽快JDK1.5时代ReentrantLock曾经在性能上领先过synchronized,但这已经是十多年之前的胜利了。从长远来看,Java虚拟机更容易针对synchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized的锁的相关信息,而使用JUC中的Lock的话,Java虚拟机很难的值具体哪些锁对象是由特定的线程锁持有的。
13.3 锁优化
高效并发是从JDK5升级到JDK6后一项重要的改进项,HotSpot虚拟机开发团队在这个版本上花费了大量的资源去实现啊各种锁优化技术,如适应性自旋、锁消除、锁粗化,轻量级锁、偏向锁等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率。
