1. 前言
锁
在并发编程中非常重要,但是锁的种类有点多。这边文章的目的就是为了梳理锁
的分类以及 锁升级的原理。
2. 锁的分类
种类\名称 | synchronized | ReentrantLock | ReentrantReadWriteLock |
---|---|---|---|
可重入锁 | √ | √ | √ |
不可重入锁 | × | × | × |
乐观锁 | ① | ① | ① |
悲观锁 | √ | √ | √ |
公平锁 | × | √ | √ |
非公平锁 | √ | √ | √ |
互斥锁 | √ | √ | √ |
共享锁 | × | × | √ |
①:Java中提供的CAS操作,就是乐观锁的一种实现。其余的锁的形式都是悲观锁
重入锁
:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的
public static void add() {
synchronized (T11_Thread_Lock01.class) {
synchronized (T11_Thread_Lock01.class) {
}
}
}
不可重入锁
:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到的,因为A锁被当前线程占用着,需要等待自己释放锁再获取锁悲观锁
:获取不到锁资源时,会将当前线程挂起(进入BLOCKED
、WAITING
),线程挂起会涉及到用户态和内核的太的切换,而这种切换是比较消耗资源的乐观锁
:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源,Atomic原子性类中,就是基于CAS乐观锁实现的公平锁
:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队。直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。一句话:排队顺序,一个一个来
非公平锁
:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波- 拿到锁资源:开心,插队成功
- 没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源
互斥锁
:同一时间点,只会有一个线程持有者当前互斥锁共享锁
:同一时间点,当前共享锁可以被多个线程同时持有
3. 类锁以及对象锁
- synchronized的使用一般就是同步方法和同步代码块。
- synchronized的锁是基于对象实现的。
class Test {
// 此方法锁定的是Test.class
public static synchronized void a() {
}
// 此方法锁定的是this
public synchronized void b() {
}
}
static
:此时使用的是当前类.class 作为锁(类锁)非static
:此时使用的是当前对象作为锁(对象锁)
4. synchronized的优化
在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中,对synchronized做了大量的优化
4.1 锁消除
在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了synchronized,他也不会触发
public class T13_Thread_Lock03 {
public synchronized static void add() {
// 方法内无任何临界操作。此时synchronized是可有可无的。 依赖于锁消除
}
}
4.2 锁膨胀
如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗
public class T14_Thread_Lock04 {
private static boolean flag = true;
public static void add() {
while (flag) {
// 此处不停的创建锁 以及释放锁资源
synchronized (T14_Thread_Lock04.class) {
}
}
}
}
4.3 synchronized实现原理
synchronized是基于对象实现的。
先要对Java中对象在堆内存的存储有一个了解。
展开MarkWord
MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、
4.4 锁升级
- 锁默认情况下,开启了偏向锁延迟
- 偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启
- 因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作
- 如果正常开启偏向锁了,那么不会出现无锁状态,对象会直接变为匿名偏向
4.4.1 无锁状态 案例
public class T16_Thread_Lock06 {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
4.4.2 偏向锁 案例
public class T17_Thread_Lock07 {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
4.4.3 轻量级锁 实例
public class T18_Thread_Lock08 {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
4.4.4 重量级锁 案例
public class T19_Thread_Lock09 {
public static void main(String[] args) {
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println("锁竞争:" + ClassLayout.parseInstance(o).toPrintable());
}
}).start();
synchronized (o) {
System.out.println("主线程:" + ClassLayout.parseInstance(o).toPrintable());
}
}
}
4.4.5 锁的升级过程
4.4.6 唤醒偏向锁
的过程
public class T20_Thread_Lock10 {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object o = new Object();
// 延迟5s之后 此对象的MarkWord 标识为 匿名偏向锁
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
// 此对象的MarkWord 标识为 偏向锁
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
- 因为本身存在
偏向锁延迟
所以上述实例直接延迟5sThread.sleep(5000);
. - 如果关闭了偏向锁延迟的话,就没有了无锁态了。直接变为了
匿名偏向锁
- 加锁后,就是
偏向锁
了
5. 总结
锁的相关的部分知识先总结到这,如果还有新的认识小编会及时的补充的,如果对大家有用的话,也希望大家及时关注收藏哦.