死锁和CAS的相关知识点

什么是死锁?

我们看看这串代码这样写有没有什么问题

public synchronized void increaes(){
        synchronized (this) {
            count++;
        }
    }

调用方法,然后第一个synchronized 先针对this加锁,此时假设加锁成功了

接下来执行到代码块中的synchronized,此时,还是针对this来进行加锁

此时就会产生锁竞争,当前this对象已经处于加锁状态了,此时线程就会阻塞,一直阻塞到锁被释放,才有机会拿到锁

在这个代码中,第一个synchronized 给 this 上的锁,得在 increase 方法执行完毕之后才能释放,但是要想执行完毕,得第二个synchronized 加锁成功获取到锁,方法才能继续执行.

这个时候就出现了矛盾,要想代码继续往下执行,就要把第二次的加锁获取到,也就是把第一次加锁释放,但是我们要想把第一次加锁释放,又需要保证代码先继续执行

此时,由于this 的锁没法释放,这个代码就卡在这里了,因此这个线程就僵住了

这个情况我们就称之为"死锁",这是死锁的第一个体现形式

按理来说,第二次尝试加锁的时候,该线程已经有了这个锁的权限了,这个时候不应该加锁失败,不应该阻塞等待的,但是这里的关键在于,两次加锁都是"同一个线程"

如果是不可重入锁,这把锁不会保存是哪个线程对它加锁的,只要他处于加锁状态之后,收到了"加锁"这样的请求就会拒绝,而不管当下的线程是哪个,就会产生死锁.

可重入锁,则是会让这个锁保存,是哪个线程加上的锁,后续收到加锁请求之后,就会先对比一下,看看加锁的线程是不是当前持有自己这把锁的线程,这个时候就可以灵活判定了

实际上synchronized 本身是一个可重入锁,所以这个代码不会出现死锁的情况

现在我们分析一下这个情况

synchronized (this){     //这一行是真正加了锁的

    synchronized(this){      //虚晃一枪,只是校验了一下,没有真的加锁

        synchronized(this){       虚晃一枪,只是校验了一下,没有真的加锁

              ......

      }       //执行到这个代码,出了这个代码块的时候,刚刚上的锁是否要释放???

   }        //当然不能释放,如果最里层释放了锁,中间的synchronized和最外层的synchronized中的代                   码就没有在锁的保护之中了

}          //在哪加锁,就在哪解锁,所以也在最外面这层解锁

死锁的三种典型情况:

1.一个线程,一把锁,但是是不可重入锁,该线程针对这个锁连续加锁两次,就会出现死锁

2.两个线程,两把锁,这两个线程先分别获取到一把锁,然后再同时尝试获取对方的锁

举个例子,A,B一起去吃饺子,A拿了酱油,B拿了醋,A说你先把醋给我,我用完再给你, B说凭什么不是你先给我酱油,我用完再给你,如果两个人不互相让,就会产生死锁

接下来我们通过代码感受一下死锁

public class Demo25 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) {

