Java禅与计算机程序设计艺术编程语言爱好者

场景模拟线程间协作

2023-01-12  本文已影响0人  pq217

前言

多线程编程时,为完成某个目的,线程和线程之间往往要进行一些沟通、协调来掌控各步骤的处理顺序,比如在处理异步任务回调时

针对线程之间的协调作业,有很多方式可以实现,本文就通过一些场景整体模拟下常用的线程协作方法,有遗漏再不断补充

与前文结合jvisualvm一次性看清线程状态内容大部分一致,本文重点关注方法使用而非线程状态,补充了一些方法

sleep

先来最简单常用的sleep方法,顾名思义,就是让线程睡一会并指定醒来时间,并且释放cpu资源

我们用sleep模拟一个孩子睡觉的过程

public class SleepTest {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "孩子");
        thread.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("线程的状态:" + thread.getState());
    }
}

此时名为“孩子”的线程正在睡觉,对应的线程状态是TIMED_WAITING

使用sleep需要处理一个checked异常InterruptedException,这个接下来说

interrupt

interrupt字面意思上讲就是打断当前的干的事,只是修改了线程的一个中断标识(一个字段),换句话说,如果被打断的线程不理会,打断等于无效

那么interrupt有什么用呐?大部分情况是结合isInterrupted()或interrupted()使用,二者可以查看线程是否被打断,也就是说interrupt相当于发送一个打断信号给某线程,该线程通过isInterrupted可以获取打断信号,至于收到打断信号后如何处理线程己决定

我们用代码模拟一个家长喊在外面玩耍的孩子吃饭的故事

public class IsInterruptedTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("孩子正在玩...");
            // 时刻关注自己是否被打断
            while (!Thread.currentThread().isInterrupted()) {}
            System.out.println("孩子回家吃饭");
        }, "son");
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        System.out.println("孩子的状态:" + thread.getState());
        System.out.println("家长喊孩子回家吃饭");
        thread.interrupt();
    }
}

输出:

孩子正在玩...
孩子的状态:RUNNABLE
家长喊孩子回家吃饭
孩子回家吃饭

在这里,主子线程完成了一个简单的协调

当然son线程也可以忽略interrupt信号继续的玩下去

以上代码使用的是isInterrupted()方法,同样也可以使用interrupted()方法,二者的区别是后者执行完后会擦拭掉中断标识

上面讲到的sleep()方法默认响应打断,而响应的方式就是抛出InterruptedException异常,这就是为什么我们使用sleep()一定要处理InterruptedException异常

我们用代码模拟一个家长叫睡觉的孩子起床的故事

public class InterruptedExceptionTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("孩子在睡觉...");
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                System.out.println("孩子起床吃饭: " + Thread.currentThread().isInterrupted());
            }
        }, "son");
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        System.out.println("家长喊孩子起床吃饭");
        thread.interrupt();
        while (true) {}
    }
}

输出

孩子在睡觉...
家长喊孩子起床吃饭
孩子起床吃饭: false

最后一条出现false,说明sleep也会先擦除打断信息再抛出异常

join

join也是一种常用的线程协调方式,通过join某线程可以等待另一个线程执行结束

模拟一个家长等孩子起床一起吃饭的过程

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("孩子正睡觉...");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("孩子洗脸刷牙...");
            int i = 0;
            while (++i>0) {
                new Object(); // 模拟刷牙过程
            }
            System.out.println("孩子起来了");
        }, "son");
        thread.start();
        System.out.println("家长等孩子醒...");
        thread.join();
        System.out.println("家长孩子一起开始吃早饭");
    }
}

输出

家长等孩子醒...
孩子正睡觉...
孩子洗脸刷牙...
孩子起来了
家长孩子一起开始早饭

join也响应interrupte,和sleep一样抛出异常InterruptedException

synchronized

这个作用就不多说了,模拟一下家长孩子早上抢着上厕所的场景

public class SyncTest {
    public static void main(String[] args) throws InterruptedException {
        Thread son = new Thread(() -> {
            doRun();
        }, "孩子");
        son.start();
        // 家长比孩子慢一秒开始抢厕所
        Thread father = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            doRun();
        }, "家长");
        father.start();
    }

    public synchronized static void doRun() {
        System.out.println(Thread.currentThread().getName() + "开始上厕所");
        // 模拟蹲坑
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "上完");
    }
}

输出

孩子开始上厕所
孩子上完
家长开始上厕所
家长上完

synchronized是不响应interrupte的

ReentrantLock

接下来看同样是锁工具的ReentrantLock,修改上面代码使用ReentrantLock

public class LockTest {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();
        Thread son = new Thread(() -> {
            lock.lock(); // 拿到锁不释放
            doRun();
            lock.unlock();
        }, "孩子");
        son.start();
        Thread father = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            doRun();
            lock.unlock();
        }, "家长");
        father.start();
    }

    public static void doRun() {
        System.out.println(Thread.currentThread().getName() + "开始上厕所");
        // 模拟蹲坑
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "上完");
    }
}

输出

孩子开始上厕所
孩子上完
家长开始上厕所
家长上完

ReentrantLock的lock方法也不响应interrupt,但ReentrantLock很灵活,如果希望响应可以使用lockInterruptibly()方法

实际上ReentrantLock内部是使用Unsafe.park方法让线程进行等待的

Unsafe.park

这是一种让线程等待的方法,并需要其他线程通过unpark唤醒

依然模拟家长喊休息的孩子起来吃饭场景

