前言
本篇博客是《Java锁深入理解》系列博客的第三篇,建议依次阅读。
各篇博客链接如下:
Java锁深入理解1——概述及总结
Java锁深入理解2——ReentrantLock
Java锁深入理解3——synchronized
Java锁深入理解4——ReentrantLock VS synchronized
Java锁深入理解5——共享锁
Demo
//作用在方法上。锁对象就是当前对象,临界区就是整个方法
public synchronized void m1() {
System.out.println("----------do something...");
}
//作用在代码块
final Object lock = new Object();//定义一个锁对象
public void m2() {
synchronized (lock) {
System.out.println("----------do something...");
}
}
这两个方法被调用时,在synchronized的包围圈内(方法内或者,代码块内),都可以保证线程安全(但线程要调用同一个对象的这两个方法,否则锁对象都变了,就不存在保证线程安全了)。
效果和ReentrantLock的lock-unlock一样。
/**
* 锁对象:当前类的对象
* 功能:阻塞
*/
public synchronized void m4(){
System.out.println("----------m4");
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* 锁对象:lockObj
* 功能:阻塞
*/
public void m5(){
synchronized(lockObj) {
System.out.println("----------m5");
try {
lockObj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
这两个方法演示了wait方法都用法:wait写在synchronized包围的临界区内。
用法和功能和ReentrantLock的await方法一样。甚至底层实现方法都是park(给操作系统一个信号量,将线程上下文保存之后挂起来)
public synchronized void m6() {
this.notify();
}
上面这个方法演示了notify方法都用法:同样要写在synchronized包围的临界区内。用法和功能和ReentrantLock的signal方法一样。
注意:synchronized作为一个关键字,可以写在很多地方:
作用位置 | 锁 |
---|---|
方法上 | 当前对象 |
静态方法上 | 当前类 |
成员变量上 | 成员变量 |
只要同一个锁,那么同一时刻只能有一个线程访问这个锁管控的资源。即便是两块不同的资源,只要被同一个锁 锁住,那么同时只能一个线程访问。
典型的就是一个类中多个方法都用了synchronized,那么同一时刻只有一个方法可以被一个线程访问。(没有synchronized的方法不受锁限制)
内部机制
从synchronized的发展来讲,可以这么理解:
一开始synchronized就相当于ReentrantLock(原理也基本一样),也就是现在所说的其中的重量级锁。后来发现性能太低,所以增加了轻量级锁和偏向锁。增加了锁升级机制。
深层原因,synchronized和ReentrantLock都是MESA管程模型的不同实现。下面是他们各自一些关键概念的对照:
MESA模型 | 同步队列 | 条件队列 | wait | notify | |
---|---|---|---|---|---|
AQS | state | 同步队列(sync queue) | 条件队列(condition queue) | await | signal |
synchronize | count | _EntryList | _WaitSet | wait | notify |
synchronized和ReentrantLock不一样的地方
- synchronized由JVM实现,是C++写的。AQS由JDK实现,Java写的。
- synchronized只有非公平锁。可以认为synchronized和ReentrantLock的非公平锁实现的原理基本一致。
- synchronized可以把所有对象当成锁。因为每个对象都可生成一个对应的ObjectMonitor(类似AQS),而且每个对象头上都还有锁标记。
- synchronized存在锁升级过程(后面详细讲这一点)
- synchronized退出锁(解锁)同样需要CAS。因为前面的”锁升级“。自己还在临界区里没出来,外面拿不到锁”气急败坏“的线程,可能就把锁升级了。
- synchronized不会被中断。(后面会讲)
synchronized锁升级
Java起初并没有这个机制,使用synchronized就是重量级锁,需要在用户态-内核态之间切换,性能消耗较大。但实际应用场景中,多数情况下,锁竞争并没有那么激烈。重量级锁带来的性能问题显得没有必要。自1.6后优化的功能。
- 无锁状态
如果没有线程访问,那么这个锁就是无锁状态。 - 偏向锁
当一个线程发现锁是“无锁”状态,那么它就会进行自旋CAS获得锁(改锁状态为“偏向锁”,同时把自己的线程ID写入锁对象头中) - 轻量级锁
当一个线程发现锁是“偏向锁”状态,那么它会进行CAS尝试获得锁(一般情况下都能获得到)。
但如果它CAS失败,那么它就会把这个锁升级为轻量级锁(把锁状态改为“轻量级”,然后在自己帧栈中新增一块空间,来存锁头内容。把自己这块空间的地址存到锁对象头中)。 - 重量级锁
当一个线程发现锁是“轻量级锁”,它同样会进行CAS尝试获得锁。
如果它CAS失败,那么它就会把这个锁升级为重量级别锁。
锁升级后会降级吗
无论说不会(第一层传统认知),还是会(第二层)都不完全对。
说不会的,那就是网上的普遍说法。然后这还导致很多人产生这样的认知:锁对象被改成重量级锁之后,之后就算没线程访问,依然保持重量级锁的状态。
我们做实验就很容易发现这个认知是错误的。如这篇博客就指出了这个问题。但其实这种认知也是不完全正确的。也就是说,一开始被广泛传播的说法并非空穴来风。只是表述有问题,才引起了大家误解。
正确的解释是:它不会降级,但会在释放锁之后直接回归无锁状态(之后有线程访问,还会继续升)。
但是,并不是说前一个线程释放锁之后,锁立马回归无锁状态,而是要等个几秒。如果前一个线程释放完锁,然后唤醒了后面的线程继续运行,说明此时处于激烈竞争时期,锁是不会回归无锁状态的,毕竟升降都很消耗性能。
(在上面那篇博客的评论区里,我也指出了这个问题,并给出了一个佐证)
大家可以根据那个博客自己做实验验证,挺简单的。
关于synchronized,我就不多讲了,因为它的重量级锁(也就是它最核心的锁)和ReentrantLock的非公平锁是一样的,都是管程的实现。代码逻辑也几乎一样,这里就不再重复了。如果你想看详细分析一下,请看这篇文章(关于synchronized,这篇文章讲的挺详细的)。
synchronized是怎么释放锁的
看一下demo的反编译
使用synchronized关键字后,代码逻辑有两个出口(monitorexit):一个正常出口,一个异常出口。
所以:synchronized无论是正常结束,还是异常结束,都会有JVM自动释放锁。