一、轻量级锁
轻量级锁是由偏向锁升级的来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
轻量级锁加锁的过程
- 在线程进入同步代码块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Reocrd)的空间,用于存储锁对象目前的markword的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如下图所示:
- 拷贝对象头中的markword到锁记录中;
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的 markword 更新为指向Lock Record的指针,并将Lock Record 里的 owner指针 指向对象的mark word。如果更新成功,则执行步骤4,否则执行步骤5;
- 如果跟新动作成功了,那么这个线程就拥有了该对象的锁,将对象markword的锁标志位设置为 “00” ,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示:
- 如果这个更新操作失败了,则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志位的状态值变为 “10” ,markword 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋锁来获取锁,自旋就是为了不让线程阻塞,而采取循环去获取锁的过程。
【轻量级锁总结】轻量级锁是jdk1.6引入的线程优化,当锁对象使用的是轻量级锁时,说明当前同步块虽然有多个线程访问,但是没有同时访问,不需要上升到操作系统级别的阻塞。
【轻量级锁加锁过程总结】首先在线程的栈帧中创建一个锁记录(Lock Record),然后将锁对象的markword复制到锁记录中,然后通过CAS尝试将markword变更为指向锁记录的指针,并将Lock Record中的owner指针指向对象的 markword。如果成功说明轻量级锁加锁成功,则修改锁标志位为 “00”,如果失败,说明有多个线程竞争,就要膨胀为重量级锁,锁标志位变为 “10”,当前线程还需要尝试通过自旋锁来获取锁。
二、自旋锁
- 概念:在锁膨胀之后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力——自旋锁。
- JVM会进行一次赌注:它会假设在不久的将来,线程可以得到这把锁。因此,虚拟机会让当前线程做几个空循环(这也是自旋的含义,也叫cpu空转),在经过若干次循环后,如果可以得到锁,那么就顺利进入临界区。如果还不能获得锁,才会真正地将线程在操作系统层面挂起,升级为重量级锁。
- JDK1.6中,通过 -XX:+UseSpinning 开启⾃旋锁,通过 -XX:PreBlockSpin=10 设置 ⾃旋次数;
JDK1.7后,已去掉此参数,由jvm⾃主控制;
三、锁消除
- 锁消除是一种更彻底的锁优化。Java虚拟机在JIT(即时编译)编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。通过锁消除,可以节省毫无意义的请求锁时间。比如,我们有可能在⼀个不可能存在并发竞争的场合使⽤Vector,⽽Vector内部使⽤了synchronized请求锁。例如如下代码:
- 在上述代码中的Vector,由于变量 vector 只在 createStrings() 函数中使⽤,因此,它只是⼀个单纯的 局部变量。局部变量是在线程栈上分配的,属于线程私有的数据,因此不可能被其他线程访问。所以,如果虚拟机检测到这种情况,就会将这些⽆⽤的锁操作去除掉。
锁消除涉及到对象分配过程中的知识点,逃逸分析。所谓逃逸分析就是观察某⼀个变量是否会逃出某⼀个作⽤域。
四、重量级锁
组件说明
整体处理流程
流程详解
- JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList(contention:争论,争夺)会被大量线程进行CAS访问(需要一直新增等待的线程),为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList(entry:进入)中作为候选竞争线程。
- Owner线程会在unlock时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList中的某个线程为 OnDeck线程(一般是最先进去的那个线程)。
- Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权力交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择性为称之为”竞争切换“。
- OnDeck线程获取到锁资源后会变成Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait()方法阻塞,则转移到 Waiting Queue 中,直到某个时刻通过 notify或者notifyAll唤醒,会重新进去EntryList中。处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
- Synchronized是非公平锁。Synchronized在线程进入 ContentionList 前,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能 直接抢占 OnDeck线程的锁资源。
【总结】多线程竞争锁对象时,首先会有一个线程抢到锁执行同步块,其他线程会自旋获取锁,如果自旋获取不到会进入ContentionList队列,当Owner线程unlock状态时,会将ContentionList尾部的一些线程放入EntryList,并且指定其中一个为OnDeck线程,该线程可以和正在竞争锁的线程一起竞争锁,OnDeck线程会一直等到竞争到锁,成为Owner线程。在Owner线程执行过程中,如果调用了wait() 方法,则进入WaitSet队列,直到锁对象调用notify或者notifyAll,再进入EntryList队列,等待成为OnDeck线程参与竞争。