互斥锁的本质是共享资源。
当有多个线程同时对一个资源进行操作时,为了线程安全,要对资源加锁。
更多基础内容参看上文《深入了解Java线程锁(一)》
接下来,我们来看看两个线程抢占重量级锁的情形:
上图讲述了两个线程ThreadA和ThreadB两个线程在争抢锁,
ThreadA进入来临界区,可以执行资源代码;
ThreadB没有抢到锁,进入BLOCKED队列,等待唤醒。
当ThreadA执行完后,释放锁,去唤醒ThreadB。
这是重量级锁的流程。
因为它会带来阻塞和唤醒,会带来开销,所以叫“重量级”锁。
加锁会带来性能开销,Java 1.6之前只有重量级锁。
Java 1.6 之后做了优化,出现了“轻量级锁 和 偏向锁”,能够减少重量级锁的获得和释放的性能开销。
我们在线程竞争的情况下,才会用重量级锁来处理。
我们是否能在加重量级锁之前,先去试着判断一下,是否存在竞争呢?
如果不存在,就不必使用重量级锁了,就可以带来性能优化了。
锁的升级
ThreadA 和ThreadB 加锁不是一步到位直接重量级锁。
而是先通过偏向锁抢占,当A获取来偏向锁后,B去抢占;
如果不成功,B会升级到轻量级锁继续尝试抢占;
如果不成功,B会去重量级锁抢占;
如果不成功,进入阻塞队列,等待唤醒。
线程最终的结果是要实现互斥的特性,我们希望能在线程B阻塞之前抢占到锁,如果可以,那么就不需要进行重量级锁的BLOCKED和唤醒,就会提高性能。
加锁一定会带来性能开销,怎么优化?—— 不加锁!(在不加锁的情况下解决线程安全问题)
锁的升级以及优化的目的,就是如何能在线程B不阻塞的情况下也能抢占到锁。
偏向锁
偏向锁是偏向的特性。
如果我们某个线程获取的锁是偏向锁,在对象头中会存储线程ID
,偏向锁标志为1
偏向锁是不存在竞争的情况下可以使用。
名字叫做“偏向”就是当前偏向于某个线程,就将其线程ID存放在对象头中。
这种情况(加了锁但是没有竞争)比较少,所以默认情况下,偏向锁是关闭的。
我们来看 偏向锁的获得和撤销流程图
图中有两个线程A和B,中间是对象头。
- 线程A去访问同步代码块的时候,会检查对象头中是否存储了线程A,如果说没有存储的话,它会通过CAS替换对象头,把线程A的ID存储到对象头中。
CAS 相当于是一个乐观锁的概念,会去比较预期数据和原始数据是否一致,如果一致则修改,不一致则修改失败。CAS是原子性的操作。
- 替换成功的话,对象头中会存储A线程的ID和锁的标记
101
。
我们来实际看一下:
因为偏向锁默认关系,所以要先打开偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
执行没有线程抢占的代码:(与此代码相关背景请见《深入了解JAVA线程锁(一)》)
import org.openjdk.jol.info.ClassLayout;
public class MyClassLayout {
public static void main(String[] args) {
MyClassLayout myClassLayout = new MyClassLayout();
synchronized (myClassLayout) {
System.out.println(ClassLayout.parseInstance(myClassLayout).toPrintable());
}
}
}
执行结果为:
demo.MyClassLayout object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 a0 c5 45 (00000101 10100000 11000101 01000101) (1170579461)
4 4 (object header) cb 01 00 00 (11001011 00000001 00000000 00000000) (459)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们可以看到当前是 101
,是偏向锁。
- 线程A可以执行同步代码块。
- 此时如果有线程B在线程A已经获得偏向锁的情况下,访问同步代码块。
- 检查对象头中是否存储了线程B,如果没有的话,使用CAS替换对象头,可能成果也可能失败。
- 如果替换失败的话,线程B就要撤销原来A获得的偏向锁。
- 线程B暂停线程A,进行撤销。这里的撤销有两种,一种是撤销,一种是升级。
- 如果撤销成果,则线程B会获得偏向锁。什么情况可以成功撤销:
① 线程A已经执行结束;
② 线程A处于全局安全点,表示已经没有指令在运行。 - 如果有线程A的指令在执行的话,就无法撤销(即撤销失败)。那么就会升级到轻量级锁,会把对象头标记改为轻量级锁的标记。
偏向锁中没有阻塞,只有一个CAS。
轻量级锁中也没有阻塞,而是有个重试,即多次CAS(自旋锁)。
我们来看轻量级锁的流程图:
- 同时两个线程A、B过来。
- 线程A进来访问同步代码块,会在线程的栈针中分配一个rock record空间,把对象头复制到rock record空间里。
把当前的指针,指向栈中锁记录。
- 抢占锁在轻量级锁范围内所做的事就是通过CAS修改 Mark Word中指向 lock record 的指针。 如果成功,表示当前线程A获得轻量级锁,它会把对象头存储指向栈中记录的指针。状态是
00
。 - 因为当前锁已被线程A获得,线程B竞争失败,它会有重试机制。(轻量级锁的多次CAS就是体现中这里,它叫自旋锁)
很多情况下,线程获得锁再释放锁的时间很短。这种情况下,我们通过重试去抢占,相比通过阻塞去抢占,更划算,所以通过重试去达到抢占锁的目的。
但是重试也是有次数的,不能无限重试。在JDK1.6之前,自旋锁默认重试10次。JDK1.6之后,引入来自适应自旋锁,会根据上一次抢占锁时间长,这次自适应时间长一些,否则会短一些。
- 如果线程B自旋失败,就会锁膨胀,升级为重量级锁。
Synchronized是个非公平锁,允许插队。
5. 重量级锁有个 Monitor 监视器,线程A、B会进行 monitor enter的抢占,
6. 如果线程A在monitor enter中抢占成功,就会持有锁对象,可以执行资源代码块。
7. 这时,没有抢到锁的线程B,会阻塞,然后放入到队列中。
8. 当线程A执行完资源代码释放后,阻塞的线程B会被唤醒,会进行新一轮的抢占。
我们通过代码来看锁膨胀的示例:
public class MyLockDemo {
public static void main(String[] args) throws InterruptedException {
MyLockDemo myLockDemo = new MyLockDemo();
new Thread(()->{
synchronized (myLockDemo){
System.out.println("Thread-A caught the lock.");
System.out.println(ClassLayout.parseInstance(myLockDemo).toPrintable());
}
},"Thread-A").start();
//sleep 10秒,则是轻量级锁
Thread.sleep(10000);
synchronized (myLockDemo){
System.out.println("main is catching.");
System.out.println(ClassLayout.parseInstance(myLockDemo).toPrintable());
}
}
}
执行上述代码,结果为:
Thread-A caught the lock.
LockDemo.MyLockDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) d0 f1 1f 62 (11010000 11110001 00011111 01100010) (1646260688)
4 4 (object header) 1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
main is catching.
LockDemo.MyLockDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 38 f7 af 60 (00111000 11110111 10101111 01100000) (1622144824)
4 4 (object header) 1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
我们可以看到最末三位是 000
,所以是轻量级锁。
接下来,我们把代码中的sleep 注释掉,再看结果:
main is catching.
LockDemo.MyLockDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a a1 70 b3 (00001010 10100001 01110000 10110011) (-1284464374)
4 4 (object header) a8 02 00 00 (10101000 00000010 00000000 00000000) (680)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread-A caught the lock.
LockDemo.MyLockDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a a1 70 b3 (00001010 10100001 01110000 10110011) (-1284464374)
4 4 (object header) a8 02 00 00 (10101000 00000010 00000000 00000000) (680)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
这时我们发现末尾三位是 010
,是重量级锁无疑了。