【多线程:活跃性】死锁、活锁、饥饿

简介: 【多线程:活跃性】死锁、活锁、饥饿

【多线程:活跃性】死锁、活锁、饥饿

01.介绍

死锁:两个线程 t1 t2,两把锁 A B,t1持有A锁 t2持有B锁,此时t1想要再获得B锁 t2想要再获得A锁,但是它们都不释放自己所持有的锁,最终导致死锁。
活锁:两个线程互相改变对方的结束条件,导致最后谁也没办法结束
饥饿:多个线程中 有一个线程t,由于线程之间的冲突 导致t线程分的时间片低,导致t线程基本不运行,这种情况下t线程就处于饥饿

02.死锁

案例

两个线程 t1 t2,两把锁 A B,t1持有A锁 t2持有B锁,此时t1想要再获得B锁 t2想要再获得A锁

package cn.itcast.n4.deadlock;

import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

结果

01:08:32.822 c.TestDeadLock [t1] - lock A
01:08:32.822 c.TestDeadLock [t2] - lock B

解释
可以看出t1 t2线程都尝试获取对方的锁,但最终陷入死锁,代码没有往下执行

定位死锁

前提:运行上述案例的代码,用下面的方法查看是否死锁 以及 死锁信息

方法一:命令行jstack

首先我们用jps命令找到 我们需要的java程序 也就是 TestDeadLock

然后我们用jstack命令 查看19008进程的线程详细信息

我们在里面发现了记录死锁的信息,可以看出是 t1 t2线程死锁

再后面列出死锁的详细信息,包括死锁的位置

方法二
通过图像界面工具jconsole来查看

首先我们win+R搜索jconsole

然后进入图形界面选择TestDeadLock 然后连接

然后我们点击左上方线程,查看线程的详细信息,再点击左下角的检查死锁

我们看出死锁线程为 t1 t2,点击t1 t2线程可以查看详细信息

03.死锁-哲学家就餐问题

介绍


哲学家就餐问题是操作系统中介绍死锁时的经典案例。
这个故事说的是,有五位哲学家 坐在圆桌旁,他们只做两件事,吃饭和思考,思考一会吃口饭 吃完饭继续思考。吃饭时需要两根筷子,桌子上共有5根筷子,每位哲学家左右手各有一根筷子,如果身边的筷子被拿完,自己就得等待。
条件补充
我们规定 每个人都先拿自己左手边的筷子 再拿右手边的筷子。
问题矛盾点
我们发现 如果按上述描述规则吃饭,势必会有一种情况 就是 每个人都拿到自己左手边的筷子 但还需要右手边的筷子,但是右手边的筷子已经被占用,导致死锁。

代码

