从JVM的角度来看Java的多线程(2)

而5个线程并发执行的时候完全有可能5个线程都先执行了第一步,这样5个线程的工作内存里a的初始值都是0,然后执行a=a+1后在工作内存里的运算结果都是1,最后同步回主内存的值肯定也是1。

而避免这种情况的方法就是:在多个线程并发访问a的时候,保证a在同一个时刻只被一个线程使用。

同步(synchronized)就是:在多个线程并发访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用。

同步基本思想

为了保证共享数据在同一时刻只被一个线程使用,我们有一种很简单的实现思想,就是在共享数据里保存一个锁,当没有线程访问时,锁是空的,当有第一个线程访问时,就在锁里保存这个线程的标识并允许这个线程访问共享数据。在当前线程释放共享数据之前,如果再有其他线程想要访问共享数据,就要等待锁释放

我们把这种思想的三个关键点抽出来:

在共享数据里保存一个锁

在锁里保存这个线程的标识

其他线程访问已加锁共享数据要等待锁释放

Jvm的同步实现  

可以说jvm中的三种锁都是以上述思想为基础的,只是实现的“重量级”不同,jvm中有以下三种锁(由上到下越来越“重量级”):

偏向锁

轻量级锁

重量级锁

其中重量级锁是最初的锁机制,偏向锁和轻量级锁是在jdk1.6加入的,可以选择打开或关闭。如果把偏向锁和轻量级锁都打开,那么在java代码中使用synchronized关键字的时候,jvm底层会尝试先使用偏向锁,如果偏向锁不可用,则转换为轻量级锁,如果轻量级锁不可用,则转换为重量级锁。具体转换过程下面会讲。

要想深入了解这3种锁需要了解对象的内存结构(MarkWord头),会涉及到字节码的内部存储格式,但是其实我觉得脱离细节的实现,单从原理上理解这三个锁是很容易的,只需要了解两个大体的概念:

MarkWord:java中的每个对象在存储的时候,都有统一的数据结构。每个对象都包含一个对象头,称为MarkWord,里面会保存关于这个对象的加锁信息。

Lock Record: 即锁记录,每个线程在执行的时候,会有自己的虚拟机栈,当个方法的调用相当于虚拟机栈里的一个栈帧,而Lock Record就位于栈帧上,是用来保存关于这个线程的加锁信息。

最初jvm没有前两种锁(前两种都是jdk1.6才引入的),只有重量级锁。

我们之前给出了同步基本思想的三个点,我们也说了jvm的三种锁都是以基本思想为基础的,而这三种锁在第1、2点的实现上本质上是一样的:

在共享数据里保存一个锁  //java同步是通过synchronized关键字实现的,synchronized有三种用法:一种是同步块,这种用法需要指明一个锁定对象;一种是修饰静态方法,这种用法相当于锁定Class对象;一种是修饰普通方法,这种用法相当于锁定方法所在的实例对象。因此,在java里能够被synchronized关键字锁定的一定是对象,因此就要在对象里保存一个锁,而对象内存结构里的MarkWord就可以认为是这个锁。三种锁虽然实现细节不同,但是都是使用MarkWord保存锁的。

在锁里保存这个线程的标识  //偏向锁是在MarkWord里保存线程id,轻量级锁是在MarkWord里保存指向拥有锁的线程栈中锁记录的指针,重量级锁是在MarkWord中保存指向互斥量的指针(互斥量只向一个线程授予对共享资源的独占访问权,可以认为是记录了线程的标识)

而区分这三种锁的关键,就是同步基本思想的第三点:

   3.其他线程访问已加锁共享数据要等待锁释放

这里的等待锁释放是一个抽象的说法,并没有严格要求怎么等待。而重量级锁因为使用了互斥量,这里的等待就是线程阻塞。使用互斥量可以保证所有情况下的并发安全,但是使用互斥量会带来较大的性能消耗。而且在实际的项目代码中,很可能一段本来不会有并发情况的代码被加了锁,这样每次使用互斥量就白白消耗了性能。能不能先假设被加锁的代码不会有并发的情况,等到发现有并发的时候再使用互斥量呢?答案是可以的,轻量级锁和偏向锁都是基于这种假设来实现的。

轻量级锁

轻量级锁的核心思想就是“被加锁的代码不会发生并发,如果发生并发,那就膨胀成重量级锁(膨胀指的锁的重量级上升,一旦升级,就不会降级了)”。

轻量级锁依赖了一种叫做CAS(compare and swap)的操作,这个操作是由底层硬件提供相关指令实现的:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/bdd7864195536f77ad276fc726b9bc62.html