多把锁,死锁,活锁,饥饿

文章讨论了在Java并发编程中使用多把锁来提高并发度的概念,解释了死锁的原因和四个必要条件,并提供了手写死锁的示例。通过jconsole工具展示了如何定位和分析死锁。同时,提到了活锁问题以及解决饥饿现象的策略,强调了使用ReentrantLock等机制的重要性。
摘要由CSDN通过智能技术生成

目录

多把锁

多把锁的优缺点

活跃性

死锁

手写死锁

死锁的四个必要条件

定位死锁

jconsole运行命令

jps 定位进程 id,再用 jstack 定位死锁

死锁的三种场景

一个线程一把锁

两个线程两把锁

多个线程多把锁

解决死锁

活锁

饥饿


多把锁

现在有一个场景,有一个大屋子,小南想去睡觉,小女想去学习,两个人做的事毫不相关,如果他们同时想去用一间屋子的话,那么并发度就会很低

我们看一下代码 :

@Slf4j(topic = "c.BigRoom")
public class BigRoom {
    public void studying() {
        synchronized (this) {//对这个大房间加锁
            log.debug("study2000ms....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void sleeping() {
        synchronized (this) {//对这个大房间加锁
            log.debug("sleeping1000ms....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class TestMain {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            bigRoom.studying();
        },"小南").start();
        new Thread(()->{
            bigRoom.sleeping();
        }).start();
    }
}

也就是说他们做的事情都要对同一个房间加锁,也就是只有一个人用完了这个房间,另一个人才能用.

这和我们以前两个线程同时访问一个共享的东西时一样的.

但是现在小南和小女一个要去学习,一个要取睡觉,两个人做的事情毫不相干,就可以去大房子的不同房间去做事.

也就是我们现在使用多把锁->当多个线程干的事情毫不相干就可以使用多把锁.

@Slf4j(topic = "c.BigRoom")
public class BigRoom {
    private static final Object studyingRoom = new Object();
    private static final Object sleepingRoom = new Object();

    public void studying() {
        synchronized (studyingRoom) {
            log.debug("study2000ms....");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void sleeping() {
        synchronized (sleepingRoom) {
            log.debug("sleeping1000ms....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class TestMain {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            bigRoom.studying();
        },"小南").start();
        new Thread(()->{
            bigRoom.sleeping();
        },"小女").start();
    }
}

我们这时候改了一下代码,小南要去学习就去学习房间,小女要去睡觉就去睡觉房间,两者干的事情毫不相关

这时候,就会提高并发度,让锁的粒度变得更细.

多把锁的优缺点

  • 优点 : 可以提高并发度,让锁的粒度变得更细.
  • 缺点 : 如果一个线程同时要获得多把锁,容易发生死锁.

活跃性

死锁

当一个线程同时要获取多把锁,这个时候就容易发生死锁.

就比如 t1线程获得A对象的锁,t2线程获得了B对象的锁,

而此时t1线程又想获取B对象的锁,等待t2线程释放B对象的锁,t2线程又想获取A对象的锁,等待t1线程释放A对象的锁.

也就是双方各自有一把锁,还想获取到对方的锁,这就是死锁现象

手写死锁

@Slf4j(topic = "c.DeadLock")
public class DeadLock {

    private static final Object A = new Object();
    private static final Object B = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                //线程t1获取了A对象的锁,1s之后,又要获取B对象的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
                    log.debug("lockB");
                    log.debug("操作...");
                }
            }
        },"t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                //线程t2获取了B对象的锁,1s之后,又要获取A对象的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

死锁的四个必要条件

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
  • 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系

定位死锁

发生了死锁现象我们怎么来定位死锁呢 ?

我们最经常使用的是jconsole命令工具检测死锁, 也可以使用使用 jps 定位进程 id,再用 jstack 定位死锁

jconsole运行命令

比如我运行刚才死锁的程序.然后利用jconsole来检测死锁.

点击连接.

t1线程也同理

从这里可以看见想获取的锁的持有者,还能看见具体发生死锁现象的代码行数,然后对其进行分析修改打破死锁现象即可.

jps 定位进程 id,再用 jstack 定位死锁

同样也可以从这里可以看见想获取的锁的持有者,还能看见具体发生死锁现象的代码行数,然后对其进行分析修改打破死锁现象即可.

死锁的三种场景

一个线程一把锁

如果是不可重入锁,一个线程加锁两次,就会导致第一个锁的释放依赖第二个锁加锁成功,而第二个加锁成功又依赖第一个锁的释放,导致死锁.

两个线程两把锁

线程A持有资源1,线程B持有资源2,两个线程又同时想得到对方的资源,导致死锁

多个线程多把锁

哲学家就餐问题

最经典的导致死锁的问题,那就是哲学家就餐问题.

哲学家就餐问题说的就是有5位哲学家(相当于5个线程)正在吃饭,但是只有五根筷子(筷子就相当于这5个线程所共享的资源),每位哲学家左边有一个筷子,右边有一根筷子,哲学家只有拿到左边筷子有拿到右边筷子才能吃饭,如果筷子被别的哲学家拿到了,自己只能等待那个哲学家吃完,自己才能吃.

很容易发生死锁的现象,就是五个哲学家同时各拿一根筷子,当哲学家同时又要拿另一根筷子的时候,就会发生死锁,也就是自己持有了一个资源,想获取另一个资源而又等待对方去释放资源.

这样5个哲学家就构成了循环等待的这种关系.

代码实现

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Philosophers")
public class Philosophers extends Thread{
    private Chopstick left;//左手筷子
    private Chopstick right;//右手筷子

    public Philosophers(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while(true) {
            synchronized (left) { //先拿左手筷子
                synchronized (right) { //在拿右手筷子
                    eat();//然后吃饭
                }
            }
        }
    }

    private void eat() {
        log.debug("eating...");
        try {
            Thread.sleep(1000);//思考1s钟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Chopstick {
    private String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Chopstick{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosophers("苏格拉底",c1,c2).start();
        new Philosophers("柏拉图",c2,c3).start();
        new Philosophers("亚里士多德",c3,c4).start();
        new Philosophers("赫拉克利特",c4,c5).start();
        new Philosophers("阿基米德",c5,c1).start();
     }
}

这就会导致死锁,五位哲学家构成了循环等待的这种关系

要想打破死锁,可以使用ReentrantLock,或者打破死锁的必要条件之一,最重要的是循环等待必要条件

解决死锁

我们首先看一下产生死锁的必要条件 :

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。
  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放.(最最重要)

最最重要的就是破坏循环等待条件,

针对每一把锁进行编号,约定在获取多把锁的时候,明确获取锁的顺序(升序还是降序都可以),所有线程都遵守这样的顺序,同一顺序拿到锁,就会反序释放锁,就会打破循环等待

我们按照顺序在执行一下哲学家就餐问题.

每一个哲学家左右有两个筷子,我们按顺序规定从小到大的顺序来拿筷子(比如1和2先拿1号,2和3先拿2),最后,只有一个哲学家同时拿到两根筷子,等到它吃完了,然后在放下筷子,别的哲学家就可以吃,从小到大的拿取筷子,最后就从大到小的放下筷子.

我们分析一下上面的代码为什么避免了死锁的发生?

我们按照一定顺序加锁后当线程1获得了resource1的监事锁,线程2就获取不到了.然后线程1再去获取resource2的监事锁,可以获取到,然后线程1释放了对resource1,resource2的监事锁的占用,线程2获取到就可以执行了,这样就破坏了循环等待条件.

对于代码层面,我们还可以使用ReentrantLock来解决

活锁

活锁 : 两个线程互相改变对方的结束条件,最终谁也没有结束

解决活锁: 执行时间有一定的交错-->让睡眠的时间是随机数

能让其交错开,第一个线程马上要运行完了,第二个线程就没有机会改变对方的结束条件了

import lombok.extern.slf4j.Slf4j;

/**
 * 活锁 : 两个线程互相改变对方的结束条件,最终谁也没有结束
 * 解决活锁: 执行时间有一定的交错-->让睡眠的时间是随机数
 * 能让其交错开,第一个线程马上要运行完了,第二个线程就没有机会改变对方的结束条件了
 */
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
    private static int count = 10;

    public static void main(String[] args) {
        /**
         * t1线程想将count=10减到0
         */
        new Thread(() -> {
             while (count>0) {
                 try {
                     Thread.sleep(200);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 count--;
                 log.debug("count : {}",count);
             }
        },"t1").start();
        /**
         * t2线程想将count=10减到0
         */
        new Thread(() -> {
            while (count<20) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                log.debug("count : {}",count);
            }
        },"t2").start();
    }
}

解决活锁: 执行时间有一定的交错-->让睡眠的时间是随机数

能让其交错开,第一个线程马上要运行完了,第二个线程就没有机会改变对方的结束条件了

饥饿

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

还是拿刚才哲学家就餐问题:

虽然打破了死锁的局面,但是发现有的线程执行次数太少,都被其他线程抢去锁了,这就是一种饥饿现象.(拿不到筷子吃不上饭了,都被别人抢去了)

可以通过ReentrantLock来解决.

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值