Synchronize 是什么
synchronized,中文意思为同步,用于多线程资源共享与维护的最常用手段。它通过线程互斥的手段,保存证了资源的原子性。
使用如下:
synchronized (o) {
// 业务处理
}
实现的原理
本文要讲的主要是1.6以后的版本。1.6版本以前版本直接使用重量级锁,没什么好讲的。
1.6版本针对synchronize做了使用优化,根据使用的情使用不同的锁。包括:
偏向锁:
1:当线程取得锁时, warkword偏向锁位标记为1,并记录使用该对象的线程指针。
2:当线程尝试向 偏向锁对象 取锁时, 锁将升级为轻量级锁。
轻量级锁:
1.当线程拿不到锁对象时,线程不会释放,而是继续while循环空转,直到获取到锁为止。
2.轻量级锁的Markword会记录lock Record的指针,lock Record会记录对象自旋的次数,当它达到一定自旋次数之后,jvm会将它升级为重量级锁。
优点:
无需从用户态转向内核态,在锁竞争比较低的情况,线程只需消化几个时钟周期就能获得锁,所以性能很快。
缺点:
线程自旋是需要消耗cpu性能的,在锁竞争激烈的情况,空转的线程数量和自旋的次数会变高,此时会白白浪费cpu时钟周期。
重量级锁:
jvm对重量级锁的实现,是需要依赖操作系统底层的,操作系统底层维护了一个锁的队列,当jvm所有重量级锁的申请,都需要在这个锁队列里面进行排队,线程需要从用户态转向内核态,排队过程线程被挂起,无需消耗cpu时钟频率,直到轮到这个线程获取锁时,系统才会唤醒该争用的线程。
优点:
不消耗cpu,特别时对于大量锁的争用时。
缺点:
等待锁的时间长。
synchronize 锁升级过程
如下图所示:
- 初始化,无锁。
- 有且只有一线程取得锁时,为偏向锁。
- 对象已被锁,并有其它线程尝试取锁时,锁升级为轻量级锁。
- 锁状态为轻量级锁,并有更多(达到临界值时)的线程尝试去取锁,轻量级锁将升级为重量级锁。
线程获取锁过程
如下图所示:
- 对象无锁时。直接取得锁
- 有锁并且是轻量级锁,线程自旋取得锁。
- 有锁并且是重量级锁,线程阻塞,等待锁释放再取得锁。
- 取得锁后,用完释放。
锁状态与markword
对象锁的状态是存在markword记录的,如下图所示:
- 无锁时,锁标志为01,另外存储了其它各种信息(包括偏向锁状态,分代年龄,hashcode)。
- 偏向锁时,锁标志为01,另外存储了取得锁的线程。
- 轻量锁时,锁标志为00,存储了线程栈Lock Record的指针。
- 重量级锁时,锁标志为10,存储了,重量级锁的指针。
实例
下面将通过一个实例,结合markword一步一步地演示synchronize 锁的升级过程。
测试过程
- 引入JOL打印出对象的结构(关键是markword)。
- 模拟无锁,单线程锁,2个线程,100个级线。输出对应的markword。
pom.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>SynchronizeTest</groupId>
<artifactId>SynchronizeTest</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
</dependencies>
</project>
代码:
import org.openjdk.jol.info.ClassLayout;
public class ApplicationTest {
static volatile String strMsg = "";
static volatile String str2Msg = "";
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);//因为偏向锁在jvm启动4秒后才启动。所以在这里设置等待5秒。
Object o = new Object();
//输出线程
new Thread() {
@Override
public void run() {
while (true) {
if (str2Msg.equals(strMsg) == false) {
str2Msg = strMsg;
System.out.print(str2Msg);
}
}
}
}.start();
//*****************根据测试需要解注*************************
//未锁
strMsg = ClassLayout.parseInstance(o).toPrintable();
//一个线程锁
//SynsTest(1, o);
//两个线程
// SynsTest(2,o);
//100个线程
// SynsTest(100,o);
}
/*
开启多个线程线线程使用
*/
public static void SynsTest(int num, final Object o) throws InterruptedException {
//偏向锁
for (int i = 0; i < num; i++) {
System.out.println("启开第" + (i + 1) + "线程");
new Thread() {
@Override
public void run() {
synchronized (o) {
for (; ; ) {
strMsg = ClassLayout.parseInstance(o).toPrintable();
try {
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
}
测试步骤:
第一步:
1、o对象初始化后,直接打印出o对象。解注以下代码
//未锁
strMsg = ClassLayout.parseInstance(o).toPrintable();
//一个线程锁
//SynsTest(1, o);
//两个线程锁
// SynsTest(2,o);
//100个线程
// SynsTest(100,o);
2、运行结果如下图所示: 状态位为01,断定为无锁。
第二步:
1、开启1个线程取锁。解注以下代码
//未锁
//strMsg = ClassLayout.parseInstance(o).toPrintable();
//一个线程锁
SynsTest(1, o);
//两个线程锁
// SynsTest(2,o);
//100个线程
// SynsTest(100,o);
2、结果如下图所示:状态位为01,后面还记录着 对应的线程指针,断定为 偏向锁。
第三步:
1、开启2个线程最锁。解注以下代码
//未锁
//strMsg = ClassLayout.parseInstance(o).toPrintable();
//一个线程锁
//SynsTest(1, o);
//两个线程锁
SynsTest(2,o);
//100个线程
// SynsTest(100,o);
2、如下图所示:状态位为00,断定为轻量级锁。
第四步:
1、开始100个线程取锁。解注以下代码
//未锁
//strMsg = ClassLayout.parseInstance(o).toPrintable();
//一个线程锁
//SynsTest(1, o);
//两个线程锁
//SynsTest(2,o);
//100个线程
SynsTest(100,o);
2、结果如下图所示:状态位为10,断定为重量级锁。
PS:
因为偏向锁默认在jvm启动4秒后才启动。所以在这里设置等待5秒。
具体的结果与配置有关。
本机测试用的环境如下,所有配置都是默认的。
可以通过以下命令打印出配置的参数。
本人能力有限,如有错误请指出。
参考文献:
周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)
马老师视屏。