****
一、概述
synchronized 是 Java 内建的同步机制,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能阻塞在那里
synchronized 同步锁在加锁和解锁的过程中,依赖于操作系统互斥锁(Mutex SLock)所实现的锁
二、synchronized 底层实现
-
使用 synchronized 保证线程安全,就是保证原子性,简单来说就是在执行过程中不会被其他线程干扰
-
通过反编译得知,synchronized 代码块是由一对 monitorenter/monitorexit 指令实现
-
线程通过执行 monitorenter 指令尝试获取 monitor 的所有权,当其被占用时就会处于锁定状态
三、监视器(monitor)
在 JVM 实现规范中关于 monitor 描述:每个对象都有一个监视器(monitor),线程通过执行monitorenter指令尝试获取monitor的所有权,当其被占用时就会处于被锁定状态
![在这里插入图片描述](https://img-blog.csdnimg.cn/cf331420e7bd4b4ea9586260adc504db.png#pic_center
获取 monitor 的所有权的过程如下
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者,代表持有锁;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加+1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
四、锁升级
在JVM底层实现锁的过程中,有三种类型的锁:偏斜锁、轻量级锁、重量级锁
在Java 6之前,synchronized的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,非常消耗系统资源。
在Java 6之后,在OracleJDK中,JVM对此synchronized进行了大刀阔斧地改进,提供了三种不同的Monitor实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
所谓锁的升级、降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
1、偏向锁
核心思想:
- 偏向锁的核心思想是“假设加锁的代码从始至终就只有一个线程在调用,如果发现有多于一个线程调用,再升级成轻量级锁”。
偏向锁是为了在单线程(没有出现多个线程并发)执行情况下,尽量减少不必要的轻量级锁执行路径,该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。因为轻量级锁的加锁与释放锁,也需要多次执行CAS原子指令。而偏向锁只需要在切换线程设置ThreadID的时候,执行一次CAS原子指令。所以,偏向锁的作用是在只有一个线程执行同步块时,进一步提高性能。
当没有线程并发出现时,默认会使用偏斜锁。JVM会利用CAS操作(compare and swap),在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
2、轻量级锁
(1)核心思想
- 轻量级”的概念,是相对于“使用操作系统互斥锁来实现的重量级锁”,但轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一把锁的情况,就会导致轻量级锁升级为重量级锁。
- 根据轻量级锁的实现,虽然轻量级锁不支持“并发”,遇到“并发”就要升级为重量级锁。但是轻量级锁可以支持多个线程以串行的方式访问同一个加锁对象。但是,每次执行,都消耗了重复的加锁与解锁的性能开销
- 例如:A线程可以先获取对象obj的轻量锁,然后A线程释放了锁,这个时候B线程来获取obj的轻量锁,可以成功获取obj的轻量锁。其余线程对这个obj轻量锁的获取,也以这种方式可以一直串行下去。之所以能实现这种串行,是因为有一个释放锁的动作。
(2)偏向锁和轻量级锁的区别
- 轻量级锁与偏向锁的区别:假设有一个加锁的方法,这个方法在运行的时候,并没有出现并发的情况,从始至终只有一个线程在调用,如果使用轻量级锁,每次调用完也要释放锁,下次调用还要重新获得锁。
锁的状态,保存在对象头中。在Hotspot虚拟机中,一个JAVA对象的存储结构,在内存中的存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)**
- lock标志位:2位二进制,锁状态标记位。
- ageJava对象年龄:在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。
- thread:持有偏向锁的线程ID。
- ptr_to_lock_record:指向栈中锁记录的指针。
(3)轻量级锁的加锁过程
- 在代码进入同步块的时候,如果对象锁状态为无锁状态(lock标志位“01”,biased_lock标志位“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方命名为Displaced Mark Word。
- 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中。
- 拷贝成功后,虚拟机将尝试将对象的Mark Word中的ptr_to_lock_record更新为指向Lock Record的指针,并将Lock record里的owner指针指向到对象的Mark Word。如果更新成功,则执行步骤4,否则执行步骤5。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的lock标志位设置为“00”,即表示此对象处于轻量级锁定状态
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否已经指向当前线程的栈帧。如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争该对象的锁,轻量级锁就要升级为重量级锁,lock标志位的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
(4)轻量级锁的解锁过程
- 通过CAS指令,尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
- 如果替换成功,整个同步过程就完成了
- 如果替换失败,说明有其他线程尝试过获取该锁,该锁已升级为重量级锁,那就要在释放锁的同时,通知其它线程重新参与锁的竞争。
3、重量级锁
依赖于操作系统互斥锁(Mutex Lock)所实现的锁。操作系统的互斥锁实现线程之间的切换,需要从用户态转换到核心态,切换成本非常高,状态之间的转换需要相对比较长的时间,这是早期Synchronized效率低的原因。因此,这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁,称之为“重量级锁”。