【Java进阶营】synchronized同步锁的使用与原理

x33g5p2x  于2022-04-23 转载在 Java  
字(2.9k)|赞(0)|评价(0)|浏览(223)

多线程下,无锁代码可能产生的问题

首先看下面的代码,逻辑很简单,定义了5个线程,这5个线程分别对同一个对象的成员变量num进行10000次递增操作,最后等待所有线程执行完成后,将num结果输出。

正常情况下,我们预期的输出是50000,但运行程序之后,实际输出结果并不是如此,结果可能是50000,也可能小于50000。

为什么会出现结果小于50000的情况呢?

首先,我们要知道,线程在执行"num++"这句代码时,实际上会执行三个动作:

读取num的值;

修改num的值;

将修改后的num刷新到内存中。

由于此处有5个线程同时在执行这段代码,所以可能出现这种情况:线程1读取到了num的值为0,此时,线程1还没有执行修改num值的动作,线程2也读取了num的值,那么线程2读取到的num的值也是0,那此时线程1和线程2对num做递增操作时,都是对0进行加1,所以线程1和线程2的运行结果都是1。可以看到,虽然线程1和线程2都对num执行了一次++操作,但结果并不是2,而是1。

这就是导致我们上面程序的运行结果可能出现不是50000的情况的原因。

解决方案:synchronized

通过上面的分析我们知道了出现这种情况的原因是多个线程同时对num这个变量做操作导致的,那我们就知道该如何解决这个问题了:每次只允许一个线程执行该段代码,其他线程要等待这个线程执行完成后再执行该段代码。而synchronized关键字就可以做到这一点。

synchronized是java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。内置锁是排它锁,也就是当一个线程获取这个锁后, 其他线程必须等待该线程释放锁后才能获取该锁。

synchronized的使用方式

synchronized关键字作用在普通方法上,作用范围是整个方法,作用对象为调用这个方法的对象。即同一时间只能有一个线程进入该对象的这个方法,其他线程需在这个方法外等待上个线程退出该方法后才能进入。使用案例:

synchronized关键字作用在某段代码块上,作用范围是处于synchronized同步块中的代码段,作用对象是synchronized括号中的对象。使用案例:

synchronized关键字作用在静态方法上,作用范围是整个静态方法,作用对象是这个类的所有对象。使用案例:

一个对象未被当成锁时,就是一个普通的对象,此时,Mark Word中,是否偏向锁的值是0,锁标志位的值是01;

Mark Word

synchronized原理分析

到这里,我们已经会使用synchronized关键字来解决多线程并发的同步安全问题了,那synchronized是如何实现这种线程同步互斥功能的呢?

从synchronized的3种使用方式中,我们知道,synchronized实际是作用在对象上的,那锁的实现肯定也与对象在内存中的存储有关系。所以,在学习其原理之前,我们有必要先来了解下另一个名词:Mark Word。

Mark Word

JVM虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头又包括两部分信息:运行时数据和类型指针。运行时数据(如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等)的长度在32位和64位的虚拟机中分别占32Bits和64Bits(没有开启压缩指针的情况),官方称他为“Mark Word”。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

在32位虚拟机中,一个对象的Mark Word默认存储结构如下:

此时的对象处于无锁状态,32Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bits固定为0。当给该对象加上不同级别的锁之后(偏向锁、轻量级锁、重量级锁),Mark Word的存储结构如下:

我们可以根据Mark Word中的是否偏向锁(biased_lock)和锁标志位(lock)的值来判断对象当前的状态。

了解了Mark Word之后,我们就可以分析锁的实现原理了。

锁升级

从Mark Word的存储结构图中,我们可以看到,锁的级别分为:无锁状态、偏向锁、轻量级锁和重量级锁。一个对象从无锁状态->偏向锁->轻量级锁->重量级锁的转化,称为锁的升级。升级过程参考下图:

  1. 一个对象未被当成锁时,就是一个普通的对象,此时,Mark Word中,是否偏向锁的值是0,锁标志位的值是01;
  2. 当这个对象被当作锁来用(例如使用synchronized修饰),并且有一个线程A抢占到了这把锁,此时,该对象的锁就变为了偏向锁,Mark Word中,是否偏向锁的值被修改为1,锁标志位的值不变,还是01,线程ID的值存储的是线程A的id;
  3. 当线程A再次尝试获得锁时,发现当前对象的锁已经是偏向锁了,就会比较Mark Word中线程ID的值是否与当前线程的ID一致,若一致,则表示线程A已经获得了这把偏向锁,此处就可以直接执行同步代码块,而不需要再次加锁解锁处理;
  4. 当线程B尝试获得该锁时,发现当前对象的锁是偏向锁,并且Mark Word中线程ID也不是线程B的id,那么线程B会先通过一次CAS操作尝试获得锁。如果恰好此时线程A释放了锁,线程B就有可能抢占锁成功,那就会把Mark Word中线程ID的值修改为线程B的id;
  5. 如果线程B通过一次CAS操作抢占锁失败了,那就代表当前锁存在竞争,会被升级为轻量级锁。虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。JVM将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向Mark Word,详见下图。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为00,表示此对象处于轻量级锁定状态;

6.如果上述的更新操作失败了,表示竞争锁失败,此时,JVM会使用自旋锁(相当于一个循环)不断重试尝试获取锁的动作,JDK1.7开始,循环次数由JVM决定。如果自旋尝试获取锁成功,表示当前线程抢占到锁,可以执行同步代码块;

7.如果字段尝试获取锁后依然失败,表示当前锁竞争非常激烈,会被升级为重量级锁,Mark Word中,锁标志位的值被修改为10。此时,其他未抢占到锁的线程都会被阻塞。

以上就是synchronized的实现原理了。过程中涉及一个名词:CAS操作,意思就是拿期望值与旧值相比,如果相等,则更新为新值并返回true,否则不进行更新,返回false。

相关文章