问题
- (1) synchronized的特性?
- (2)synchronized的实现原理?
- (3)synchronized是否可重入?
- (4)synchronized是否是公平锁?
- (5)synchronized的优化?
- (6)synchronized的五种使用方式?
在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。在Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
一、实现原理
1.1 简介
从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是方法级同步是隐式执行的。
详细请看
1.2 synchronized 的锁存放位置—-对象头
在了解对象头之前,先看看对象在Java虚拟机中是如何存储的?
对象的内存布局图示
对象头(Header)
- Mark Word
用于存储对象自身的数据,如:哈希码(HashCode)、 GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32为和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit
32位下Mark Word 的结构
64位下Mark Word 的结构
MarkWord是根据对象的状态区分不同的状态位,从而区分不同的存储结构(32bit 下)
- 正常对象: 对象的HashCode (25bit) + 对象的分代年龄(4bit)+是否偏向锁状态(1bit, 值为0) + 锁标志状态(2bit,值01)
- 偏向对象: 线程ID(23bit )+ Epoch (2bit)+ 对象的分代年龄(4bit)+是否偏向锁状态(1bit) + 锁标志状态(2bit)
二、锁的升级与对比
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。注意:锁的状态等级只能升不能降。
**
2.1 偏向锁
- 什么是偏向锁?
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。但是它在应用程序启动几秒钟之后才激活,使用JVM参数来关闭延迟:
-XX:BiasedLockingStartupDelay=0。
- 偏向锁初始化流程
2.2 轻量级锁
是指当锁是偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。
- 加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
- 两个线程同时争夺锁,导致锁膨胀的流程图
2.5 自旋锁
当线程竞争轻量级锁失败后,此时并不会立即在操作系统层面挂起,而是做一些空循环,也就是所谓的自旋锁。系统希望在自旋的过程中可以获得锁。如果若干次之后还未获得到,则进入阻塞状态,加重量级锁。
2.4 重量级锁
是指当锁是轻量级锁时,当自旋的线程自旋了一定的次数后,还没有获取到锁,就会进入阻塞状态,该锁升级为重量级锁,重量级锁会使其他线程阻塞,性能降低。
2.5 锁消除
锁消除相关联的一项技术就是逃逸分析。就是分析某一个变量会不会在一个作用域外部引用。它的基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问到的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。
虚拟机启动参数
-server #在server模式下才可以启动逃逸分析
-XX:+DoEscapeAnalysis #启动逃逸分析
-XX:+EliminateAllocations #开启了标量替换
-XX:+EliminateLocks #打开锁消除
使用JOL 工具查看锁升级时,对象头的变化
http://www.debugger.wiki/article/html/156445573033210
三、关键字synchronized 的使用和特性
3.1 公平锁 VS 非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
public class SynchronizedTest {
public static void sync(String tips) {
synchronized (SynchronizedTest.class) {
System.out.println(tips);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> sync("线程1")).start();
Thread.sleep(100);
new Thread(() -> sync("线程2")).start();
Thread.sleep(100);
new Thread(() -> sync("线程3")).start();
Thread.sleep(100);
new Thread(() -> sync("线程4")).start();
}
}
//输出
线程1
线程4
线程3
线程2
由输出结果可知,synchronized是非公平锁,它并没有按顺序进行获取锁。
3.2 可重入锁
例子
public class SynchronizedTest2 {
public static void sync(String tips) {
synchronized (SynchronizedTest2.class) {
System.out.println("synchronized -> 1 当前线程 " + Thread.currentThread().getName());
synchronized (SynchronizedTest2.class) {
System.out.println("synchronized -> 2 当前线程 " + Thread.currentThread().getName());
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> sync("线程1")).start();
new Thread(() -> sync("线程2")).start();
}
}
输出
synchronized -> 1 当前线程 Thread-0
synchronized -> 2 当前线程 Thread-0
synchronized -> 1 当前线程 Thread-1
synchronized -> 2 当前线程 Thread-1
汇编
Compiled from "SynchronizedTest2.java"
public class cn.hdj.SynchronizedTest2 {
public cn.hdj.SynchronizedTest2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void sync();
Code:
0: ldc #2 // class cn/hdj/SynchronizedTest2
2: dup
3: astore_0 // 存储一个引用到本地变量0中,后面的0表示第几个变量
4: monitorenter // 调用monitorenter,它的参数变量0,也就是上面的SynchronizedTest2类对象
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: new #4 // class java/lang/StringBuilder
11: dup
12: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
15: ldc #6 // String synchronized -> 1 当前线程
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: invokestatic #8 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
23: invokevirtual #9 // Method java/lang/Thread.getName:()Ljava/lang/String;
26: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: ldc #2 // class cn/hdj/SynchronizedTest2
37: dup
38: astore_1
39: monitorenter // 调用monitorenter,它的参数变量1,也就是上面的SynchronizedTest2类对象
40: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
43: new #4 // class java/lang/StringBuilder
46: dup
47: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
50: ldc #12 // String synchronized -> 2 当前线程
52: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
55: invokestatic #8 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
58: invokevirtual #9 // Method java/lang/Thread.getName:()Ljava/lang/String;
61: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
64: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
67: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
70: aload_1 // 从本地变量表中加载第1个变量
71: monitorexit // 调用monitorexit解锁,它的参数是上面加载的变量1
72: goto 80 //跳的80行
75: astore_2
76: aload_1
77: monitorexit
78: aload_2
79: athrow
80: aload_0
81: monitorexit // 调用monitorexit解锁,它的参数是上面加载的变量0
82: goto 90 //跳的90行
85: astore_3
86: aload_0
87: monitorexit
88: aload_3
89: athrow
90: return //返回
Exception table:
from to target type
40 72 75 any
75 78 75 any
5 82 85 any
85 88 85 any
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
0: new #13 // class java/lang/Thread
3: dup
4: invokedynamic #14, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #15 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #16 // Method java/lang/Thread.start:()V
15: new #13 // class java/lang/Thread
18: dup
19: invokedynamic #17, 0 // InvokeDynamic #1:run:()Ljava/lang/Runnable;
24: invokespecial #15 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
27: invokevirtual #16 // Method java/lang/Thread.start:()V
30: return
}
3.3 synchronized的五种使用方式
public class SynchronizedUse {
private static final Object lock = new Object();
/**
* 成员方法
* 锁的是当前实例this
*/
public synchronized void sync1() {
}
/**
* 静态方法
* 锁的是SynchronizedUse.class 类对象
*/
public static synchronized void sync2() {
}
public void sync3() {
/**
* 同步代码块
* 锁的是lock实例
*/
synchronized (lock) {
}
}
public void sync4() {
/**
* 同步代码块
* 锁的是SynchronizedUse.class 类对象
*/
synchronized (SynchronizedUse.class) {
}
}
public void sync5() {
/**
* 同步代码块
* 锁的是当前实例this
*/
synchronized (this) {
}
}
}
总结
- synchronized的实现原理是使用 monitorenter 和 monitorexit 这两个指令来实现的。
- monitorenter和monitorexit字节码指令更底层是使用Java内存模型的lock和unlock指令
- synchronized 是可重入锁
- synchronized 是非公平锁
- synchronized有三种状态:偏向锁、轻量级锁、重量级锁;
- synchronized可以同时保证原子性、可见性、有序性