public class ParkTest {
    public static void main(String[] args) throws Exception {
        // 获得unsafe
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        // 开始
        Thread thread = new Thread(() -> {
            System.out.println("孩子正在休息");
            unsafe.park(false, 0);
            System.out.println("孩子被唤醒: " + Thread.currentThread().isInterrupted());
        }, "son");
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        System.out.println("孩子的状态:" + thread.getState());
        System.out.println("家长喊孩子起来吃饭");
//        thread.interrupt();
        unsafe.unpark(thread);
    }
}

输出

孩子正在休息
孩子的状态:WAITING
家长喊孩子起来吃饭
孩子被唤醒: false

unsafe.unpark(thread)改成thread.interrupt()试试打断,输出

孩子正在休息
孩子的状态:WAITING
家长喊孩子起来吃饭
孩子被唤醒: true

可以看到unsafe.park响应interrupt,效果和unpark一样,且不清除打断标记

但问题来了为啥使用park的lock不响应interrupt?原因是lock的内部做了逻辑处理,被interrupt后继续调用unsafe.park等待

wait&notify

顾名思义,wait就是等待,notify就是通知,二者天生是配合使用的

wait和sleep、join都不一样,后二者是属于Thread对象的方法,而wait以及notify是任何对象都有的方法(即Object的方法)

wait 方法是释放对象锁并让当前线程等待在该对象上,直到另一个线程调用了该对象的 notify 或 notifyAll 方法之后,才能继续恢复执行

wait&notify必须先获得对象锁才能调用(必须在synchronized下),也就是说只有获得对象的锁才有资格在对象上等待,也只有获得锁才有资格通知在对象上等待的线程

下面用代码模拟一个孩子等家长做蛋堡的场景

public class WaitTest {
    public static Object o = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            synchronized (o) {
                System.out.println("孩子正等待...");
                try {
                    o.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("孩子开始吃蛋堡");
            }
        }, "son");
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        synchronized (o) {
            System.out.println("家长做蛋堡...");
            TimeUnit.SECONDS.sleep(3);
            System.out.println("孩子的状态:" + thread.getState());
            System.out.println("家长做完喊孩子吃");
            o.notifyAll();
//            System.out.println("收拾桌子");
//            TimeUnit.SECONDS.sleep(4);
//            System.out.println("孩子的状态:" + thread.getState());
        }
        while (true) {}
    }
}

输出

孩子正等待...
家长做蛋堡...
孩子的状态:WAITING
家长做完喊孩子吃
孩子开始吃蛋堡

以上代码o是一个空对象用来实现锁,它好比一个盘子,孩子和家长的一个约定做完蛋堡就放入盘子,首先孩子获得盘子,调用wait在盘子旁边等待,并交出盘子的控制权(调用wait会释放锁,收到notify时再重新尝试获得锁并继续执行代码)

这里有个小问题,wait&notify必须在synchronized下执行,wait时会释放锁,那么执行notifyl也会立即释放锁吗,做个测试把上面代码的注释打开

synchronized (o) {
    System.out.println("家长做蛋堡...");
    TimeUnit.SECONDS.sleep(3);
    System.out.println("孩子的状态:" + thread.getState());
    System.out.println("家长做完喊孩子吃");
    o.notifyAll();
    System.out.println("收拾厨房");
    TimeUnit.SECONDS.sleep(3);
    System.out.println("孩子的状态:" + thread.getState());
}

输出

孩子正等待...
家长做蛋堡...
孩子的状态:WAITING
家长做完喊孩子吃
收拾厨房
孩子的状态:BLOCKED
孩子开始吃蛋堡

可以看到,家长做完喊孩子吃(调用notifyAll)后,孩子并没有马上开始吃,而是收拾家长完厨房后才开始吃,但孩子的状态由WAITING变为BLOCKED,证明收到notify消息是,孩子就已经不再wait了,而是尝试获取锁,也就是等在synchronized外,而最终家长的synchronized代码执行完毕才实际释放锁,孩子开始吃蛋堡

wait同样响应interrupte,响应方式和join,sleep一样是抛出InterruptedException异常

Condition

wait&notify是在synchronized下使用的,而作更灵活的锁ReentrantLock却无法使用,所以在ReentrantLock下就有一个wait&notify的替代品即:Condition

同样,我们使用Condition模拟wait&notify一样的场景:一个孩子等家长做蛋堡的场景

public class ConditionTest {
    // 模拟一个盘子
    private static final Lock lock = new ReentrantLock();
    // 食物准备好了放入盘子的条件
    private static final Condition foodReadycondition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread son = new Thread(() -> {
            // 获取盘子控制权
            lock.lock();
            System.out.println("孩子正等待...");
            try {
                // 等待食物就绪
                foodReadycondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("孩子开始吃蛋堡");
            // 释放盘子
            lock.unlock();
        }, "son");
        son.start();
        TimeUnit.SECONDS.sleep(1);
        // 获取盘子控制权
        lock.lock();
        System.out.println("家长做蛋堡...");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("孩子的状态:" + son.getState());
        System.out.println("家长做完喊孩子吃");
        // 发出盘子准备好食物的信号
        foodReadycondition.signal();
        // 交出盘子控制权
        lock.unlock();
        while (true) {}
    }
}

可以看到,与wait&notify的使用基本一致,相当于ReentrantLock下的wait&notify(await&signal)

和ReentrantLock对比synchronized一样,await&signal比wait&notify功能更强更灵活,也体现了作者Doug Lea的强大

上一篇下一篇

猜你喜欢

热点阅读