        Thread t1=new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);//睡一秒是为了确保在第一把锁拿到的情况下,验证第二把锁是否拿到
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1两把锁加锁成功");
                }
            }
        });
        Thread t2 =new Thread(()->{
            synchronized(locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(locker1){
                    System.out.println("t2两把锁加锁成功");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

运行结果如图

 啥也没运行出来,两个线程都卡在了第二次加锁的地方

如果是一个服务器程序,出现死锁

死锁的线程就僵住了,就无法继续工作了,会对程序造成严重的影响

3.N个线程M把锁

哲学家就餐问题

 如果出现极端情况,就会出现死锁,比如,同一时刻,五个哲学家都想吃面,并且同时伸出左手拿起左边的筷子,再伸出右手拿起右边的筷子,显然五个哲学家都拿不到右边的筷子了,场面就僵持住了

五个哲学家就是五个线程,五根筷子就是五把锁

是否有办法避免死锁呢?

先明确死锁产生的原因,产生死锁的必要条件

这四个条件缺一不可,只要能破坏其中一个,就能避免死锁

1.互斥使用.一个线程获取到一把锁之后,别的线程就不能获取到这个锁                                                                实际使用的锁一般都是互斥的(锁的基本特性) => 无法改变

2.不可抢占.锁只能是被持有者主动释放,而不能被其他线程直接抢走                                                                     也是锁的基本特性 => 无法改变

3.请求和保持.一个线程去尝试获取多把锁,在获取第二把锁的过程中,保持对第一把锁的获取状态                            取决于代码结构 => 如果改变容易影响需求,最好不变

4.循环等待.比如 t1 尝试获取 locker2 ,需要等t2执行完释放locker2;t2 尝试获取locker1,需要等t1执行完释放locker1,他俩都指望对方能执行完,两者逻辑就构成环了                                                                           取决于代码结构 ===>所以只能指望改变这个打破死锁了,这个是解决死锁的关键要点

介绍一个简单,也有效的解决死锁的办法

针对锁进行编号,并且规定加锁的顺序

比如,约定每个线程如果要获取多把锁,必须先获取编号小的锁,后获取编号大的锁

只要所以线程加锁的顺序都严格遵守上述顺序,就一定不会出现循环等待

举个例子

 五个人五只筷子,二号小人按规矩拿了编号最小的一号筷子,三号小人拿了二号筷子,四号小人拿了三号筷子,五号小人拿了四号筷子,而一号小人身边只有一号和五号筷子,一号已经被拿走,所以一号小人就只能阻塞等待,所以五号小人就能拿起五号筷子,凑齐一双,吃完以后放下筷子,四号小人就能拿起四号筷子凑齐一双,吃完以后放下筷子,三号小人就能拿起三号筷子,凑齐一双,吃完以后放下筷子,二号小人就能拿起二号筷子,凑齐一双,吃完以后放下筷子,这时候一号小人就能拿起一号和五号筷子,顺利吃面,所有人都成功吃上了面

接下来我们修改一下上述出现死锁的代码,第一个线程先加锁locker1再加锁locker2 这个顺序是没问题的,但是线程2是先加锁locker2再加锁 locker1 这样是不行的,我们修改一下

public class Demo25 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    public static void main(String[] args) {

        Thread t1=new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);//睡一秒是为了确保在第一把锁拿到的情况下,验证第二把锁是否拿到
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1两把锁加锁成功");
                }
            }
        });
        Thread t2 =new Thread(()->{
            synchronized(locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(locker2){
                    System.out.println("t2两把锁加锁成功");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

这样两个线程都是先加锁locker1,再加锁locker2,这个顺序就每错,运行结果如下

 问题迎刃而解

上述我们针对死锁的讨论非常重要,很经典的面试题,日常开发中也是需要考虑的点

 synchronized 内部实现策略(内部原理

代码中写了一个synchronized 之后,这里可能会产生一系列的"自适应的过程",锁升级(锁膨胀)

无锁->偏向锁->轻量级锁->重量级锁

偏向锁不是真的加锁,只是做了一个标记,如果有别的线程来竞争锁了,才会真的加锁,如果没有别的线程竞争锁,就自始至终都不会真的加锁,加锁本身有一定开销,能不加就不加,等有人竞争了才会真的加锁,有点懒汉模式的思维

轻量级锁

synchronized 通过自旋的方式来实现轻量级锁

我这边把锁占据了,另一个线程就会按照自旋的方式,来反复查询当前的锁的状态是不是被释放了

但是,后续如果竞争这把锁的线程越来越多了(锁冲突更激烈了),就会从轻量级锁升级成重量级锁

轻量级锁是比较消耗CPU资源的,如果能快速拿到锁,多消耗点资源也不亏,但是随着竞争越来越激烈,即使前一个线程释放锁,也不一定能拿到,这个时间可能会比较久

锁消除

编译器会智能判断这个代码是否有必要加锁

如果你写了加锁,但是实际上没有必要加锁,就会把加锁操作自动删除掉,但是有些情况编译器不能作出准确判断,就不一定会优化,所以我们不能全指望编译器来提高代码效率,咱自己也要发挥一些作用,判断何时加锁,也是我们非常重要的工作

锁粗化

关于"锁的粒度"

如果加锁操作里包含的实际要执行的代码越多,就认为锁的粒度越大

 有的时候,希望锁的粒度小比较好,并发程度更高

有的时候,也希望锁的粒度大比较好,因为加锁解锁本身也有开销

把锁变成粒度大就是锁粗化

上述关于synchronized 内部的优化过程,也是经典的面试题

CAS:全称Compare and Swap,字面意思"比较并交换"

能够比较和交换某个寄存器中的值和内存中的值,看是否相等,如果相等,则把另外一个寄存器中的值和内存进行交换

 可以简单理解为赋值操作,把寄存器中的值放进内存,寄存器后面如何就无所谓了

这一段逻辑,是通过一条CPU指令完成的(原子的),这个就给我们编写线程安全代码打开了新世界的大门,基于CAS又能衍生出一套"无锁编程",但是CAS的使用范围具有一定局限性,加锁的适用性范围更广

CAS有哪些应用

1.实现原子类

比如,多线程针对一个count变量进行++

在Java标准库中,已经提供了一组原子类

我们通过代码看看如何使用

import java.util.concurrent.atomic.AtomicInteger;

//使用原子类
public class Demo26 {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i<50000; i++) {
                // count++; //java不像c++/Python 能支持运算符重载,这里必须通过调用方法的方式来完成自增
                count.getAndIncrement();
                //++count
                //count.incrementAndGet();
                //count--
                //count.getAndDecrement();
                //--count
                //count.decrementAndGet();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0;i<50000;i++){
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count.get());
    }
}

 运行结果就是100000,不会像count++没加锁一样出现线程安全问题

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值