原理
synchronized
是 Java 中的一个关键字,主要解决的是多个线程之间访问资源的同步性,其实就是充当锁的功能
synchronized
实现原理依赖于JVM 的 Monitor(监视器锁)和 对象头(Object Header)
当使用 synchronized
时,实际上是通过字节码指令 monitorenter
和 monitorexit
来实现加锁和解锁操作的。
-
monitorenter 指令:
-
当线程执行到
synchronized
修饰的代码块时,JVM 会在字节码中插入monitorenter
指令。 -
在执行代码块前尝试获取指定对象的Monitor锁。如果该对象的锁计数器为 0 ,则尚未被其他线程持有,则当前线程可以成功获取锁,计数器+1,从而实现可重入性,并继续执行同步代码块。
-
如果锁已被其他线程持有,则当前线程将会被阻塞,直到锁被释放。
-
-
monitorexit 指令:
-
在
synchronized
代码块的结尾处,JVM 会插入monitorexit
指令。 -
monitorexit
指令负责释放之前获取的锁。当线程执行完同步代码块后,会执行monitorexit
指令,释放所持有的锁。-
如果在执行同步代码块的过程中发生了异常,JVM 也会确保
monitorexit
指令被执行,以防止锁被永久持有,从而导致死锁或其他并发问题。
-
-
这也就解释了为什么synchronized 出同步块后 会自动释放锁
深入到源码来说,
synchronized
的内部实际上有两个队列waitSet
和entrySet
-
当多个线程进入同步代码块时,首先进入
entryList
-
有一个线程获取到 monitor 锁后,就赋值给当前线程,并且计数器+1
-
若线程调用
wait
方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒 -
若线程调用
notify
或者notifyAll
之后又会进入entryList 竞争 monitor锁
-
-
如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
synchronized 支持重入吗?如何实现的?
可重入,底层是利用操作系统mutex Lock
实现的,每一个可重入锁都会关联一个线程ID和一个锁状态status。
当一个线程请求方法时,会去检查锁状态。
-
如果锁状态是0,代表该锁没有被占用,使用CAS操作获取锁,将线程ID替换成自己的线程ID。
-
如果锁状态不是0,代表有线程在访问该方法。此时如果线程ID是自己的线程ID,会将status自增1,然后获取到该锁
-
释放锁时同理,可重入锁的,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
锁的升级和优化
在 Java 6 之后,JVM 引入了偏向锁和轻量级锁,重量级锁等优化技术
具体的锁升级的过程是:无锁->偏向锁->轻量级锁->重量级锁。
总结
-
偏向锁: 当一个线程第一次获取锁时,JVM 会将该线程标记为“ 偏向 ”状态,后续若该线程再获取该锁,无需进行额外同步过程。
-
轻量级锁: 当另一个线程尝试获取已经被偏向的锁时,锁会升级为轻量级锁,使用CAS 操作来减少锁竞争的开销。
-
重量级锁: 当 CAS 失败无法获取锁,锁会升级为重量级锁,线程会被挂起,直到锁被释放。
Monitor 的内部结构
在 Monitor 的内部,有两个队列:等待队列(Wait Queue)和条件等待队列(Condition Queue)
-
未获得锁的线程会被放置在等待队列中,而那些调用了
wait()
方法的线程会被移到条件等待队列中。 -
当锁被释放或调用了
notify()
或notifyAll()
方法时,相应的队列中的线程会被唤醒,尝试获取锁。
自适应自旋锁
-
为了减少重量级锁带来的上下文切换开销,JVM 引入了自适应自旋锁机制。在轻量级锁阶段,如果线程发现锁未被占用,它会尝试循环(自旋)一段时间,希望在此期间锁能被释放。
-
自旋的时间长度可以根据前一次自旋等待锁的成功率来动态调整,这样可以在一定程度上提升锁的性能