关键字 Synchronied
可重入锁、悲观锁、非公平锁、可见性、原子性
几种使用场景
// 1.同步代码块,锁(obj)可以是任意对象或者this
synchronized (obj){}
// 2.同步方法,锁是this
public synchronized void ticket(){}
// 3.静态同步方法,锁是该类的.class对象
public synchronized static void ticket(){}
不建议使用String作为锁对象,因为常量池的存在,会将两个相同值的String常量指向一个地址,导致结果不符合预期
底层实现
对于同步代码块
public static void main(String[] args) {
synchronized (new Object()){
System.out.println();
}
}
查看字节码
javap -c -s -v -l xxx.class
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: dup
8: astore_1
9: monitorenter
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: invokevirtual #4 // Method java/io/PrintStream.println:()V
16: aload_1
17: monitorexit
18: goto 26
21: astore_2
22: aload_1
23: monitorexit
24: aload_2
25: athrow
26: return
9和17行有一个monitorenter
和monitorexit
来标识同步代码块开始和结束的位置,而23行的monitorexit
是为了保证发生异常是能够正确释放锁。其实synchronized就是通过获取Monitor
对象监视器,当执行到monitorenter
时尝试获取锁,也就是尝试获取对象监视器的持有权,当执行到monitorexit
时释放锁。
对于同步方法
public static void main(String[] args) {
test();
}
public static synchronized void test(){
System.out.println();
}
查看字节码
public static synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokevirtual #4 // Method java/io/PrintStream.println:()V
6: return
LineNumberTable:
line 19: 0
line 20: 6
可以看到同步方法没有了monitorenter
和monitorexit
取而代之的是ACC_SYNCHRONIZED
标识,JVM通过这个标识来表明方法是否是同步方法,从而执行相应的同步调用
不过两者的本质都是对对象监视器 monitor
的获取
锁升级
synchronized的锁是存在java对象头的MarkWord中的,早期实现是重量级锁,1.6以后引用了锁升级的概念加入了偏向锁、轻量级锁。
synchronized锁的四种状态
锁状态 | 是否偏向锁(1bit) | 锁标识位(2bit) | |
---|---|---|---|
无锁状态 | 0 | 01 | |
偏向锁 | 1 | 01 | |
轻量级锁 | 指向栈帧中的锁记录的指针 | 00 | |
重量级锁 | 指向互斥量的指针 | 10 |
锁可以升级但是不能降级: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
这种策略主要是为了提高获取锁的效率
偏向锁:
当一个线程访问同步块并获取锁时,会在对象头的MarkWord和栈帧中的锁记录里存储锁偏向的线程ID,以后在线程进入和退出时就不需要进行CAS操作来加锁和解锁,只需要检查MarkWord中是否存在指向当前线程的偏向锁ID,如果存在则表示已经获取到了锁,如果不存在则看下偏向锁标识是否为1,如果是则通过CAS竞争锁,如果不是则通过CAS将偏向锁指向当前线程。
轻量级锁:
如果存在两个线程竞争的情况,则偏向锁会升级成为轻量级锁
首先两个线程都在自己的栈帧中创建用于存储锁记录的空间,并将MarkWord复制到锁记录中。然后A线程尝试通过CAS操作将对象头中的MarkWord替换为指向锁记录的指针,如果成功则代表获取到了锁。这时B线程也尝试通过CAS获取锁,因为MarkWord已经被A线程修改,所以获取失败,为了提高索取锁的效率,B线程会自旋获取锁,当达到一定次数时还是获取失败,这时MarkWord中的记录就会被B线程修改为重量级锁,B线程被挂起,后续线程看到是重量级锁也直接挂起。
解锁时,A线程尝试使用CAS将对象头中的MarkWord改回自己锁记录中的MarkWord,因为对象头中的MarkWord已经被指向为重量级锁了, 所以CAS失败,A线程会释放锁并唤起等待的线程, 进行新一轮的竞争。
比较
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁解锁无需额外消耗,速度快 | 如果存在锁竞争,则会带来额外的锁撤销的消耗 | 一个线程访问同步块 |
轻量级锁 | 竞争的线程不会阻塞 | 如果始终得不到锁竞争的线程,自旋会消耗CPU资源 | 追求响应速度,同步块执行速度快 |
重量级锁 | 线程不使用自旋获取锁,不会消耗CPU资源 | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行速度较慢 |
这里有个问题,锁不能降级,那是不是意味着当synchronize升级到重量级锁后,后续就一直是重量级锁了?
其实是不对的,当重量级锁释放后又会变成无锁状态,下次重新走升级路线
验证:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
Object LOCK = new Object();
System.out.println("no lock: " + ClassLayout.parseInstance(LOCK).toPrintable());
IntStream.rangeClosed(1, 3).forEach(item -> {
new Thread(() -> {
synchronized (LOCK) {
System.out.println(Thread.currentThread().getName() + " lock: " + ClassLayout.parseInstance(LOCK).toPrintable());
}
}).start();
});
Thread.sleep(1000);
System.out.println("current lock: " + ClassLayout.parseInstance(LOCK).toPrintable());
如果只有一个线程在获取锁,会发现其实是轻量级锁而不是偏向锁,这是因为1.8 的偏向锁得在 JVM 启动 4 秒后才生效,需要通过jvm参数 -XX:BiasedLockingStartupDelay=0
关闭偏向锁延迟才能观察到偏向锁标识
参考博客: Synchronized升级成重量级锁之后就下不来了?你错了!