目录
一、常见的锁策略
🍅1、常见的锁策略
🍅2、Synchronized实现了哪些锁策略?
🍅3、自旋锁的实现方式—CAS
(1)CAS伪代码
(2)演示 使用CAS方式来实现自增操作:
(3)分析上述自增的getAndIncrement函数过程
(4)CAS实现自旋锁
(5)CAS的ABA问题(面试题)
二、Synchronized原理
🍅1、synchronized在不同时期的锁策略
🍅2、代码演示不同时期的锁策略
🍅3、锁消除与锁粗化
进阶内容存在很多八股内容,工作中不常用,但是面试常考。
一、常见的锁策略
🍅1、常见的锁策略
1、乐观锁 && 悲观锁
乐观锁:对运行环境持有乐观态度,刚开始的时候不加锁,等有竞争的时候再去加锁;
悲观锁:对运行环境持有悲观态度,刚开始就加锁。
2、轻量级锁 && 重量级锁
区分两者主要看在实现锁的过程中,消耗的资源多不多。
轻量级锁:纯用户态的锁,消耗的资源比较少;
重量级锁:可能会调用到系统的内核态,消耗的资源比较多。
3、读写锁 && 普通互斥锁
在现实中并不是所有的锁都要互斥,互斥必然会消耗掉很多的资源,所以优化出读写锁。
读锁:是共享锁,读和读可以同时进行拿到锁资源;
写锁:是排它锁,不能同时写写,写读,读写。
普通互斥锁:synchronized,只能一个线程拿到锁资源,其他的要参与锁竞争,当没有竞争到锁的时候就要阻塞等待。
4、自旋锁 && 挂起等待锁
自旋锁:不停的询问资源是否释放,如果释放了第一时间可以获取到锁资源;
挂起等待锁:等待通知之后再去竞争锁,并不会第一时间获取到锁资源。
5、可重入锁 && 不可重入锁
可重入锁:对于同一个锁对象可以加多次锁;
不可重入锁:不能对同一个对象加多次锁。
6、公平锁 && 非公平锁
公平锁:先排队等待的线程先获取到锁资源
非公平锁:谁先抢到锁资源就是谁的,没有先来后到这一说。
🍅2、Synchronized实现了哪些锁策略?
乐观锁✅ | & | 悲观锁✅ |
轻量级锁✅ | & | 重量级锁✅ |
读写锁 | & | 普通互斥锁✅ |
自旋锁✅ | & | 挂起等待锁✅ |
可重入锁✅ | & | 不可重入锁 |
公平锁 | & | 非公平锁 ✅ |
注意:其中轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待锁实现的。
自旋锁是基于CAS实现的。
🍅3、自旋锁的实现方式—CAS
CAS:Compare And Swap(比较并交换)
(1)CAS伪代码
一个 CAS 涉及到以下操作:
1. 比较 A 与 V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V。(交换)
3. 返回操作是否成功。
CAS操作:去LOAD一个内存地址中的值,然后跟我们的期望值去比较宁并赋值的过程, 可以实现一个原子类。CAS操作将LOAD,CMP,STORE打包成一个指令,不需要再加锁了。
(2)演示 使用CAS方式来实现自增操作:
public static void main(String[] args) throws InterruptedException {
//1、直接调用JDK中定义好的,原子整型
AtomicInteger atomicInteger = new AtomicInteger();
//2、变量1自增
Thread t1 =new Thread(()->{
for (int i = 0; i < 50000; i++) {
//3、自增操作;通过getAndIncrement()方式,而不是直接++;
atomicInteger.getAndIncrement();
}
});
t1.start();
//变量2自增
Thread t2 =new Thread(()->{
for (int i = 0; i < 50000; i++) {
//自增操作;通过getAndIncrement()方式,而不是直接++;
atomicInteger.getAndIncrement();
}
});
t2.start();
//4、等待两个线程执行完成
t1.join();
t2.join();
//5、打印结果
System.out.println(atomicInteger.get());
}
(3)分析上述自增的getAndIncrement函数过程
上述自增过程图解:
可以看出,两个线程同CAS同时对一个共享变量做自增操作,通过不停的自旋检查预期值就可以保证线程安全,没有通过加锁就可以实现正确的自增。whilie循环是在应用层执行的,也就是用户态中执行,所以比内核态的锁效率要高很多。其中,cmpxchg是CPU中的一条指令,可以完成CAS的整个操作(比较并交换),是从硬件层面上支持了原子性。
(4)CAS实现自旋锁
(5)CAS的ABA问题(面试题)
A B A分别表示预期值的三个状态。如果CAS出现ABA问题,可能会造成的影响。
🌰 场景:
(1)A和B约定好今天要给C转账1000元;
(2)如果晚上10.00前A太忙了还没来得及转账,那就由B来转账;
(3)下午6.00,A账户当前一共有2k,转账过去1k后只剩下1k;
(4)下午7.00,A账户收到来自公司的一笔加班费1k,此时账户一共2k;
(5)等到晚上10.00,B检查了一下A的账户,发现还是2k,以为没有转账,又从自己的账户发起了1k的转账给C。
(6)最终的结果就是C中多了一笔钱。
可以看出,A的账户中的虽然是2k,但是已经不是原来的同一个值了。两个A 虽然在校验的时候可以通过,但是中途经过了修改或者其他操作,最后已经不是同一个值了。
解决ABA问题:
给预期值加一个版本号,在做CAS操作的时候同时更新预期值的版本号,版本号只增不减。
预期值 A B A
版本号 1 2 3
关于CAS:
(1)先获取预期值
(2)通过CAS指定完成比较并交换
(3)如果在CAS的过程中,预期值与真实值不相等,就进入自旋操作;
(4)如果出现ABA问题,就是给预期值加一个版本号,在比较的时候同时比较预期值和版本号。
二、Synchronized原理
🍅1、synchronized在不同时期的锁策略
synchronized在不同的时期可能会用到不同的锁策略。
🌰 场景:
(1)一栋正在新建的大楼:每一层都有一个装修师傅,有一个卫生间。那么每一层的装修师傅去卫生间的时候可以在卫生间门口贴一个标签,但是并没有真正的上锁。因为不存在锁竞争,这个贴标签的过程可以理解为是偏向锁。
(2)随着任务越来越重,每一层现在有多了一个装修师傅,那么这个时候两个人需要竞争,演化为轻量级锁;
(3)随着大楼建设好,打工人开始入驻,此时每一层的卫生间的竞争压力越来越大,就需要排队等待资源,演化为重量级锁。
🍅2、代码演示不同时期的锁策略
大家可以自己试着下面的代码跑一下,观察一下不同状态下的锁状态,可以有更深的理解。 在pom.xml中加入以下代码:
<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
</dependencies>
public class a02_synchronizedLock {
//通过这个类可以查看锁对象的对象头中的信息。
// 定义一些变量
private int count;
private long count1 = 200;
private String hello = "";
// 定义一个对象变量
private TestLayout test001 = new TestLayout();
public static void main(String[] args) throws InterruptedException {
// 创建一个对象的实例
Object obj = new Object();
// 打印实例布局
System.out.println("=== 任意Object对象布局,起初为无锁状态");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println("=== 延时4S开启偏向锁");
// 延时4S开启偏向锁
Thread.sleep(5000);
// 创建本类的实例
a02_synchronizedLock monitor = new a02_synchronizedLock();
// 打印实例布局,注意查看锁状态为偏向锁
System.out.println("=== 打印实例布局,注意查看锁状态为偏向锁");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
System.out.println("==== synchronized加锁");
// 加锁后观察加锁信息
synchronized (monitor) {
System.out.println("==== 第一层synchronized加锁后");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
// 锁重入,查看锁信息
synchronized (monitor) {
System.out.println("==== 第二层synchronized加锁后,锁重入");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
// 释放里层的锁
System.out.println("==== 释放内层锁后");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
// 释放所有锁之后
System.out.println("==== 释放 所有锁");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
System.out.println("==== 多个线程参与锁竞争,观察锁状态");
Thread thread1 = new Thread(() -> {
synchronized (monitor) {
System.out.println("=== 在线程A 中获取锁,参与锁竞争,当前只有线程A 竞争锁,轻度锁竞争");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
});
thread1.start();
// 休眠一会,不与线程A 激烈竞争
Thread.sleep(100);
Thread thread2 = new Thread(() -> {
synchronized (monitor) {
System.out.println("=== 在线程B 中获取锁,与其他线程进行锁竞争");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
});
thread2.start();
// 不休眠直接竞争锁,产生激烈竞争
System.out.println("==== 不休眠直接竞争锁,产生激烈竞争");
synchronized (monitor) {
// 加锁后的类对象
System.out.println("==== 与线程B 产生激烈的锁竞争,观察锁状态为fat lock");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}
// 休眠一会释放锁后
Thread.sleep(100);
System.out.println("==== 释放锁后");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
System.out.println("===========================================================================================");
// 调用hashCode后才保存hashCode的值
monitor.hashCode();
// 调用hashCode后观察现象
System.out.println("==== 调用hashCode后查看hashCode的值");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
// 强制执行垃圾回收
System.gc();
// 观察GC计数
System.out.println("==== 调用GC后查看age的值");
System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
// 打印类布局,注意调用的方法不同
System.out.println("==== 查看类布局");
System.out.println(ClassLayout.parseClass(a02_synchronizedLock.class).toPrintable());
// 打印类对象布局
System.out.println("==== 查看类对象布局");
System.out.println(ClassLayout.parseInstance(a02_synchronizedLock.class).toPrintable());
}
}
class TestLayout {
}
🍅3、锁消除与锁粗化
1、锁消除
在写代码的时候,程序员会加synchronized来保证线程的安全。如果加了synchronized的代码块中,只有读操作,没有写操作,那么JVM就会认为这个代码没有必要加锁,JVM在运行的时候就会被优化掉,这个现象就叫做锁消除。也就是说过滤到无效的synchronized来提高效率。锁消除的前提是:JVM只有100%的把握的时候才会进行优化。
2、锁粗化
代码有一连串的方法调用,方法1-4都加了synchronized,代码执行的时候是先执行方法1,出了方法1继续执行方法2,依次类推。在这个过程中执行一个业务逻辑要进行4次锁竞争。因此,在保证程序执行真确的前提下,JVM会做出优化,只加一次锁,整个逻辑执行完之后再释放。
加油~~