Featured image of post 《深入理解Java虚拟机》 学习笔记(五.终章)——Java内存模型与线程安全/优化

《深入理解Java虚拟机》 学习笔记(五.终章)——Java内存模型与线程安全/优化

JVM笔记系列索引
《深入理解Java虚拟机》 学习笔记(一)——JVM内存结构
《深入理解Java虚拟机》 学习笔记(二)——垃圾回收
《深入理解Java虚拟机》 学习笔记(三)——类文件结构
《深入理解Java虚拟机》 学习笔记(四)——类加载机制与JVM优化
《深入理解Java虚拟机》 学习笔记(五.终章)——Java内存模型与线程安全/优化

Java内存模型

JVM定义了一种Java内存模型以消除各种硬件和操作系统的内存访问差异,保证Java程序的跨平台性。

主内存和工作内存

所有的变量存储在主内存,每个线程有自己的工作内存,保存了被这个线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都在对应工作内存进行。
主内存和工作内存与第一篇的内存结构不是一个层面上的概念,主内存主要对应Java堆的对象实例数据,工作内存主要对应虚拟机栈的部分区域,而且工作内存可能优先存储与寄存器和高速缓存中。

内存操作

Java内存模型规定了8种内存操作:

  1. lock锁定,将变量表示为某线程独占;
  2. unlock解锁,将变量的lock状态借出,执行后才能被其他线程lock;
  3. read读取,将变量的值从主内存中读取,准备load;
  4. load载入,必须先进行read,将read到的数据放入工作内存的变量副本;
  5. use使用,将工作内存的变量的值传递给执行引擎;
  6. assign赋值,接收执行引擎的值付给工作内存的变量;
  7. store存储,将变量的值从工作内存读取,准备write;
  8. write写入,必须先进行store,将store到的数据放入主内存对应变量。

此外规定了对应的一些规则,如read、load和store、write不能单独出现,assign后的变量必须同步回主内存,没有assign的变量不能同步回注内存等等。

volatile变量

volatile声明能保证变量对所有线程可见性,即一个线程修改了变量的值,其他线程立即得知新的值。但由于Java操作并非全是原子操作,所以volatile变量在并发下不能保证线程安全。例如++操作,对应字节码为4个指令,大致可分为读值/加一/写入等步骤,volatile关键值只能保证在取值放入操作栈顶的时候是最新的值,此后的操作之前可能其他线程已经修改了变量的值,就会导致线程安全问题。
适合使用volatile的情况有:

  1. 运算结果不依赖变量的当前值,或者只有单一线程修改变量的值;
  2. 变量不需要与其他状态变量共同参与不变约束。

volatile关键字还能禁止指令重排优化,普通变量只能保证依赖赋值结果的地方都获得正确结果,但不能保证赋值操作的顺序与代码一致。volatile关键字可以避免多线程情况下,代码执行顺序被重排导致的一些错误。
volatile变量的读操作性能与普通变量基本无差别,写操作可能慢一些,总体开销比锁小。
在Java内存模型层面上来看,volatile变量要求load操作和use操作必须连续一起出现,assign和store操作也必须连续一起出现,即每次使用volatile变量前必须从主内存刷新最新的值,每次修改volatile变量之后必须立刻同步到主内存。

long和double变量

没有被volatile修饰的64位数据(long和double)的读写操作划分位两次32位操作,不能保证其操作的原子性。但实际目前商用JVM基本都把64位数据的读写操作作为原子操作。

Java线程

Java线程实现

一般来说线程有3种实现方法:

  1. 使用内核线程:使用操作系统内核的轻量级进程(LWP),每个轻量级进程与一个内核线程一一对应,一对一。缺点是代价较高,在内核态和用户态之间来回切换,并消耗内核资源,操作系统支持的轻量级进程数量有限;
  2. 使用用户线程:用户线程建立在用户空间的线程库上,其建立、同步、销毁、调度完全在用户态完成,不需要内核帮助,一对多。因此快速且低消耗,支持更大的线程数量。缺点是实现复杂,很多问题需要考虑实现;
  3. 用户线程+轻量级进程:以上两者的混合,多対多。

