文章目录
一、死锁
1.什么是死锁
死锁是指由于两个或者两个以上的线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。
甲和乙两名同学做卫生,但是只有一把扫帚和簸箕。甲同学拿到了扫帚,乙同学拿到了簸箕,此时甲同学要等乙同学的簸箕才能做卫生,而乙同学要等甲同学的扫帚才能做卫生。这个时候就形成了僵局,也就是死锁。
2.死锁产生条件
-
互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 甲拿到扫帚,乙就不可能拿到扫帚
-
不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 甲虽然很需要簸箕,但是不能直接抢过来只能等乙自己给甲
-
请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 甲虽然没有簸箕导致不能做卫生,但也不能主动把扫帚给乙
-
循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路
- 甲拿着扫帚在等乙的簸箕,而乙拿着簸箕等甲的扫帚,形成闭环
3.死锁的三种场景
3.1 一个线程,针对同一把锁
public static void main(String[] args) {
Object lock = new Object();
Thread t = new Thread(()->{
synchronized (lock){
synchronized (lock){
}
}
});
}
t线程针对lock这个锁对象进行了两次加锁操作
如果是
可重入锁
,不会产生死锁,如果不是可重入锁,产生死锁。
3.2 多个线程,针对同一把锁
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
//t1线程先抢lock1,再抢lock2
Thread t1 = new Thread(()->{
synchronized (lock1){
synchronized(lock2){
}
}
});
//t2线程先抢lock2,再抢lock1
Thread t2 = new Thread(()->{
synchronized (lock2){
synchronized(lock1){
}
}
});
}
t1占用了lock1,t2占用了lock2,当t1想再去占用lock2时,发现lock2已经被占用,同理t2也不能占用lock1,就形成了死锁。
这种情况即即使是
可重入锁
也会发生死锁
3.3 多个线程,针对多个锁
哲学家就餐问题
假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有五碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。
如果所有哲学家都抢到了左边的餐叉,再去抢右边的餐叉发现已经被抢了,且所有哲学家都不会放在已经抢到的餐叉,只会等待有右边的餐叉,这就形成了死锁。
打破任意一个产生死锁的条件就能解决问题,其中循环等待是最容易打破的
解决办法:给所有资源编号,规定进程请求所需资源的顺序必须按照一定的约定执行
比如我们约定,只能从编号小的锁开始加锁
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(()->{
synchronized (lock1){
synchronized(lock2){
}
}
});
Thread t2 = new Thread(()->{
synchronized (lock1){
synchronized(lock2){
}
}
});
}
约定了抢占锁的顺序之后,就可以避免死锁了。
二、CAS
1.什么是CAS
CAS: Compare and swap,寄存器A中的值如果和内存中M的值相等,就把寄存器B中的值和M的值进行交换
比较 A 与 V 是否相等。(比较)
如果比较相等,将 B 写入 V。(交换)
返回操作是否成功。
虽然CAS中既有比较,也有赋值操作,但CAS仍然属于一条CPU指令【原子性】
伪代码:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
**CAS作用:**CAS可以在不加锁的情况下保证线程的安全
2.CAS应用
2.1实现原子类
标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类,这个类能够保证执行加或者减的时候线程安全。
AtomicInteger伪代码:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
oldValue:表示寄存器的值
( CAS(value, oldValue, oldValue+1) != true : 如果内存中的value和寄存器中的oldValue相等,就把oldValue++,然后返回true,循环结束;如果不相等,返回false,进入循环,重新设置oldValue的值。
注意:oldValue++之后应该返回true,但是在多线程环境下,可能会返回false。CAS能保证线程的安全,就是因为它会判断oldValue是否发生过变化,如果发生了会先更新再比较
2.2 实现自旋锁
伪代码:
public class SpinLock {
//ownerv用来记录当前锁被那个线程所持有
private Thread ownerv = null;
public void lock(){
//如果owner为null,那就比较成功,然后当前线程就持有这个锁
//如果owner不为null,比较失败,返回false,继续执行循环
//循环执行速度很快,一旦ownerv没有被其他线程持有,马上就能获取到
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
注意:CAS本身就是指令级别,属于读取内存的操作,不存在编译器优化导致内存可见性
3.CAS的ABA问题
CAS做的事情是比较内存中的值前后是否相等,但不能判断中间过程是否发生过变化。a->b->a
CAS是能通过的,这就引出了CAS中的ABA问题。
比如你买了一个手机,这个手机有可能是纯新机,有可能是翻新机,一个普通用户区分不了就认为翻新机也是纯新机。
解决办法:使用版本号,同时约定版本号自增,最后比较的时候就去比较版本号,只要版本号不一样,就认为是发生了变化