synchronized能够实现线程同步。无论怎么使用,最终都是对对象加锁。
为什么synchronized最终都是作用在对象上呢? 因为对象在堆中除了除了有字段属性外,还有固定的对象头,通过对象头最终可以得知这个对象是否被加过锁,以及持有锁的线程是谁。(第2节 对象结构)
仔细研究对象头后,发现其中记录了多种锁的状态。锁的升级与其息息相关。(第3节 锁优化策略)
许多线程长时间阻塞,说明锁已经升级到了重量级锁,JVM使用Monitor处理阻塞线程,如wait、notify等。(第1节 Monitor阻塞机制)
AQS也是参照jvm底层Monitor处理方式实现的,但是性能上更优一些。(第4节 与AQS的对比)
synchronized是非公平、可重入、独占锁
wait和notify使用时线程必须持有锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Description:Application
* CreationTime: 2022/1/20 10:39
*
* @author dreambyday
* @since 1.0
*/
public class Application {
public synchronized static void show(int seconds) {
try {
TimeUnit.MILLISECONDS.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
public static void testSync() throws InterruptedException {
// 0->200ms 1获取锁 234blocked -> 234在cxq
new Thread(()->Application.show(200),"1").start();
// 保证1线程先获取锁
TimeUnit.MILLISECONDS.sleep(10);
// 200->300ms 1释放锁 234从cxq进入EntryList,先进后出,4获取锁,23继续blocked
new Thread(()->Application.show(400),"2").start();
new Thread(()->Application.show(400),"3").start();
new Thread(()->Application.show(400),"4").start();
// 300->(200+400)ms
// 4继续获取锁,23blocked,处于EntryList。
// 567线程启动,获取锁失败,进入cxq
TimeUnit.MILLISECONDS.sleep(300);
new Thread(()->Application.show(400),"5").start();
new Thread(()->Application.show(400),"6").start();
new Thread(()->Application.show(400),"7").start();
// EntryList先被唤醒,EntryList空了后,将cxq整体移动到EntryList继续唤醒
// 200->(200+400*3)ms 处于EntryList的线程依次被唤醒,顺序为432
// (200+400*3)ms 时,EntryList为空,cxq移动到EntryList,内容为567
// (200+400*3)ms到结束,处于EntryList的线程依次被唤醒,顺序为765
}
public static void main(String[] args) throws InterruptedException {
// 最终顺序为1432765
testSync();
}
}
为了降低cxq尾部并发竞争
假如只有cxq先进后出队列,队列尾部面临的操作有
拆分两个队列后,唤醒操作发生在EntryList上
new Object()在内存中的字节数为16 = 8(Mark Word)+4(开启压缩指针的klass pointer) + 0(不是数组,不占字节) + 0(对象体,无属性,不占字节) + 4(对齐填充,保证整体是8的倍数)
计算各种对象大小看这篇文章,包括基础类型数组、String等
占1个字宽大小(32位机则为32位长,64位机为64位长)。是实现轻量级锁和偏向锁的关键。
包括: hashcode、分代年龄、是否偏向、锁的标志、GC标记、指向monitor的指针、指向持有锁线程的lockRecord的指针、偏向锁线程ID、epoch(第几代偏向锁)
虚拟机栈保存lockRecord: 记录了持有锁的线程、无锁状态下的Mark Word信息(为了备份还原,以及记录hashcode等信息)
锁重入情况: 栈新压入Mark Word记录为null的lockRecord,释放一次锁就从栈弹出一次lockRecord
monitor对象和java对象同生共死,存储在堆中。重量级锁状态下,monitor同样保存了无锁状态的MarkWord信息。
为什么指向元数据而不是类对象,我只能靠一个例子理解:如果锁的对象是类对象,那么klass pointer相当于自己指向自己,会令人奇怪吧
32位机和64位机都是32位长,因此数组长度理论上不能超过 2 32 2^{32} 232 。实际上,根据JVM对对象头的处理,目前数组的最大长度是 2 31 − 1 2^{31}-1 231−1 ,即Integer最大值
锁级别: 无锁->偏向锁->轻量级锁->重量级锁
锁只能升级不能降级。
偏向锁是加锁操作的优化手段。为了消除数据无竞争下的锁重入开销引入偏向锁。在无锁竞争场合,偏向锁性能较好。
偏向锁使用了一种等到竞争出现才释放锁的机制,消除偏向锁的开销比较大
自JDK6,JVM默认启动偏向锁模式,但是会在虚拟机启动后延迟4s左右才会开启
在启动偏向锁后创建的对象,对象头的Mark Word的锁状态默认变成偏向锁状态,并且Thread ID为0 。此时处于 可偏向但未偏向任何线程 ,也叫匿名偏向状态
为什么要有延迟偏向: 虚拟机启动过程中,许多后台线程可能会争抢锁,导致对象头的锁状态从偏向锁撤销,再升级到 轻量级锁或重量级锁。
对象可偏向或已偏向时,调用hashcode会使对象无法偏向。
几种情况:
偏向锁状态,执行
轻量级锁适用于锁竞争不激烈的场景。 如两个线程交替运行
多线程竞争同一把锁(偏向锁或轻量级锁)时,竞争失败的线程若在短暂的自旋后获取到锁,则不会导致锁膨胀为重量级锁。
重量级锁适用于锁竞争激烈或同步块执行时间长的场景
重量级锁基于Monitor实现,用户态转内核态耗时较长。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加解锁无额外消耗,单线程下加锁基本无消耗 | 线程竞争产生额外的锁撤销成本 | 单线程访问同步代码块 |
轻量级锁 | 竞争线程不阻塞,基于cas自旋在用户态实现 | 长时间锁自旋获取不到锁还是会锁膨胀,并且消耗CPU | 同步块执行速度快、锁竞争不激烈 |
重量级锁 | 锁不自旋,不消耗cpu | 线程阻塞,用户态转内核态效率低 | 追求吞吐量、竞争激烈、同步代码块执行时间长 |
锁消除的依据是逃逸分析的数据支持。如果JVM检测同步内容不会逃逸,则会直接消除加锁操作
连续的加锁、解锁操作合并为一个加锁解锁操作可能提升性能,因此有了锁粗化的概念。
JVM检测到对同一个对象连续的加解锁,并且可以合并时,会将加解锁操作移动到循环之外。
for x : xx
synchronized(obj) {}
结论: 是。LockSupport也是基于mutex实现的,有用户态和内核态的切换
ReentrantLock最终会使用LockSupport.park阻塞线程,因此最终和synchronized的重量级锁一样
参考
ReentrantLock基于AQS实现。AQS在Java代码层面管理阻塞队列,synchronized由jvm层面管理阻塞队列。AQS使用CAS较多,阻塞队列操作逻辑比jvm实现的好,因此性能高一些。
性能比较1
性能比较2
区别 | synchronized | ReentrantLock |
---|---|---|
修饰位置不同 | 静态方法、普通方法、代码块 | 代码块 |
自动与非自动释放锁 | 自动释放锁 | 手动释放锁 |
锁类型不同 | 非公平锁 | 默认非公平锁,也可以创建公平锁 |
响应中断不同 | 不可以响应中断 | 能够响应中断 |
底层实现不同 | 基于JVM的Monitor | 基于AQS |
阻塞后线程状态不同 | 阻塞进入BLOCKED状态 | 阻塞进入WAITING状态 |
同步队列实现方式不同 | 类似栈,有两个,先进后出 | 队列且只有一个,先进先出 |