JDK1.2之前使用用户线程实现,JDK1.2开始替换为基于操作系统原生线程模型实现。对Sun JDK而言,Windows和Linux版都是一对一线程模型,一条Java线程映射到一条轻量级进程,Solaris中支持一对一和多対多。

Java线程调度

线程调度方式分为协同式和抢占式。协同式(Cooperative)指线程执行时间由线程自身控制,执行完完成后主动通知系统切换线程;抢占式(Preemptive)指由系统统一分配每个线程的执行时间,线程自身不能决定线程切换。Java使用抢占式。

Java线程安全

线程安全等级

按由强至弱分为以下5种:

  1. 不可变:基本数据类型加final修饰,对象则保证其行为不影响其状态。注意AtomicInteger和AtomicLong并非不可变,这样的设计应该是考虑到线程访问外部变量需要final,但有时候需要可变的数,于是有了这些类;
  2. 绝对线程安全:不需要任何额外的同步措施,即可实现线程安全。Java API标注线程安全的类很多并不是绝对线程安全;
  3. 相对线程安全:通常意义上的线程安全,指保证对象单独的操作是线程安全的,但不保证任何顺序连续调用都能保证线程安全/正确性;
  4. 线程兼容:通常意义上的线程不安全,指对象本身并不线程安全,但可以通过同步手段保证在并发环境下安全、准确;
  5. 相互层对立:无论是否采取同步手段,都无法在多线程环境下并发使用。极少出现,比如Thread类的suspend()和resume()方法,如果两个线程同时持有同一个线程对象,同时分别去中断及恢复线程,中断的是进行恢复操作的线程,那么就会产生死锁。

线程安全实现方法

包括以下几种:

  1. 互斥同步:保证共享数据同一时刻制备一个线程使用。Java最基本的互斥同步手段是sychronized关键字,编译后在同步块前后形成monitorenter和monitorexit两个字节码指令,需要一个reference类型参数来指定锁定/解锁的对象;执行monitorenter指令时,先尝试获取锁,如果锁对象没被锁定或者当前线程已经拥有这个锁,那么锁的计数器加一,并进入代码块,到monitorexit指令执行时则计数器减一,到计数器为0时释放锁。也就是说sychronized对同个线程而言是可重入的,不会自己把自己死锁。但阻塞或唤醒线程开销都比较大,需要切换用户态/内核态,因此sychronized是重量级操作。还可以使用ReentrantLock实现同步,相比sychronized,有等待可中断、公平锁、绑定多条件(Condition)等功能。JDK1.6之后sychronized与ReentrantLock性能基本持平。
  2. 非阻塞同步:先进行操作,没有其他线程争用共享数据则操作成功,否则产生冲突,则采取补偿措施(比如不断重试),基于处理器的一些新指令实现,如Compare-and-Swap(比较并交换,CAS),用户程序不能直接调用,但AtomicInteger等类使用到了。
  3. 无同步方案:如可重入代码(可以在代码执行的任何时刻中断,去执行别的代码,再返回继续执行而不出现错误),线程本地存储(Thread Local Storage)。

Java锁优化

JDK1.6开始引入了许多高效并发优化,实现了各种锁优化技术:

  1. 自旋锁与自适应锁:自旋锁即多个线程请求锁,若持有锁的线程很快会释放锁的话,让其他线程在其他CPU内核执行忙循环(自旋),而不是来回切换挂起/恢复线程,减少开销。自适应锁就是自旋时间由前一次在同一个锁上自旋时间、以及锁的拥有者状态决定自旋时间的自旋锁。
  2. 锁消除:根据逃逸分析结果,判定代码对应堆中数据都不会逃逸的话,可以认为是线程私有的数据,就可以不加同步锁,提高效率。
  3. 锁粗化:如果一系列连续操作都对同一个对象反复加锁解锁,甚至是循环体内加锁,那么频繁进行互斥同步操作会导致不必要的性能损耗,可以将锁的范围扩大,即称为锁粗化。
  4. 轻量级锁:在无竞争的情况下使用CAS操作消除同步的互斥量
  5. 偏向锁:锁偏向第一个获取该锁的线程,如果执行过程中锁没有被其他线程获取,那么持有偏向锁的线程无需同步;如果有其他线程尝试获取该锁,那么结束偏向模式。
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy