Synchronized关键字原理!

Synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块。

两类锁:

对象锁:

  • 方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)。

类锁:

  • 修饰静态的方法或指定锁为Class对象。

底层实现

修饰方法:

  • 在字节码上给方法加了一个 flag:ACC_SYNCHRONIZED

代码块:

  • 通过 monitorenter 和monitorexit 两个指令进行控制的。
    • 本质上是通过 monitor 来实现的。

在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ObjectMonitor() {
_header = NULL; //markOop对象头
_count = 0; //记录个数
_waiters = 0, //等待线程数
_recursions = 0; //重入次数
_object = NULL; //监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL; //指向获得ObjectMonitor对象的线程或基础锁
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL;
_succ = NULL;
_cxq = NULL;
FreeNext = NULL;
_EntryList = NULL; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ; // _owner is (Thread *) vs SP/BasicLock
_previous_owner_tid = 0; // 监视器前一个拥有者线程的ID
}

每个 Java 对象在 JVM 的对等对象的头中保存锁状态,指向 ObjectMonitor。

  • ObjectMonitor 保存了当前持有锁的线程引用
  • EntryList 中保存目前等待获取锁的线程
  • WaitSet 保存 wait 的线程。

计数器count:

  • 每当线程获得 monitor 锁,计数器 +1,当线程重入此锁时,计数器还会 +1。
  • 当计数器不为 0 时
    • 其它尝试获取 monitor 锁的线程将会被保存到EntryList中,并被阻塞。
  • 当持有锁的线程释放了monitor 锁后,计数器 -1。
  • 当计数器归位为 0 时,所有 EntryList 中的线程会尝试去获取锁
    • 但只会有一个线程会成功,没有成功的线程仍旧保存在 EntryList 中。

image-20231018135610476

加锁时,即遇到Synchronized关键字时

  • 线程会先进入monitor的_EntryList队列阻塞等待。

如果monitor的_owner为空,则从队列中移出并赋值与_owner

如果在程序里调用了wait()方法,wait方法会释放monitor锁

  • _owner赋值为null,并进入_WaitSet队列阻塞等待。

  • 这时其他在_EntryList中的线程就可以获取锁了。

当程序里其他线程调用了notify/notifyAll方法时

  • 就会唤醒_WaitSet中的某个线程,这个线程就会再次尝试获取monitor锁。

  • 如果成功,则就会成为monitor的owner。

当程序里遇到Synchronized关键字的作用范围结束时,就会将monitor的owner设为null,退出。

Synchronized和Lock的区别:

Synchronized属于JVM层面

  • Lock是API层面的东西,JUC提供的具体类。

Synchronized不需要用户手动释放锁

  • 当代码执行完毕之后会自动让线程释放持有的锁,Lock需要去手动释放锁。

Synchronized是不可中断的

  • 除非抛出异常或者程序正常退出,Lock可中断。

Synchronized是非公平锁

  • Lock默认是非公平锁,但是可以通过构造函数传入boolean类型值更改是否为公平锁。

Synchronized要么唤醒所有线程,要么随机唤醒一个线程

  • Lock可以使用condition实现分组唤醒需要唤醒的线程。

Synchronized只能同时被一个线程拥有

  • 但是 Lock 锁没有这个限制,例如在读写锁中的读锁,是可以同时被多个线程持有的。

Synchronized 锁了类,那再锁实例还能锁住吗?冲突吗?

Synchronized 锁住类和锁住实例是两种不同的锁机制,它们互不干扰。

类级锁和实例级锁控制的资源不同,锁住类不会影响锁住实例

  • 你可以同时锁住类和不同实例,且它们是独立的。

Synchronized为什么需要维护一个计数器?独占锁一个状态表示就行了吧,计数的目的是啥?

通过记录线程 ID(或线程标识)可以判断一个锁是否被某个线程持有。

  • 但无法准确反映嵌套调用的深度,容易导致锁的过早释放问题。

维护计数器并不是为了简单判断锁的持有者,而是为了支持可重入锁的完整语义。

1
2
3
4
5
6
synchronized(obj) {
synchronized(obj) {
// 第二次获取锁
}
// 此处锁可能已经被释放,但外层还在访问共享资源
}