一、java对象头
- 锁的获取和撤销会关系到对象头,所以先来看看对象头。
- Java对象头Mark Word字段存放内容:
- 根据竞争状态的激烈程度,锁会自动进行升级,锁不能降级(为了提高锁获取)
一、偏向锁
- 由于老版本的内建锁synchronized存在最大的问题:在存在线程竞争的情况下会出现线程的阻塞以及唤醒带来的性能问题,这是一种互斥同步(阻塞同步)。
- 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让线程获取锁的开销降低引入偏向锁。
- JDK1.6之后引入了偏向锁的概念,偏向锁是一种乐观锁(假设所有线程访问共享资源时不会出现冲突)。
1.偏向锁的获取
- 1.因为锁标志位为01的时候会存储线程ID,当一个线程进入同步代码块的时候判断对象头里的Mark Word是否存储着指向当前线程ID,如果有就让当前线程获得锁。(这里可以解释,当同一个线程不断重入偏向锁的时候不需要进行CAS操作,只需要进行一个判断就可以获得锁)最后执行步骤5。如果没有,则进行步骤2的判断。
- 2.判断Mark Word中偏向锁标识是否设置为1(表示当前是偏向锁), 如果是的话指向步骤3 ,否则执行步骤4
- 3.尝试使用CAS将对象头的偏向锁指向当前线程, 成功表示获取偏向锁成功, 则执行步骤5, 失败则表示存在竞争, 偏向锁要升级为轻量级锁, 偏向锁撤销和升级的流程下面再进行说明
- 4.此时表示需要CAS竞争锁。
- 5.执行同步代码块
2.偏向锁的撤销或者升级
- 偏向锁可以膨胀升级为轻量级锁,前提是有第二个线程试图获取此偏向锁。
- 当两个线程试图获取同一个偏向锁的时候,如下图所示:
- 在线程1执行同步代码块之前的步骤上面已经解释了,下面从线程2尝试获取锁开始:
- 1.判断Mark Word中是否有指向自己的线程ID,否。
- 2.判断当前锁是否为偏向锁,是。那么线程2会用CAS替换来尝试获取锁。 CAS替换Mark Word成功表示获取偏向锁成功, 这里由于对象头中Mark Word已经指向了线程1, 所以替换失败, 需要进行撤销操作。
- 3.撤销偏向锁, 需要等待全局安全点(safepoint),就是当前时间点上没有字节码正在执行。
- 4.撤销的时候需要等线程1全部跑完run方法,然后暂停线程1,如果线程1已经终止了,则将锁对象的对象头设置为无锁状态(方便下一个线程进来)。如果对象1还未终止,恢复线程1,并将锁升级为轻量级锁,然后和线程2一起CAS竞争轻量级锁。
- 我个人认为,拥有偏向锁的线程不会自动释放锁,第一个线程申请偏向锁的时候会成功,第二个线程来申请偏向锁的时候,不管第一个线程执行完毕还是未完毕,都要进行一次撤销或者升级的操作,因为当时Mark Wrod中保存的指针不是线程2,所以只有撤销为无锁状态,线程2才能成为新的偏向锁偏向的线程
- 锁的对象头中偏向着线程1,因为它不知道线程1什么时候来,所以一直偏向着,就算线程1已经死亡了。所以撤销锁的时候,先检查对象头所指向的线程是否存活,如果不存活,那么偏向锁撤销为无锁,如果存在,那么线程1目前没有拿着锁而在干别的事情,这样锁就在不同时间段被不同线程访问了升级为轻量级锁,线程2就拿到了锁。
- 下面是我理解的偏向锁整个的流程图:
3.偏向锁关闭
偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;
二、轻量级锁
- 轻量级锁可以允许多个线程尝试获取同一个锁,但是必须是不同时间段的。
- 如果在同一时间有多个锁同时竞争轻量级锁,那么轻量级锁就会膨胀变为重量级锁。
- 先看一张流程图再详解:
1.轻量级加锁操作
- 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word拷贝到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
- 为什么要拷贝mark word?
其实很简单,原因是为了不想在lock与unlock这种底层操作上再加同步。(我的理解是如果每个线程进来都不拷贝,直接对内容进行更改的话,可能会出错) - 在拷贝完object mark word之后,JVM做了一步交换指针的操作,即流程中第一个橙色矩形框内容所述。
- 上述操作如下图所示:
- 如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。
- 等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。
这就是自旋锁,尝试获取锁的线程,在没有获得锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,如果还没有获得锁,则才被挂起,获得锁,则执行代码。 - 虽然自旋可以防止阻塞,节省从内核态到用户态的开销,但是如果长时间自旋,则会导致CPU长时间做一个同样的无用循环操作。浪费CPU的资源。这时候引入了自适应自旋。
自适应自旋
- 此操作为了防止长时间的自旋,在自旋操作上加了一些限制条件。
- 比如一开始给线程自旋的时间是10秒,如果线程在这个时间内获得了锁,那么就认为这个线程比较容易获得锁,就会适当的加长它的自旋时间。
- 如果这个线程在规定时间内没有获得到锁,并且阻塞了。那么就认为这个线程不容易获得锁,下次当这个线程进行自旋的时候会减少它的自旋时间
2.轻量级锁解锁操作
- 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头。
- 如果成功,则表示没有竞争发生。成功替换,等待下一个线程获取锁。
- 如果失败,表示当前锁存在竞争(因为自旋失败的线程已经将对象头中的轻量级锁00改变为了10),锁就会膨胀成重量级锁。
- 因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再 恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后 会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
三、重量级锁
- 重量级锁是JVM中为基础的锁实现。在这种状态下,JVM虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的 时候,唤醒这些线程。
- Java线程的阻塞以及唤醒,都是依靠操作系统来完成的。举例来说,对于符合posix接口的操作系统(如macOS和绝大 部分的Linux),上述操作通过pthread的互斥锁(mutex)来实现的。此外,这些操作将涉及系统调用,需要从操作系统 的用户态切换至内核态,其开销非常之大。
- 为了尽量避免昂贵的线程阻塞、唤醒操作,JVM会在线程进入阻塞状态之前,以及被唤醒之后竞争不到锁的情况 下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。如果此时锁恰好被释放了,那么当前线程便无须进入 阻塞状态,而是直接获得这把锁。
四、总结
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。
- 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适 应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
- 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对 象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
- 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过 程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。