文章目录
常见的锁策略
乐观锁 VS 悲观锁
乐观锁和悲观锁描述的是两种不同的加锁态度。
乐观锁:预测锁冲突的概率不高,因此做的工作就可以简单一点。
悲观锁:预测锁冲突的概率较高,因此做的工作就要复杂一点。
比如:你明天就要去面试:
乐观的做法:早睡早起,按时正常去参加面试流程。
悲观的做法:晚上熬夜复习、订好回家的机票、第二天早上早早起来准备、提前到公司一段时间再参加面试。
读写锁 VS 普通互斥锁
普通互斥锁:就像synchronized,当两个线程竞争同一把锁,就会有人得阻塞等待。
读写锁:分为加读锁和加写锁
读锁和读锁之间,不会产生竞争。 多个线程同时读一个变量没事儿。
写锁和写锁之间,会产生竞争。 就像普通互斥锁,有“锁竞争”,发生阻塞等待。
读锁和写锁之间,也会产生竞争。 就像普通互斥锁,有“锁竞争”,发生阻塞等待。
注:在实际环境中,读的频率要远远大于写的频率。加读写锁就会少很多的“锁竞争”,优化了执行效率
重量级锁 VS 轻量级锁
重量级锁:加锁和解锁开销比较大。 是典型的从用户态进入内核态的逻辑,开销较大。
轻量级锁:加锁和解锁开销比较小。 是典型的纯用户态的逻辑,开销较小。
注:
- 乐观锁和悲观锁是站在过程的角度考虑:看重加锁解锁过程中工作做的多少。
- 重量级锁和轻量级锁是站在结果的角度考虑:看重加锁解锁过程中消耗时间的多少。
- 通常情况下,乐观锁是轻量级锁,悲观锁是重量级锁。
自旋锁 VS 挂起等待锁
自旋锁:如果获取锁失败,立即再次尝试重新获取锁,无限循环,直到获取到了锁。
优点:第一:不释放CPU资源;第二:如果其他线程释放了锁,这个线程就能马上获取到锁。
缺点:如果其他线程持有锁的时间较长时,会造成CPU资源的浪费。
挂起等待锁:如果获取锁失败,则进入阻塞等待状态,一段时间后再次尝试重新获取锁。
优点:在阻塞等待阶段会释放CPU资源。
缺点:不能及时获取到锁。
公平锁 VS 非公平锁
前提:三个线程请求获取锁的先后顺序:t1、t2、t3
公平锁:遵守先来后到。t1先获取到锁、然后t2获取到锁、最后t3获取到锁
非公平锁:随机调度。t1、t2、t3谁能先获取到锁是随机的
可重入锁 VS 不可重入锁
可重入锁:同一个线程针对同一把锁,连续加锁两次,不会死锁。
不可重入锁:同一个线程针对同一把锁,连续加锁两次,会死锁。
总结:
对于synchronized
- 既是乐观锁也是悲观锁
- 既是轻量级锁也是重量级锁
- 乐观锁的部分是基于自旋锁实现的;悲观锁的部分是基于挂起等待锁实现的
- 是普通互斥锁不是读写锁
- 是非公平锁
- 是可重入锁
注:
synchronized是自适应的。初始使用的时候是乐观锁、轻量级锁、自旋锁;如果当前“锁竞争”不激烈,就保持最开始的状态不变。如果“锁竞争”激烈,就会自动升级为悲观锁、重量级锁、挂起等待锁。
CAS
什么是CAS?
CAS(Compare And Swap):比较和交换。
即:把内存中的某个值和CPU寄存器A中的值进行比较。如果两个值相同,就把寄存器B中的值和内存中的值进行交换。如果不同,就不做操作。
优势:这个操作是通过一条指令来完成的。所以是线程安全的,也是高效率的。
CAS的使用
- 实现原子类
//原子类 多用于计数
//count.getAndIncrement = count++
//count.incrementAndGer = ++count
//count.getAndDecrement = count--
//count.decrementAndGer = --count
public static void main(String[] args) {
AtomicInteger count = new AtomicInteger();
Thread thread = new Thread(() -> {
for (int i = 0; i < 50000; i++){
//相当与count++;
count.getAndIncrement();
}
});
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 50000; i++){
//相当于count++;
count.getAndIncrement();
}
});
thread.start();
thread1.start();
try {
thread.join();
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.get());
}
- 实现自旋锁
CAS的ABA问题
在CAS中进行比较的时候,当主内存和寄存器A中的值相同时,我们无法判断主内存中的值是一直没变,还是已经变了又变回来了。
前提:用户的存款有1000元
解决办法:
另外搞一个内存(寄存器C),记录内存中数据的变化。比如:
- 保存主内存的修改次数,只增不减
- 保存主内存的版本号, 只增不减
- 保存上次修改时间 只增不减
在每次比较的时候,同时比较寄存器A和寄存器C中读到的数据和主内存中的数据是否一致。
死锁
死锁:一个线程在加上锁之后,就无法释放锁了。
场景一:一个线程,一把锁,该线程连续加锁两次。
解决:使用可重入锁,比如:synchronized
场景二:两个线程,两把锁。
解决:设计时考虑周到
//死锁的案例
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (locker1){
System.out.println("在locker1里,获取对locker1的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2){
System.out.println("在locker1里,获取对locker2的锁");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (locker2){
System.out.println("在locker2里,获取对locker2的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker1){
System.out.println("在locker2里,获取对locker1的锁");
}
}
});
thread1.start();
thread2.start();
}
}
场景三:多个线程,多把锁。“哲学家就餐问题”
解决:1. 约定必须先拿哪一把锁再拿哪一把锁 2. “银行家算法”
总结:
死锁的必要条件:
- 互斥使用。锁A被线程1占用,线程2就用不了
- 不可抢占。锁A被线程1占用,线程2就不能把锁A抢过来,除非线程1主动释放
- 请求和保持。有多把锁,线程1拿到锁A后,不想释放锁A,还想再拿一个锁B
- 循环等待。线程1等待线程2释放锁,线程2等待线程1释放锁。
对应的解决:
- 锁的基本特性,解决不了。
- 锁的基本特性,解决不了。
- 在写代码时自己注意,不普适。
- 约定好加锁顺序,就可以避免循环等待。比如:给锁编号。