并发编程系列之volatile和synchronized实现原理

前言

上节我们讲了并发的一些挑战,算是开启并发编程的大门,今天我们就来说说并发中最基本的两个东西volatile和Synchronized的底层实现原理,我们都知道Java代码在编译后会变成字节码,然后被类加载器加载到JVM中,JVM执行字节码,最终需要转化为汇编指令在CPU上去执行,因此Java中所使用的的并发机制是依赖于JVM的实现和CPU的指令完成的。ok,那么我们现在就开启我们今天的并发之旅吧。

 

Synchronized

Synchronized是并发编程中最基本最古老的元素,一般也被称为重量级锁

 

Synchronized实现同步的基础

Java中每一个对象都可以作为锁,具体表现为下面3种形式:

  • 对于普通同步方法,锁是当前实例对象

  • 对于静态同步方法,锁是当前类的class对象

  • 对于同步方法块,锁是synchronized包含的代码块

 

Synchronized在JVM中的实现原理

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,synchronized关键字在经过编译之后,会在同步块的前后形成monitorenter和monitorexit这;两个字节码指令,这两个字节码都需要一个引用类型的参数来指明要锁定和解锁的对象,如果synchronized明确指定了对象参数,那就是这个对象的引用,如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应对象的实例或者class对象来作为锁的对象。

 

monitorenter:执行monitorenter时,首先要尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了该对象的锁,那么就把锁的计数器+1;

monitorexit:执行monitorexit的时候就会将锁的计数器-1,当计数器为0时,锁就被释放,如果一个线程获取锁失败,那么就会阻塞等待,直到对象锁被释放为止;

对于monitorenter和monitorexit行为描述中有2点需要注意:

  • synchronized同步块对同一个线程来说是可重入的,不会出现自己把自己锁死的问题

  • 同步块在已进入的线程执行完之前,会阻塞后续线程的进入,也就是说同步块只允许一个线程正在执行

 

synchronized使用的锁存在哪?

synchronized用的锁存放在Java对象头里面的,对象头存储结构如下:

Java对象头分为2部分信息,第一部分存储对象自身运行数据(哈希码,GC分代年龄等)官方称Mark Word,第二部分用于存储指向方法区对象类型数据的指针,如果对象为数组,额外存储个数组的长度;

Mark Word默认存储对象的hashcode,分代年龄和锁标记位,在32位JVM和64位JVM下存储是不同的,32位下大小为32bit,64位下大小为64bit,分别如下所示:

在运行期间,Mark Word中存储的数据会随着锁标志位变化发生改变,如下图所示:

 

锁的升级和对比

引入偏向锁和轻量级锁的目的是为了减少获得锁和释放锁带来的性能开销,锁一共有4种状态,级别由低到高:无锁状态、偏向锁状态·轻量级锁状态和重量级锁状态,这几个状态随着竞争情况逐渐升级,但是不可以降级,也就是说偏向锁升级为轻量级锁之后,不能降级为偏向锁,这种策略也是为了提高锁的获取和释放效率。

 

偏向锁

偏向锁的获取:

偏向锁的释放:使用一种等到竞争出现才释放锁的机制,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,过程如下:

偏向锁的关闭:偏向锁在1.6和1.7中默认是启用的,但是在应用程序启动后会有几秒钟的延迟才能激活,通过JVM参数-XX:BiasedLockingStartupDelay=0来设置关闭延迟激活,如果你想要彻底关闭偏向锁,可以使用-XX:-UseBiasedLocking=false,那么程序将禁止使用偏向锁,默认直接进入轻量级锁状态。

 

轻量级锁

轻量级锁获取:

轻量级锁释放:

因为自旋会消耗CPU,所以锁一旦升级为重量级锁,就不会再降级到轻量级锁,这也就是前面提到的锁升级策略的原因。

 

对比

 

volatile

volatile是轻量级的synchronized,它在多处理器环境下保证了共享变量的“可见性”(当一个线程修改一个共享变量时,这个变量值得修改,对于其他线程都是可见的,即其他线程都能正确的获取到修改后的值),volatile由于不存在线程上下文切换和调度,所以一般情况下比synchronized的执行成本要低,那么接下来我们就看下,处理器是如何实现volatile的。

 

volatile的定义

Java语言规范中指出:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量

Java提供的volatile,在某些情况下会比锁更加的好用,Java线程内存模型确保所有线程看到的变量(volatile声明)都是一致的。

 

volatile是如何保证可见性的

首先我们了解下,被volatile修饰的变量进行写操作时JVM是怎么操作的:JVM会向处理器发送一条带有Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作时,就会有问题,所以,在多处理器下,为了保证各个处理器的缓存一致性,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期的,当处理器发现自己缓存的行对应的内存地址被修改,就会将当期处理器的缓存设置为无效状态,当处理器对这个数据进行修改时,会重新从系统内存中把最新数据读到处理器缓存中;

 

从上述过程我们可以得到,volatile进行写操作时,CPU会对收到一条带有Lock前缀的汇编指令,该指令主要处理以下两件事情:

  • Lock前缀指令会引起处理器缓存回写到内存

  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

 

可以理解为,volatile修饰的变量有一份本地缓存的数据,当变量被其他线程修改时,主内存就会通知各个本地缓存的数据,并将其设置为无效的,当该线程再操作本地缓存数据时,发现数据失效,就会强制去主内存获取最新数据,并写回本地缓存,状态改为有效,从而保证了变量的获取总是取最新的值,也就是说变量的修改对于所有线程都是可见的。

 

今天的内容就到这,主要是介绍并发中synchronized和volatile并发机制的底层实现原理,后期还会从别的角度谈synchronized和volatile的具体应用和细节

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章