多线程学习Day04

本文详细介绍了Java中的Park和Unpark方法,以及它们与wait/notify的区别,讨论了线程阻塞和唤醒的精确控制。此外,文章还涵盖了死锁、活锁和饥饿的概念,通过实例分析了并发编程中的这些问题和解决方案。
摘要由CSDN通过智能技术生成

摆烂了两天重新启程

Park和Unpark

与 Object 的 wait & notify 相比wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必

park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
park & unpark 可以先 unpark,而 wait & notify 不能先 notify,小例子如下

@Slf4j(topic = "c.TestUnpark")
public class TestUnpark {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            log.debug("start...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        },"t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

Park和Unpark的原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex

打个比喻 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)

调用 park 就是要看需不需要停下来歇息

    如果备用干粮耗尽,那么钻进帐篷歇息

    如果备用干粮充足,那么不需停留,继续前进

调用 unpark,就好比令干粮充足

     如果这时线程还在帐篷,就唤醒让他继续前进

     如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

      

1. 当前线程调用 Unsafe.park() 方法

2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁

3. 线程进入 _cond 条件变量阻塞

4. 设置 _counter = 0

    

1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

2. 唤醒 _cond 条件变量中的 Thread_0

3. Thread_0 恢复运行

4. 设置 _counter 为 0

     

1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

2. 当前线程调用 Unsafe.park() 方法

3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行

4. 设置 _counter 为 0

线程状态转换(加星)

1.NEW->RUNNABLE

调用t.start()方法

2.RUNNABLE<-->WAITING

t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
     竞争锁成功,t 线程从 WAITING --> RUNNABLE
     竞争锁失败,t 线程从 WAITING --> BLOCKED

3.RUNNABLE <--> WAITING
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
     注意是当前线程在t 线程对象的监视器上等待
     t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

4.RUNNABLE <--> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->
RUNNABLE

5.RUNNABLE <--> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后
      调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
      t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
         竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
         竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

6.RUNNABLE <--> TIMED_WAITING

当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
     注意是当前线程在t 线程对象的监视器上等待
当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE

7.RUNNABLE <--> TIMED_WAITING

当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

8.RUNNABLE <--> TIMED_WAITING

当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE

9.RUNNABLE <--> BLOCKED

t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

10.RUNNABLE <--> TERMINATED

当前线程所有代码运行完毕,进入TERMINATED


多把锁

一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法是准备多个房间(多个对象锁),下面是例子,可以看到二者互不干扰

@Slf4j(topic = "c.Test18")
public class Test18 {
    public static void main(String[] args) {
        BigRoom bigRoom=new BigRoom();
        new Thread(()->{
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"小南").start();
        new Thread(()->{
            try {
                bigRoom.sleep();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"小女").start();
    }
}
@Slf4j(topic = "c.BigRoom")
class BigRoom{
    private final Object studyRoom=new Object();
    private final Object bedRoom=new Object();

    public void sleep() throws InterruptedException {
        synchronized (bedRoom){
            log.debug("睡觉2小时");
            Thread.sleep(2000);
        }
    }
    public void study() throws InterruptedException {
        synchronized (studyRoom){
            log.debug("学习1小时");
            Thread.sleep(1000);
        }
    }
}
21:27:04 [小女] c.BigRoom - 睡觉2小时
21:27:04 [小南] c.BigRoom - 学习1小时

将锁的粒度细分
好处,是可以增强并发度
坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

线程的活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得 A对象 锁,接下来想获取 B对象的锁

t2 线程 获得 B对象 锁,接下来想获取 A对象的锁  例:

@Slf4j(topic = "c.Test19")
public class Test19 {
    public static void main(String[] args) {
        test1();
    }
    public static void test1(){
        Object A=new Object();
        Object B=new Object();
        Thread t1=new Thread(()->{
            synchronized (A){
                log.debug("lock A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B){
                    log.debug("lock B");
                    log.debug("操作");
                }
            }

        },"t1");
        Thread t2=new Thread(()->{
            synchronized (B){
                log.debug("lock B");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A){
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        },"t2");
        t1.start();
        t2.start();
    }

}
定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

避免死锁要注意加锁顺序
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

哲学家就餐问题

有五位哲学家,围坐在圆桌旁。
     他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
     吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
     如果筷子被身边的人拿着,自己就得等待

例子:

@Slf4j(topic = "c.PhilosophersEat")
public class PhilosophersEat {
    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();
    }

}
class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
@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;
    }
    private void eat() throws InterruptedException {
        log.debug("eating...");
        Thread.sleep(1000);
    }
    @Override
    public void run() {
        while (true) {
// 获得左手筷子
            synchronized (left) {
// 获得右手筷子
                synchronized (right) {
// 吃饭
                    try {
                        eat();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
// 放下右手筷子
            }
// 放下左手筷子
        }
    }
}

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如,一个有点意思的事情,就是这两个线程的睡眠时间如果不一样就不会发生活锁

@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) {
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
// 期望超过 20 退出循环
            while (count < 20) {
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

饥饿

先来看看使用顺序加锁的方式解决之前的死锁问题

顺序加锁的解决方案,都按先获取A再获取B的顺序去获取锁,不过这就可能出现饥饿的问题

这个方法也可以简单解决一下刚才的哲学家就餐问题

@Slf4j(topic = "c.PhilosophersEat")
public class PhilosophersEat {
    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();
        new Philosopher("阿基米德", c1, c5).start();
    }

}
class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
@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;
    }
    private void eat() throws InterruptedException {
        log.debug("eating...");
        Thread.sleep(1000);
    }
    @Override
    public void run() {
        while (true) {
// 获得左手筷子
            synchronized (left) {
// 获得右手筷子
                synchronized (right) {
// 吃饭
                    try {
                        eat();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
// 放下右手筷子
            }
// 放下左手筷子
        }
    }
}

按顺序拿他就不会死锁,但是结果还是有问题的,看这里

21:59:27 [亚里士多德] c.Philosopher - eating...
21:59:28 [亚里士多德] c.Philosopher - eating...
21:59:29 [赫拉克利特] c.Philosopher - eating...
21:59:30 [亚里士多德] c.Philosopher - eating...
21:59:31 [赫拉克利特] c.Philosopher - eating...
21:59:32 [赫拉克利特] c.Philosopher - eating...
21:59:33 [赫拉克利特] c.Philosopher - eating...
21:59:34 [赫拉克利特] c.Philosopher - eating...
21:59:35 [赫拉克利特] c.Philosopher - eating...
21:59:37 [赫拉克利特] c.Philosopher - eating...
21:59:38 [赫拉克利特] c.Philosopher - eating...
21:59:39 [赫拉克利特] c.Philosopher - eating...
21:59:40 [赫拉克利特] c.Philosopher - eating...
21:59:41 [赫拉克利特] c.Philosopher - eating...
21:59:42 [赫拉克利特] c.Philosopher - eating...

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值