public class TestDeadLock {
    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 Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(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();
                }
            }
        }
    }

    Random random = new Random();
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick {
    String name;

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

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

结果

01:48:37.984 c.Philosopher [苏格拉底] - eating...
01:48:37.984 c.Philosopher [亚里士多德] - eating...
01:48:38.493 c.Philosopher [阿基米德] - eating...
01:48:38.493 c.Philosopher [柏拉图] - eating...
01:48:39.003 c.Philosopher [柏拉图] - eating...
01:48:39.003 c.Philosopher [赫拉克利特] - eating...
01:48:39.513 c.Philosopher [苏格拉底] - eating...
01:48:39.513 c.Philosopher [赫拉克利特] - eating...

解释
我们创建一个筷子类Chopstick,一个哲学家类Philosopher,创建了五个筷子类对象 五个哲学家类对象,哲学家每次先尝试获取左边的筷子 再尝试获取右边的筷子,可以看到结果 产生了死锁。

根据jstack的结果 我们可以看出,确实发生了死锁,原因是五个哲学家都需要自己右手边哲学家的筷子

04.活锁

介绍
两个线程互相改变对方的结束条件,导致最后谁也没办法结束
例子
两个线程t1 t2 一个成员变量count=10,t1线程的结束条件是count<=0,t2线程的结束条件是count>=20,但是t1线程是在不断减少count,t2线程是在不断增加count,最终导致两个线程动达不到结束条件 导致活锁

代码

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

结果

一直运行,数值保持在0~20期间

解决方法
把两个线程指令执行的时间进行交错就行了,具体就是暂停时间改为随机

05.饥饿

介绍
多个线程中 有一个线程t,由于线程之间的冲突 导致t线程分的时间片低,导致t线程基本不运行,这种情况下t线程就处于饥饿
案例
我们前面遇见了死锁-哲学家问题,这个问题可以通过 顺序加锁的方法解决,但是这个方法会导致饥饿

代码

public class TestDeadLock {
    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 Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c1, c5).start();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(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();
                }
            }
        }
    }

    Random random = new Random();
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick {
    String name;

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

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

结果

02:32:24.639 c.Philosopher [阿基米德] - eating...
02:32:24.639 c.Philosopher [亚里士多德] - eating...
02:32:25.149 c.Philosopher [赫拉克利特] - eating...
02:32:25.658 c.Philosopher [赫拉克利特] - eating...
02:32:26.168 c.Philosopher [亚里士多德] - eating...
02:32:26.168 c.Philosopher [阿基米德] - eating...
02:32:26.678 c.Philosopher [亚里士多德] - eating...
02:32:26.678 c.Philosopher [阿基米德] - eating...
02:32:27.188 c.Philosopher [赫拉克利特] - eating...
02:32:27.697 c.Philosopher [阿基米德] - eating...
02:32:27.697 c.Philosopher [亚里士多德] - eating...
02:32:28.207 c.Philosopher [赫拉克利特] - eating...
02:32:28.717 c.Philosopher [赫拉克利特] - eating...
02:32:29.226 c.Philosopher [赫拉克利特] - eating...
02:32:29.736 c.Philosopher [阿基米德] - eating...
02:32:29.736 c.Philosopher [亚里士多德] - eating...
02:32:30.246 c.Philosopher [阿基米德] - eating...
02:32:30.246 c.Philosopher [亚里士多德] - eating...
02:32:30.756 c.Philosopher [赫拉克利特] - eating...
02:32:31.265 c.Philosopher [赫拉克利特] - eating...
02:32:31.775 c.Philosopher [亚里士多德] - eating...
02:32:31.775 c.Philosopher [阿基米德] - eating...
02:32:32.285 c.Philosopher [亚里士多德] - eating...
02:32:32.795 c.Philosopher [亚里士多德] - eating...
02:32:33.304 c.Philosopher [赫拉克利特] - eating...
02:32:33.814 c.Philosopher [亚里士多德] - eating...
02:32:34.324 c.Philosopher [赫拉克利特] - eating...
02:32:34.833 c.Philosopher [赫拉克利特] - eating...
02:32:35.343 c.Philosopher [赫拉克利特] - eating...
02:32:35.853 c.Philosopher [赫拉克利特] - eating...
02:32:36.363 c.Philosopher [赫拉克利特] - eating...
02:32:36.872 c.Philosopher [赫拉克利特] - eating...

解释
观察下面两个图


我们通过顺序加锁 解决了死锁问题,但也同时使得 阿基米德竞争加大获得两个筷子的概率大大下降 使得赫拉克利特竞争减小获得两个筷子的概率大大增强,也就是最终结果显示的那样。
阿基米德现在的这种情况就被成为饥饿。
如何解决饥饿
后续文章会讲解ReentrantLock 它可以解决。

目录
相关文章
|
3月前
|
安全 Java 程序员
【多线程-从零开始-肆】线程安全、加锁和死锁
【多线程-从零开始-肆】线程安全、加锁和死锁
69 0
|
5月前
|
安全 算法 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(下)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
93 6
|
5月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(中)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
100 5
|
5月前
|
存储 安全 Java
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)(上)
17 Java多线程(线程创建+线程状态+线程安全+死锁+线程池+Lock接口+线程安全集合)
96 3
|
5月前
|
Java
Java多线程-死锁的出现和解决
死锁是指多线程程序中,两个或以上的线程在运行时因争夺资源而造成的一种僵局。每个线程都在等待其中一个线程释放资源,但由于所有线程都被阻塞,故无法继续执行,导致程序停滞。例如,两个线程各持有一把钥匙(资源),却都需要对方的钥匙才能继续,结果双方都无法前进。这种情况常因不当使用`synchronized`关键字引起,该关键字用于同步线程对特定对象的访问,确保同一时刻只有一个线程可执行特定代码块。要避免死锁,需确保不同时满足互斥、不剥夺、请求保持及循环等待四个条件。
|
5月前
|
Java 测试技术 PHP
父子任务使用不当线程池死锁怎么解决?
在Java多线程编程中,线程池有助于提升性能与资源利用效率,但若父子任务共用同一池,则可能诱发死锁。本文通过一个具体案例剖析此问题:在一个固定大小为2的线程池中,父任务直接调用`outerTask`,而`outerTask`再次使用同一线程池异步调用`innerTask`。理论上,任务应迅速完成,但实际上却超时未完成。经由`jstack`输出的线程调用栈分析发现,线程陷入等待状态,形成“死锁”。原因是子任务需待父任务完成,而父任务则需等待子任务执行完毕以释放线程,从而相互阻塞。此问题在测试环境中不易显现,常在生产环境下高并发时爆发,重启或扩容仅能暂时缓解。
|
6月前
|
消息中间件 算法 Java
(十四)深入并发之线程、进程、纤程、协程、管程与死锁、活锁、锁饥饿详解
本文深入探讨了并发编程的关键概念和技术挑战。首先介绍了进程、线程、纤程、协程、管程等概念,强调了这些概念是如何随多核时代的到来而演变的,以满足高性能计算的需求。随后,文章详细解释了死锁、活锁与锁饥饿等问题,通过生动的例子帮助理解这些现象,并提供了预防和解决这些问题的方法。最后,通过一个具体的死锁示例代码展示了如何在实践中遇到并发问题,并提供了几种常用的工具和技术来诊断和解决这些问题。本文旨在为并发编程的实践者提供一个全面的理解框架,帮助他们在开发过程中更好地处理并发问题。
111 0
|
28天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
61 1
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
71 1
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
51 3

相关实验场景

更多