JVM笔记 - 高效并发(Java内存模型与线程)

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》笔记

1、概述

2、硬件的效率与一致性

基于告诉缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题:缓存一致性。

处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的。

3、Java内存模型

线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行。

3.1、主内存与工作内存

3.2、内存间交互操作

3.3、对于volatile型变量的特殊规则

当一个变量定义为volitile之后,它将具备两种特性。

第一是保证此变量对所有线程的可见性。

volitile变量在各个线程中是一致的,并不能得出基于volitile变量的运算在并发下是安全的这个结论。

volatile变量的运算在并发下一样是不安全的。

当getstatic指令把变量的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把变量的值加大了。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

使用volatile变量的第二个语义是禁止指令重排优化。

普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

指令重排是并发编程中最容易让开发人员产生疑惑的地方,volitile关键字可以禁止指令重排序优化。

volatile变量读取操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要再笨的代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下volatile的总开销仍然要比锁第,我们在volatile与锁之中选择的唯一依据仅仅是volatile的语义能否满足使用场景的需求。

如一个变量的修改不依赖与原值,则这个时候可以使用volatile关键字实现先行发生关系。

3.4、对于long和double型变量的特殊规则

在实际开发中,目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此我们在编码时一般不需要把用到的long和double变量专门声明为volatile。

3.5、原子性、可见性与有序性

原子性

由Java内存模型来直接保证的原子性变量操作包括:read、load、assign、use、store和write。

可见性

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。

有序性

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

大部分并发控制都能使用synchronized来完成。synchronized的“万能”也间接早就了它被程序员滥用的局面,越“万能”的并发控制,通常会伴随着越大的性能影响。

3.6、先行发生原则

依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。

下面是Java内存模型下一些“天然的”先行发生关系:

  • 程序次序规则
  • 管程锁定规则
  • volatile变量规则
  • 线程启动规则
  • 线程终止规则
  • 线程中断规则
  • 对象终结规则
  • 传递性

4、Java与线程

4.1、线程的实现

Thread类与大部分的Java API有显著的差别,它的所有关键方法都是声明为Native的。

实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。

使用内核线程实现

由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的

使用用户线程实现

使用用户线程加轻量级进程混合实现

Java线程的实现

4.2、Java线程调度

分为协同式线程调度和抢占式线程调度。Java使用的线程调度方式就是抢占式调度。

线程优先级并不是太靠谱,原因是Java的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

4.3、状态转换

以下方法会让线程陷入无限期的等待状态:

  • 没有设置Timeout参数的Object.wait()方法
  • 没有设置Timeout参数的Thread.join()方法
  • LockSupport.park()方法

以下方法会让线程进入限期等待状体:

  • Thread.sleep()
  • 设置了Timeout参数的Object.wait()方法
  • 设置了Timeout参数的Thread.join()方法
  • LockSupport.parkNanos()方法
  • LockSupport.parkUntil()方法

5、本章小结

arthinking wechat
欢迎关注itzhai公众号