一、常见锁策略
乐观锁 & 悲观锁
乐观锁:假设在大多数情况下并发操作不会发生冲突,因此允许多个线程同时进行操作,只有在真正更新数据时才进行冲突检测。
悲观锁:假设并发操作会发生冲突,因此在读取和修改数据时会进行加锁操作,保证同一时间只有一个线程能够操作数据。
synchronized初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。
乐观锁适用于读多写少的情况,可以提高并发性能,但可能会导致重复操作或数据丢失的问题。悲观锁适用于写多读少的情况,能够确保数据的一致性,但并发性能相对较低。
重量级锁 & 轻量级锁
锁的核心特性 “原子性” 这样的机制追根溯源是 CPU 这样的硬件设备提供的
- CPU 提供了“原子操作指令”
- 操作系统基于 CPU 的原子指令,实现了mutex互斥锁。
- JVM基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类
重量级锁:重量级锁是传统的锁实现方式,即当多个线程同时竞争一个锁时,除了获取锁的线程可以进入临界区,其他线程都需要进入阻塞状态等待锁的释放。(加锁的开销比较大,要做更多的工作)加锁机制重度依赖了OS提供mutex(大量的内核态用户态切换,很容易引发线程的调度)
轻量级锁:轻量级锁是一种优化机制,用于替代传统的重量级锁,减少锁的竞争和开销。(加锁的开销比较小,要做的工作相对更少)加锁机制尽可能不使用mutex,而是尽量在用户态代码完成,实在搞不定了,再使用mutex.(少量的内核态用户态切换,不太容易引发线程调度)
轻量级锁适用于多线程并发度不高、同步代码执行时间短的场景,因为轻量级锁的获取和释放都是在用户态下完成的,不需要切换到内核态。重量级锁适用于多线程并发度高、同步代码执行时间长的场景,因为重量级锁需要通过操作系统的原语来实现,对于等待锁的线程会进入阻塞状态,减少了不必要的自旋等待。
挂起等待锁 & 自旋锁
挂起等待锁: 当一个线程尝试获取一个锁但当前锁已经被其他线程占用时,该线程会被挂起(即进入等待状态),直到锁可用为止。在挂起等待锁的策略中,线程会被移出运行状态,不占用CPU资源,并且线程的状态会被切换为等待状态,直到锁释放后被唤醒。
自旋锁: 自旋锁是一种等待锁的策略,当一个线程尝试获取一个锁但当前锁已经被其他线程占用时,该线程不会立即被挂起,而是进入自旋状态,持续不断地检查锁是否可用。在自旋锁的策略中,线程会一直占用CPU资源,不会被挂起。
公平锁 & 非公平锁
公平锁:遵循"先来后到"
非公平锁:不遵循"先来后到"
可重入锁 & 不可重入锁
可重入锁是指线程在持有一个锁的情况下,可以再次获取该锁,而不会被自己持有的锁所阻塞。
不可重入锁是指线程在持有一个锁的情况下,再次尝试去获取该锁时会被阻塞。
读写锁
读写锁(ReadWrite Lock)是一种特殊的锁,用于在多线程环境下进行读和写操作的并发控制。它与普通的互斥锁(如synchronized)不同,读写锁允许多个线程同时读取共享资源,但在写操作时需要独占锁定,禁止其他线程进行读或写操作。
一个线程对于数据的访问,主要存在两种操作:读数据 和 写数据
- 两个线程都是只读一个数据,此时并没有线程安全问题,直接并发读取即可
- 两个线程都要写一个数据,有线程安全问题
- 一个线程读另一个线程写,有线程安全问题
读写锁就是把读操作和写操作区分对待,Java标准库提供了 ReentrantReadWriteLock 类,实现了读写锁
public class reentrantLock {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
lock.lock();
count++;
lock.unlock();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
lock.lock();
count++;
lock.unlock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
//输出结果100000
}
}
- 读加锁和读加锁之间,不互斥
- 写加锁和写加锁之间,互斥
- 读加锁和写加锁之间,互斥
读写锁特别适合于 "读多写少" 的场景中
二 CAS
CAS(Compare and Swap,比较并交换)是一种常见的并发控制机制,用于实现无锁算法和线程安全操作。
CAS操作的基本流程:
-
读取内存中的值。
-
比较内存中的值与期望值是否相等。
-
如果相等,将新值写入内存,操作成功。
-
如果不相等,说明其他线程已修改了值,重新读取内存中的值,重复上述步骤。
CAS具体使用场景
(1)实现原子类
标准库中提供了java.util.concurrent.atomic包,里面的类都是基于这种方式来实现的,典型的就是AtomicInteger类
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();//count++
// count.incrementAndGet();//++count
// count.getAndDecrement();//count--
// count.decrementAndGet();//--count
// count.getAndAdd(10);//count+=10
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count.getAndIncrement();//count++
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count.get());
}
伪代码实现
class AtomicInteger{
private int value;
public int getAndIncrement(){
int oldValue = value;
while(CAS(value,oldValue,oldValue+1)!=true){
oldValue = value;
}
return oldValue;
}
}
(2)自旋锁
伪代码实现:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就⾃旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null,Thread.currentThread()){
}
}
public void unlock() {
this.owner = null;
}
}
CAS的ABA问题
CAS的ABA问题是指在使用CAS操作时,可能会出现一个线程在进行CAS操作之前,读取到的值与期望值相等,但是在执行CAS操作之前,其他线程对这个值进行了一系列修改,最终又恢复成与期望值相等的值(即发生了一次ABA操作),导致CAS操作成功,但实际上中间的修改操作被忽略了。
例如:
假设初始值为A,线程1读取到A后,线程2将A修改为B,然后又将B修改回A,最后线程1执行CAS操作将值由A改为C,CAS操作成功。在线程1看来,CAS操作前后值都是A,所以认为操作是正确的,但实际上中间发生了修改,导致了数据的不一致。
ABA问题可能会导致一些潜在的问题,例如内存泄漏、数据错误等。
解决方案
给要修改的值,引入版本号.在CAS比较数据当前值和旧值的同时,也要比较版本号是否符合预期
在Java中,AtomicStampedReference就是一种带有标记的CAS操作类,它通过维护一个版本号来解决ABA问题。每次修改都会增加版本号,这样在CAS操作时,即使值相同但版本号不一致,也能保证CAS操作失败,防止了ABA问题的发生。
三 synchronized原理
基本特点
- 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁
-
开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁
-
自旋 | 挂起等待锁 ,自适应
-
不公平锁
-
可重入锁
-
不是读写锁
加锁过程
刚开始使用synchronized加锁,首先锁会处于 "偏向锁" 状态,遇到线程之间的锁竞争,升级到 "轻量级锁" 进一步的统计竞争出现的频次,达到一定程度后,升级到"重量级锁"
synchronized加锁的时候,会经历 无锁=>偏向锁=>轻量级锁=>重量级锁
锁升级的过程不可逆
锁消除
编译器+JVM判断锁是否可以消除,如果可以,就直接消除.
锁粗化
上图中,上面的锁粒度更细,下面的锁粒度更粗
锁粗化,就是把多个"细粒度"的锁合并成"粗粒度"的锁,避免频繁申请释放锁
例如,下属小明给领导汇报工作:
方式一:
打电话,汇报工作1,挂电话
打电话,汇报工作2,挂电话
打电话,汇报工作3,挂电话
方式二:
打电话,汇报工作1,2,3,挂电话
显然,方式2更高效