使用方式
synchronzied是用来修饰对象的,通过改变对象的锁标志位来判断该线程是否能获取对象锁然后进入synchronzied修饰的代码块中,底层是通过操作系统的monitor(监视器)来保证同步控制的,所以比较耗费系统性能资源.
如图:
java虚拟机对monitorenter和monitorexit指令的描述解释
官网地址:
https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.ht...
monitorenter
Description
The objectref must be of type reference.
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
monitorexit
Description
The objectref must be of type reference.
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
大致意思是对象必须为引用类型,只有当该对象引用关联的监视器的条目计数为零,该线程才能进入并将条目计数设置为1,该线程重复进入会增加计数.其他线程进入到该对象引用关联的监视器内,该线程会被阻塞,直到该监视器的条目计数为0,才能再次尝试获取所有权.
代码示例
作用在方法上和作用在代码块内,非静态方法是作用在自身实例对象上,静态方法是作用在自身类对象上.
@Slf4j
public class SynchronizedDemo {
public void method() {
synchronized (this) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("实例对象锁同步代码块执行了...");
}
}
public static void methodStatic() {
synchronized (SynchronizedDemo.class) {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("静态类锁同步代码块执行了...");
}
}
public static synchronized void syncStatic() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("静态同步方法执行了...");
}
public synchronized void sync() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("实例同步方法执行了...");
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
new Thread(() -> {
SynchronizedDemo.syncStatic();
}).start();
new Thread(() -> {
SynchronizedDemo.methodStatic();
}).start();
new Thread(() -> {
synchronizedDemo.sync();
}).start();
new Thread(() -> {
synchronizedDemo.method();
}).start();
}
}
main方法执行结果,如图:
结论:
实例同步方法和方法内部使用实例对象锁操作的引用对象都是this,静态同步方法和方法内部使用类对象锁操作的引用对象都是SynchronizedDemo.class
对象结构
java的Hotspot虚拟机的对象头主要包括两部分:Mark Word(标记字段)、Klass Pointer(类型指针)
32位系统中:
Mark Word = 4 bytes = 32 bits,对象头 = 8 bytes = 64 bits;
64位系统中:
Mark Word = 8 bytes = 64 bits ,对象头 = 16 bytes = 128bits;
Mark Word最后3位或最后两位对应分别对应锁的状态
正常->(001)偏向锁(101)->轻量级锁(00)->重量级锁(10)
代码示例
使用jol的sdk打印对象头信息
org.openjdk.jol
jol-core
0.10
代码示例如下:
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class MarkWordDemo {
public static void main(String[] args) {
MarkWordDemo markWordDemo = new MarkWordDemo();
toBinary(markWordDemo);
}
public static void toBinary(Object o) {
System.out.println("===================");
System.out.println("markWordDemo的hashcode值=" + Integer.toBinaryString(o.hashCode()));
long headSize = ClassLayout.parseInstance(o).headerSize();
System.out.println(headSize);
for (long off = 0L; off < headSize; off += 4L) {
int word = VM.current().getInt(o, off);
System.out.printf("%s%n", " (" + toBinary(word >> 24 & 255) + " " + toBinary(word >> 16 & 255) + " " + toBinary(word >> 8 & 255) + " " + toBinary(word >> 0 & 255) + ")");
}
}
private static String toBinary(int x) {
String s = Integer.toBinaryString(x);
int deficit = 8 - s.length();
for (int c = 0; c < deficit; ++c) {
s = "0" + s;
}
return s;
}
}
无锁状态下的对象头信息是完全能比对上的,其他状态下的头信息大家也可以参照示例代码写个main方法测试下.
锁类型
之前提到synchronized锁同步机制是用到系统的监视器来实现的,性能上是比较废资源的.在很多情况下,java代码也不存在多线程的对象锁资源的竞争,所以jvm对synchronized关键字做了很多性能情况下的优化,包括偏向锁、轻量级锁、重量级锁等情况.
ObjectMonitor对象openjdk的源码地址:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08...
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0;
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
偏向锁
场景:
在没有多线程竞争的情况下,系统默认(-XX:+UseBiasedLocking)会采用加偏向锁的方式来获取锁.
获取锁步骤
1.检测Mark Word是否为可偏向状态,及是否为偏向锁1,锁标识位是否为01,如果不为偏向锁标识不为1,则走轻量级锁获取锁流程
2.如果为可偏向状态,则判断锁对象的线程是否为当前线程,如果是,则直接走步骤5,否则执行步骤3
3.如果锁对象的Mark Word线程id不为当前线程id,如果Mark Word线程id为空,则直接走CAS操作竞争锁,如果竞争成功,对象锁的Mark Word更新为当前线程的id,然后直接走步骤5.否则走步骤4;如果Mark Word线程id不为空,则直接走轻量级锁流程,并将重偏向的记录+1,如果默认类对象阈值累计达到了20且未超过规定间隔时间,后续将走批量重偏向流程,则直接走CAS操作竞争锁,如果竞争成功,对象锁的Mark Word更新为当前线程的id,然后直接走步骤5.否则走步骤4.
4.通过CAS竞争锁失败,证明当前存在多线程竞争的情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后阻被阻塞在安全点的线程继续往下执行同步代码块.
5.执行同步代码块
6.特别说明如果该对象类锁发生二次重偏向的累计值超过默认40且未超过规定间隔时间,后续创建锁的类对象的对象默认为正常对象后续将不为进行获取偏向锁的流程
释放锁步骤
1.暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态
2.撤销偏向锁,恢复到无锁状态01或轻量级锁的状态
总结:
引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行.因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的.因为偏向锁只需检查偏向锁、锁标识以及ThreadId即可.
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程不会主动去释放偏向锁,需要等待其他线程来竞争.偏向锁的撤销需要等待全局安全点.(这个时间点上没有正在执行的代码)
可使用 java -XX:+PrintFlagsFinal -version |grep Biased 查看JVM相关偏向锁的默认配置,如图:
参数解析
1.UseBiasedLocking :是否开启偏向锁设置(-XX:[+/-]UseBiasedLocking)
2.TraceBiasedLocking :是否开启偏向锁的日志跟踪(-XX:[+/-]TraceBiasedLocking)
3.BiasedLockingStartupDelay :偏向锁启动的延迟时间(-XX:BiasedLockingStartupDelay=4000(时间单位毫秒)))
4.BiasedLockingDecayTime :最近该类两次偏向锁使用的间隔时间超过该时间,重偏向锁和撤销偏向锁的阈值计数变量都将清零(-XX:BiasedLockingDecayTime=25000(时间单位毫秒)))
5.BiasedLockingBulkRebiasThreshold :批量重偏向的阈值.未到达该阈值时,出现其他线程竞争该对象时,会出现锁撤销或升级为轻量级锁;达到或者超过该阈值时,出现其他线程竞争该对象时,会出现批量重偏向(之后的对象会直接替换为竞争线程的对象id)(-XX:BiasedLockingBulkRebiasThreshold=20);
6.BiasedLockingBulkRevokeThreshold :批量撤销的阈值.未达到该阈值时,直接走上面批量重偏向的逻辑.到达该阈值时,直接走轻量级锁的逻辑(-XX:BiasedLockingBulkRevokeThreshold=40)
轻量级锁
场景:
当JVM关闭偏向锁(默认是开启的,采用-XX:-UseBiasedLocking)功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁.
获取锁步骤
1.判断当前对象是否处于无锁状态(hashcode、0、01),若是则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤3
2.JVM利用CAS操作尝试将对象的Mark Word更新位指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示该对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤3
3.判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀位重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态
释放锁
1.取出在获取轻量级锁保存在Displaced Mark Word中的数据;
2.用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行3;
3.如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程.
锁自旋
线程的阻塞和唤醒需要CPU从用户态转换为核心态,频繁的阻塞和唤醒对于CPU来说是一件负担很重的工作.为了减少短时间内的线程阻塞和唤醒,JVM让竞争获取锁失败的线程进行自旋等待(通过-XX:+UseSpinning参数进行设置).如果超过了自旋等待的次数(默认为10次,通过-XX:preBlockSpin=10参数进行设置)(备注:jdk8的这两个设置参数都已不存在),锁对象膨胀为重量级锁,线程进入EntryList进行等待.jdk1.6引入了适应性自旋锁,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定.
jdk8和jdk6对应默认参数的配置情况,通过java -XX:+PrintFlagsFinal -version | findStr Spin指令进行查看,如图:
总结:
对于轻量级锁,其性能提升的依据是"对于绝大部分的锁,在整个生命周期内都是不会存在竞争的",如果打破这个一句则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢.
重量级锁
重量级锁是通过锁对象内部的一个叫做Monitor(监视器)来实现的.但是监视器本质又是依赖于底层操作系统的Mute Lock来实现的.而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对较长的时间.
Monitor对象是由C++实现的.包含两个对象_WaitSet和_EntryList,多个线程竞争对象锁的时候,未竞争到的线程会进入到_EntryList中阻塞等待,线程状态为(Blocked),如果已进入到同步代码块的线程发现执行条件不满足,它可以执行wait(等待)方法,执行后该线程会进入到_WaitSet中,同时把_Owner和_count进行清空,允许在_EntryList的线程继续去竞争获取对象锁,进行到对象锁的线程会将_Owner设置为自己线程的指针同时_count进行+1.
总结
关于synchronized涉及的底层代码逻辑是非常复杂的,文章中如正确的地方还望多加评论指出,谢谢!整理写文章不易,还望多多点赞,谢谢!
[下一篇 介绍Lock接口]