上一篇文章介绍了多线程的概念及synchronized
的使用方法《synchronized的使用(一)》,可是仅仅会用仍是不够的,只有了解其底层实现才能在开发过程当中指挥若定,因此本篇探讨synchronized
的实现原理及锁升级(膨胀)的过程。html
synchronized
是依赖于JVM
来实现同步的,在同步方法和代码块的原理有点区别。java
咱们在代码块加上synchronized
关键字数组
public void synSay() {
synchronized (object) {
System.out.println("synSay----" + Thread.currentThread().getName());
}
}
复制代码
编译以后,咱们利用反编译命令javap -v xxx.class
查看对应的字节码,这里为了减小篇幅,我就只粘贴对应的方法的字节码。安全
public void synSay();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: getfield #2 // Field object:Ljava/lang/String;
4: dup
5: astore_1
6: monitorenter
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: new #4 // class java/lang/StringBuilder
13: dup
14: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
17: ldc #6 // String synSay----
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokestatic #8 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
25: invokevirtual #9 // Method java/lang/Thread.getName:()Ljava/lang/String;
28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: aload_1
38: monitorexit
39: goto 47
42: astore_2
43: aload_1
44: monitorexit
45: aload_2
46: athrow
47: return
Exception table:
from to target type
7 39 42 any
42 45 42 any
LineNumberTable:
line 21: 0
line 22: 7
line 23: 37
line 24: 47
LocalVariableTable:
Start Length Slot Name Signature
0 48 0 this Lcn/T1;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 42
locals = [ class cn/T1, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
复制代码
能够发现synchronized
同步代码块是经过加monitorenter
和monitorexit
指令实现的。
每一个对象都有个**监视器锁(monitor) **,当monitor
被占用的时候就表明对象处于锁定状态,而monitorenter
指令的做用就是获取monitor
的全部权,monitorexit
的做用是释放monitor
的全部权,这二者的工做流程以下:
monitorenter:bash
monitor
的进入数为0,则线程进入到monitor
,而后将进入数设置为1
,该线程称为monitor
的全部者。monitor
(即monitor
进入数不为0),而后该线程又从新进入monitor
,则将monitor
的进入数+1
,这个即为锁的重入。monitor
,则该线程进入到阻塞状态,知道monitor
的进入数为0,该线程再去从新尝试获取monitor
的全部权。monitorexit:执行该指令的线程必须是monitor
的全部者,指令执行时,monitor
进入数-1
,若是-1
后进入数为0
,那么线程退出monitor
,再也不是这个monitor
的全部者。这个时候其它阻塞的线程能够尝试获取monitor
的全部权。多线程
在方法上加上synchronized
关键字并发
synchronized public void synSay() {
System.out.println("synSay----" + Thread.currentThread().getName());
}
复制代码
编译以后,咱们利用反编译命令javap -v xxx.class
查看对应的字节码,这里为了减小篇幅,我就只粘贴对应的方法的字节码。app
public synchronized void synSay();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String synSay----
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokestatic #7 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
18: invokevirtual #8 // Method java/lang/Thread.getName:()Ljava/lang/String;
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
LineNumberTable:
line 20: 0
line 21: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this Lcn/T1;
复制代码
从字节码上看,加有synchronized
关键字的方法,常量池中比普通的方法多了个ACC_SYNCHRONIZED
标识,JVM
就是根据这个标识来实现方法的同步。
当调用方法的时候,调用指令会检查方法是否有ACC_SYNCHRONIZED
标识,有的话线程须要先获取monitor
,获取成功才能继续执行方法,方法执行完毕以后,线程再释放monitor
,同一个monitor
同一时刻只能被一个线程拥有。布局
synchronized
同步代码块的时候经过加入字节码monitorenter
和monitorexit
指令来实现monitor
的获取和释放,也就是须要JVM经过字节码显式的去获取和释放monitor实现同步,而synchronized同步方法的时候,没有使用这两个指令,而是检查方法的ACC_SYNCHRONIZED
标志是否被设置,若是设置了则线程须要先去获取monitor,执行完毕了线程再释放monitor,也就是不须要JVM去显式的实现。
这两个同步方式实际都是经过获取monitor和释放monitor来实现同步的,而monitor的实现依赖于底层操做系统的mutex
互斥原语,而操做系统实现线程之间的切换的时候须要从用户态转到内核态,这个转成过程开销比较大。
线程获取、释放monitor
的过程以下:post
线程尝试获取monitor
的全部权,若是获取失败说明monitor
被其余线程占用,则将线程加入到的同步队列中,等待其余线程释放monitor
,当其余线程释放monitor
后,有可能恰好有线程来获取monitor
的全部权,那么系统会将monitor
的全部权给这个线程,而不会去唤醒同步队列的第一个节点去获取,因此synchronized
是非公平锁。若是线程获取monitor
成功则进入到monitor
中,而且将其进入数+1
。
关于什么是公平锁、非公平锁能够参考一下美团技术团队写的《不可不说的Java“锁”事》
到这里咱们也清楚了synchronized
的语义底层是经过一个monitor
的对象完成,其实wait
、notiyf
和notifyAll
等方法也是依赖于monitor
对象来完成的,这也就是为何须要在同步方法或者同步代码块中调用的缘由(须要先获取对象的锁,才能执行),不然会抛出java.lang.IllegalMonitorStateException
的异常
咱们知道了线程要访问同步方法、代码块的时候,首先须要取得锁,在退出或者抛出异常的时候又必须释放锁,那么锁究竟是什么?又储存在哪里?
为了解开这个疑问,咱们须要进入Java虚拟机(JVM) 的世界。在HotSpot
虚拟机中,Java
对象在内存中储存的布局能够分为3
块区域:对象头、实例数据、对齐填充。synchronized使用的锁对象储存在对象头中
对象头的数据长度在32
位和64
位(未开启压缩指针)的虚拟机中分别为32bit
和64bit
。对象头由如下三个部分组成:
GC
分代年龄、锁标志位、线程持有的锁、偏向线程ID
、偏向时间戳、对象分代年龄等。注意这个Mark Word结构并非固定的,它会随着锁状态标志的变化而变化,并且里面的数据也会随着锁状态标志的变化而变化,这样作的目的是为了节省空间。在32
位虚拟机下,Mark Word
的结构和数据可能为如下5
种中的一种。
在64
位虚拟机下,Mark Word
的结构和数据可能为如下2
种中的一种。
这里重点注意是否偏向锁和锁标志位,这两个标识和synchronized
的锁膨胀息息相关。
储存着对象的实际数据,也就是咱们在程序中定义的各类类型的字段内容。
HotSpot
虚拟机的对齐方式为8
字节对齐,即一个对象必须为8
字节的整数倍,若是不是,则经过这个对齐填充来占位填充。
上文介绍的 "synchronized
实现原理" 实际是synchronized实现重量级锁的原理,那么上文频繁提到monitor
对象和对象又存在什么关系呢,或者说monitor
对象储存在对象的哪一个地方呢?
在对象的对象头中,当锁的状态为重量级锁的时候,它的指针即指向monitor
对象,如图:
monitor
对象?答案:是的。
JVM
对
synchronized
的优化,咱们知道重量级锁的实现是基于底层操做系统的
mutex
互斥原语的,这个开销是很大的。因此
JVM
对
synchronized
作了优化,
JVM
先利用对象头实现锁的功能,若是线程的竞争过大则会将锁升级(膨胀)为重量级锁,也就是使用
monitor
对象。固然
JVM
对锁的优化不只仅只有这个,还有引入适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等。
那么锁的是怎么进行膨胀的或者依据什么来膨胀,这也就是本篇须要介绍的重点,首先咱们须要了解几个概念。
自旋:当有个线程A
去请求某个锁的时候,这个锁正在被其它线程占用,可是线程A
并不会立刻进入阻塞状态,而是循环请求锁(自旋)。这样作的目的是由于不少时候持有锁的线程会很快释放锁的,线程A
能够尝试一直请求锁,不必被挂起放弃CPU
时间片,由于线程被挂起而后到唤醒这个过程开销很大,固然若是线程A
自旋指定的时间尚未得到锁,仍然会被挂起。
自适应性自旋:自适应性自旋是自旋的升级、优化,自旋的时间再也不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。例如线程若是自旋成功了,那么下次自旋的次数会增多,由于JVM
认为既然上次成功了,那么此次自旋也颇有可能成功,那么它会容许自旋的次数更多。反之,若是对于某个锁,自旋不多成功,那么在之后获取这个锁的时候,自旋的次数会变少甚至忽略,避免浪费处理器资源。有了自适应性自旋,随着程序运行和性能监控信息的不断完善,JVM
对程序锁的情况预测就会变得愈来愈准确,JVM
也就变得愈来愈聪明。
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,可是被检测到不可能存在共享数据竞争的锁进行消除。
在使用锁的时候,须要让同步块的做用范围尽量小,这样作的目的是为了使须要同步的操做数量尽量小,若是存在锁竞争,那么等待锁的线程也能尽快拿到锁。
所谓轻量级锁是相对于使用底层操做系统mutex
互斥原语实现同步的重量级锁而言的,由于轻量级锁同步的实现是基于对象头的Mark Word。那么轻量级锁是怎么使用对象头来实现同步的呢,咱们看看具体实现过程。
获取锁过程:
Mark Word
拷贝到线程的锁记录(Lock Recored)中。CAS
操做尝试将对象的Mark Word
更新为指向Lock Record
的指针。若是这个更新成功了,则执行步骤4
,不然执行步骤5
。Mark Word
是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,能够直接进入同步块继续执行,不然说明这个锁对象已经被其其它线程抢占了。进行自旋执行步骤3
,若是自旋结束仍然没有得到锁,轻量级锁就须要膨胀为重量级锁,锁标志位状态值变为"10",Mark Word中储存就是指向monitor
对象的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。释放锁的过程:
CAS
操做将对象当前的Mark Word
和线程中复制的Displaced Mark Word
替换回来(依据Mark Word
中锁记录指针是否还指向本线程的锁记录),若是替换成功,则执行步骤2
,不然执行步骤3
。偏向锁的目的是消除数据在无竞争状况下的同步原语,进一步提升程序的运行性能。若是说轻量级锁是在无竞争的状况下使用CAS
操做区消除同步使用的互斥量,那么偏向锁就是在无竞争的状况下把整个同步都消除掉,连CAS
操做都不用作了。偏向锁默认是开启的,也能够关闭。
偏向锁"偏",就是"偏爱"的"偏",它的意思是这个锁会偏向于第一个得到它的程序,若是在接下来的执行过程当中,该锁没有被其余的线程获取,则持有偏向锁的线程将永远不须要再进行同步。
获取锁的过程:
Mark Word
是否为可偏向锁的状态,便是否偏向锁即为1即表示支持可偏向锁,不然为0表示不支持可偏向锁。Mark Word
储存的线程ID
是否为当前线程ID
,若是是则执行同步块,不然执行步骤3
。Mark Word
的ID
不是本线程的ID
,则经过CAS
操做去修改线程ID
修改为本线程的ID
,若是修改为功则执行同步代码块,不然执行步骤4
。锁释放的过程:
Mark Word
的锁记录指针改为当前线程的锁记录,锁升级为轻量级锁状态(00)。锁主要存在4
种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争的状况逐渐升级,这几个锁只有重量级锁是须要使用操做系统底层mutex
互斥原语来实现,其余的锁都是使用对象头来实现的。须要注意锁能够升级,可是不能够降级。
深刻理解Java虚拟机 Java的对象头和对象组成详解
JVM(三)JVM中对象的内存布局详解
JVM——深刻分析对象的内存布局
啃碎并发(七):深刻分析Synchronized原理
Java Synchronized实现原理
原文地址:ddnd.cn/2019/03/22/…