文章目录
- 1.synchronized作用
- 2.synchronized加锁原理
- 3.monitor锁
- 4.synchronized锁的优化
- 4.1.自适应性自旋锁
- 4.2.偏向锁
- 4.3.轻量级锁
- 4.3.重量级锁
- 5.总结
1.synchronized作用
synchronized是Java提供一种隐式锁,无需开发者手动加锁释放锁。保证多线程并发情况下数据的安全性,实现了同一个时刻只有一个线程能访问资源,其他线程只能阻塞等待,简单说就是互斥同步。
2.synchronized加锁原理
代码块加锁:
例如下面一段代码就是加上了对象锁

这个时候通过反编译查看class字节码信息:

可以看到,底层是通过monitorenter和monitorexit两个关键字实现的加锁与释放锁,执行同步代码之前使用monitorenter加锁,执行完同步代码使用monitorexit释放锁,抛出异常的时候也是用monitorexit释放锁。
在方法上加锁:

反编译看一下底层实现

这次只使用了一个ACC_SYNCHRONIZED关键字,实现了隐式的加锁与释放锁。其实无论是ACC_SYNCHRONIZED关键字,还是monitorenter和monitorexit,底层都是通过获取monitor锁来实现的加锁与释放锁。
3.monitor锁
Monitor 被翻译为监视器,是由jvm提供,c++语言实现
monitor锁是通过ObjectMonitor来实现的,虚拟机中ObjectMonitor数据结构如下(C++实现的):

其中:
- Owner(持有锁的线程-只能有一个):
存储当前获取锁的线程的,只能有一个线程可以获取 - EntryList(保存竞争的线程):关联
没有抢到锁的线程,处于Blocked状态的线程 - WaitSet:关联调用了
wait方法的线程,处于Waiting状态的线程
执行逻辑:


图上展示了ObjectMonitor的基本工作机制:
- 当多个线程同时访问一段同步代码时,首先会
进入 _EntryList 队列中等待。 - 当某个线程
获取到对象的Monitor锁后进入临界区域,并把Monitor中的_owner变量设置为当前线程,同时Monitor中的计数器_count 加1。即获得对象锁。 - 若持有Monitor的线程调用
wait()方法,将释放当前持有的Monitor锁,_owner变量恢复为null,_count减1,同时该线程进入_WaitSet集合中等待被唤醒。 - 在_WaitSet 集合中的线程
唤醒后会被再次放到_EntryList 队列中,重新竞争获取锁。 - 若当前线程
执行完毕也将释放Monitor并复位_owner变量的值,以便其他线程进入获取锁。
4.synchronized锁的优化
JDK1.5之前,synchronized是属于重量级锁(Monitor实现的锁属于重量级锁),涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
4.1.自适应性自旋锁
自旋锁:在没有拿到锁的时候,当前线程会进入阻塞状态,当持有锁的线程释放了锁,当前线程才可以再去竞争锁。在线程占用锁的时间很短的话。会浪费大量的性能在阻塞和唤醒的切换上。
为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,而是不断地循环检测锁是否被释放,这就是自旋。在占用锁的时间短的情况下,自旋锁表现的性能是很高的。
自适应性自旋锁:是对自旋锁的一次升级,自适应性自旋锁的意思是,自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
举例就是此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样能
最大化利用资源。
4.2.偏向锁
锁不存在多线程竞争,而且总是由同一线程多次获得锁(同一线程可重入锁)
例如如下代码:
同一个线程多次获得锁
加锁m1—>m2—>m3
释放锁m3—>m2—>m1
期间并不存在竞争,不存在阻塞等待,也不存在唤醒。


原理:
锁的争夺实际上是Monitor对象的争夺,还有每个对象都有一个对象头,对象头是由Mark Word和Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中,当同一个线程再次进入时,就不再进行同步操作,这样就省去了大量的锁申请的操作,从而提高了性能。
只有第一次使用
CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
一旦不同的线程来获取锁的时候,那么偏向锁发现Mark Word中线程id不一样了,就会向上升级为轻量级锁(不会直接升级到重量级锁)
4.3.轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。(交替执行自然不存在线程竞争)

原理:
执行同步代码块之前,JVM会在线程的栈帧中创建一个锁记录(Lock Record),并将Mark Word拷贝复制到锁记录中。然后尝试通过CAS操作将Mark Word中的锁记录的指针,指向创建的Lock Record。如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。

自旋获取锁也失败了,则升级为重量级锁,也就是把线程阻塞起来,等待唤醒。
4.3.重量级锁
也就是上述的synchronized锁,就是一个重量级锁
重量级锁就是一个悲观锁了,但是其实不是最坏的锁,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态,进入阻塞队列,能减少cpu消耗。所以说在不同的场景使用最佳的解决方案才是最好的技术。synchronized在不同的场景会自动选择不同的锁,这样一个升级锁的策略就体现出了这点。
5.总结
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
| 锁 | 对应情况 |
|---|---|
| 偏向锁 | 只被一个线程持有 |
| 轻量级锁 | 不同线程交替持有锁 |
| 重量级锁 | 多线程竞争锁 |
| 锁 | 描述 |
|---|---|
| 重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
| 轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 |
| 偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令 |
一旦锁发生了竞争,都会升级为重量级锁
参考来自:黑马程序员,公众号:java技术爱好者、一灯架构




![“OSError: [WinError 1455]页面文件太小,无法完成操作。”解决方案](https://img-blog.csdnimg.cn/6862885665e44cb385ac97262d9d0434.png)







![【LeetCode】数据结构题解(13)[设计循环链表]](https://img-blog.csdnimg.cn/ff2fb1c912194d748ab01f8ad1bdf91d.png)






