前言:
之前一直对sychronized的原理不是特别清楚,只知道在jdk1.6之前使用sychronized锁性能较低,后续进行了优化,加入了偏向锁、自旋等机制来提高sychronized性能,但是抱着模棱两可的心态总是很受的,于是特意学习了一波,充实自己,也分享于大家,本文主要是对sychronized特性以及实现原理进行剖析,本文会以代码结合反编译字节码的形式来进行分析
简介:
1.sychronized是我们经常用到的一种同步机制,用于保证线程安全
2.sychronized可以保证原子性、可见性、有序性
sychronized的使用方式有哪些?
1.修饰方法:普通方法;静态方法
3.修饰代码块
sychronized修饰方法和修饰代码块有何区别?
1.获取锁不一样:修饰普通方法获取的是当前对象锁;修饰静态方法获取的是类对象锁;修饰代码块获取的是sychronized(Object)括号中的对象锁
2.作用范围不一样:修饰方法作用范围较大;修饰代码块作用范围更小,可以更灵活地进行操作
3.底层实现原理不一样:修饰代码块是通过Monitor相关指令实现的;修饰方法是通过jvm方法表结构中ACC_SYNCHRONIZED访问标志位实现的
sychronized修饰代码块底层具体是如何实现的?
首先编写验证代码:
-
public class BlockSync
-
{
-
public void sync()
-
{
-
synchronized (this)
-
{
-
System.out.println("执行同步代码块的逻辑.");
-
}
-
}
-
}
然后将编译后的字节码文件放到linux服务上,通过javap反编译命令查看具体信息:
执行命令:
javap -p -v /home/BlockSync.class
打印信息如下:
-
Classfile /home/BlockSync.class
-
Last modified Jul 2, 2018; size 580 bytes
-
MD5 checksum 5ca0058a89445b19761e563d48903fec
-
Compiled from "BlockSync.java"
-
public class com.synchronize.service.BlockSync
-
SourceFile: "BlockSync.java"
-
minor version: 0
-
major version: 49
-
flags: ACC_PUBLIC, ACC_SUPER
-
Constant pool:
-
.... //常量池内容太多,这里手动忽略了
-
{
-
public com.synchronize.service.BlockSync();
-
descriptor: ()V
-
flags: ACC_PUBLIC
-
Code:
-
stack=1, locals=1, args_size=1
-
0: aload_0
-
1: invokespecial #8 // Method java/lang/Object."<init>":()V
-
4: return
-
LineNumberTable:
-
line 11: 0
-
LocalVariableTable:
-
Start Length Slot Name Signature
-
0 5 0 this Lcom/synchronize/service/BlockSync;
-
public void sync();
-
descriptor: ()V
-
flags: ACC_PUBLIC
-
Code:
-
stack=2, locals=2, args_size=1
-
0: aload_0
-
1: dup
-
2: astore_1
-
3: <span style="color:#ff0000;">monitorenter</span>
-
4: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
-
7: ldc #21 // String 执行同步代码块的逻辑.
-
9: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
-
12: aload_1
-
13: <span style="color:#ff0000;">monitorexit</span>
-
14: goto 20
-
17: aload_1
-
18: <span style="color:#ff0000;">monitorexit </span>
-
19: athrow
-
20: return
-
Exception table:
-
from to target type
-
4 14 17 any
-
17 19 17 any
-
LineNumberTable:
-
line 15: 0
-
line 17: 4
-
line 15: 12
-
line 19: 20
-
LocalVariableTable:
-
Start Length Slot Name Signature
-
0 21 0 this Lcom/synchronize/service/BlockSync;
-
}
由上面标红的打印信息可以看出:
1.sychronized修饰代码块,是通过调用monitorenter指令获取到对象锁
2.通过调用monitorexit指令释放对象锁
3.这里一个monitorenter对应两个monitorexit,后面一个monitorexit是为了防止异常中断情况,这时也要释放锁
sychronized修饰普通方法底层具体是如何实现的?
首先编写验证代码:
-
public class MethodSync
-
{
-
public synchronized void sync()
-
{
-
System.out.println("执行普通方法的同步逻辑.");
-
}
-
}
然后将编译后的字节码文件放到linux服务上,通过javap反编译命令查看具体信息:
执行命令:
javap -p -v /home/MethodSync.class
打印信息如下:
-
Classfile /home/MethodSync.class
-
Last modified Jul 2, 2018; size 550 bytes
-
MD5 checksum ca3aa080a3bfa2b16f99d093acf5811d
-
Compiled from "MethodSync.java"
-
public class com.synchronize.service.MethodSync
-
SourceFile: "MethodSync.java"
-
minor version: 0
-
major version: 49
-
flags: ACC_PUBLIC, ACC_SUPER
-
Constant pool:
-
.... //常量池内容太多,这里手动忽略了
-
{
-
public com.synchronize.service.MethodSync();
-
descriptor: ()V
-
flags: ACC_PUBLIC
-
Code:
-
stack=1, locals=1, args_size=1
-
0: aload_0
-
1: invokespecial #8 // Method java/lang/Object."<init>":()V
-
4: return
-
LineNumberTable:
-
line 11: 0
-
LocalVariableTable:
-
Start Length Slot Name Signature
-
0 5 0 this Lcom/synchronize/service/MethodSync;
-
public synchronized void sync();
-
descriptor: ()V
-
flags: ACC_PUBLIC, <span style="color:#ff0000;">ACC_SYNCHRONIZED</span>
-
Code:
-
stack=2, locals=1, args_size=1
-
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
-
3: ldc #21 // String 执行普通方法的同步逻辑.
-
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
-
8: return
-
LineNumberTable:
-
line 15: 0
-
line 16: 8
-
LocalVariableTable:
-
Start Length Slot Name Signature
-
0 9 0 this Lcom/synchronize/service/MethodSync;
-
}
由上面标红的打印信息可以看出:
1.sychronized修饰普通方法时,没有使用monitor指令实现锁的获取与释放,而是使用了ACC_SYNCHRONIZED标识,表示该方法是同步方法
2.当方法有ACC_SYNCHRONIZED标识,则进入方法前尝试获取与对象关联的monitor,当方法执行完成则释放monitor,如果发生异常则释放monitor
sychronized修饰静态方法底层具体是如何实现的?
首先编写验证代码:
-
public class StaticMethodSync
-
{
-
public synchronized static void sync()
-
{
-
System.out.println("执行静态方法的同步逻辑.");
-
}
-
}
然后将编译后的字节码文件放到linux服务上,通过javap反编译命令查看具体信息:
执行命令:
java -p -v /home/StaticMethodSync.class
打印信息如下:
-
Classfile /home/StaticMethodSync.class
-
Last modified Jul 2, 2018; size 558 bytes
-
MD5 checksum cdf8b4f17971ac67cee30decc2167515
-
Compiled from "StaticMethodSync.java"
-
public class com.synchronize.service.StaticMethodSync
-
SourceFile: "StaticMethodSync.java"
-
minor version: 0
-
major version: 49
-
flags: ACC_PUBLIC, ACC_SUPER
-
Constant pool:
-
.... //常量池内容太多,这里手动忽略了
-
{
-
public com.synchronize.service.StaticMethodSync();
-
descriptor: ()V
-
flags: ACC_PUBLIC
-
Code:
-
stack=1, locals=1, args_size=1
-
0: aload_0
-
1: invokespecial #8 // Method java/lang/Object."<init>":()V
-
4: return
-
LineNumberTable:
-
line 11: 0
-
LocalVariableTable:
-
Start Length Slot Name Signature
-
0 5 0 this Lcom/synchronize/service/StaticMethodSync;
-
public static synchronized void sync();
-
descriptor: ()V
-
flags: ACC_PUBLIC, ACC_STATIC, <span style="color:#ff0000;">ACC_SYNCHRONIZED</span>
-
Code:
-
stack=2, locals=0, args_size=0
-
0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;
-
3: ldc #21 // String 执行静态方法的同步逻辑.
-
5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
-
8: return
-
LineNumberTable:
-
line 15: 0
-
line 16: 8
-
LocalVariableTable:
-
Start Length Slot Name Signature
-
}
由上面标红的打印信息可以看出:
1.sychronized修饰静态法时,和修饰普通方法使用的同一种机制,即使用ACC_SYNCHRONIZED标识该方法是同步方法
sychronized底层访问Monitor具体是如何操作的?
1.多个线程访问对象的Monitor时,Monitor会将这些线程存储到不同容器中:
Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中
Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中
Wait Set:哪些调用wait方法被阻塞的线程被放置在这里
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
Owner:当前已经获取到所资源的线程被称为Owner
!Owner:当前释放锁的线程
2.JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),在并发情况下,Contention List会被大量线程进行CAS访问,为了降低对尾部元素的竞争,将一部分线程移动到Entry List中作为候选竞争线程
3.Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程),Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”
4.OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中
5.处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)
sychronized的非公平性如何实现的?
1.Synchronized在线程进入ContentionList时,当前线程会先尝试自旋获取锁,如果获取不到就进入ContentionList
2.自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源
sychronized的可重入性是如何实现的?
1.线程已经获取到对象的Monitor后,后续再次获取相同的对象锁时,就不需要再次发起锁请求,因为单线程下是同步执行的,所以可以减少不必要的资源消耗
2.如果是在偏向锁或轻量级锁的情况下,可以参照:JAVA并发-sychronized优化,其中偏向锁、轻量级锁加锁过程中,都会判断是否是同一线程,如果是同一线程则直接执行。