【Java】设计模式之顺序控制

2024-01-08 13:49:42

实际开发中,有时候一些场景需求让多个线程按照固定的顺序依次执行。这个时候就会使用这种模式。

这种模式说白了,就是给线程设定不同的条件,不符合条件的话,就算线程拿到锁也会释放锁进入等待;符合条件才让线程拿到锁能够执行代码,完毕之后再唤醒所有等待中的线程。

可以用wait/notify进行解决。让每个线程需要满足一定条件(顺序)才能执行,否则放开锁进入等待。

比如,两个线程交替打印奇偶数。

public static void main(String[] args) throws InterruptedException {
    //两个线程交替打印奇偶数
    int[] num={0};
    new Thread(()->{
        synchronized (num){
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //打印偶数
                if(num[0]%2==0){
                    logger.debug("{}",num[0]);
                    num[0]++;
                }else{
                    try {
                        num.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                num.notify();
            }
        }
    }).start();
    new Thread(()->{
        synchronized (num){
            while (true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //打印奇数
                if(num[0]%2!=0){
                    logger.debug("{}",num[0]);
                    num[0]++;
                }else{
                    try {
                        num.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                num.notify();
            }
        }
    }).start();
}

如果有多个线程进行顺序打印,使用ReentrantLock的条件变量、await、sigal会能比synchronized获得更好的性能,因为synchronized每次都唤醒所有的线程。比如说有n个线程,让n个线程循环依次打印n个数字,这时候可以让ReentrantLock创建n个条件变量,每个线程对应一个条件变量。当线程t发现还没轮到自己打印(不满足条件),就让线程t进入t-1条件变量进行等待;当线程t打印数字成功,就可以让条件变量t唤醒t+1的线程。

比如,让n条线程循环打印0-n的数字:

public static void main(String[] args) throws InterruptedException {
    //让n条线程循环依次打印0-n的数字
    int n=7;
    int[] num={0};
    ReentrantLock lock=new ReentrantLock();
    Condition[] conditions=new Condition[n];
    for (int i = 0; i < n; i++) {
        conditions[i]=lock.newCondition();
    }
    for (int i = 0; i < n; i++) {
        int finalI=i;
        new Thread(()->{
            lock.lock();
            try{
                while (true){
                    if(num[0]==finalI){
                        Thread.sleep(100);
                        //轮到自己打印了
                        logger.debug("{}",num[0]);
                        num[0]++;
                        if(num[0]==n){
                            num[0]=0;
                        }
                        //打印完成,唤醒在当前条件变量中等待的线程(第i+1条线程)
                        conditions[finalI].signal();
                    }else{
                        //没有轮到自己打印,释放锁,进入i-1的条件变量进行等待
                        if(finalI==0){
                            conditions[n-1].await();
                        }else {
                            conditions[finalI-1].await();
                        }
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        },"线程"+i).start();
    }
}

结果:

在这里插入图片描述

分析:假设要让1000条线程循环依次打印从0到1000的数字

  • 使用ReentrantLock
    • 最坏的情况下,最开始线程0抢到了锁,打印出0。第0、3-999条线程在第2条线程之前竞争到锁但是没法打印进入等待,第2条线程最后打印出2并唤醒在条件变量2中等待的线程3,此时只有线程2和线程3竞争锁,但是线程2会因为无法打印进入线程1的条件变量中等待。依此类推,每次都只有两条线程竞争锁
  • 使用synchronized
    • 最坏的情况下,最开始线程0抢到了锁,打印出0。第0、3-999条线程在第2条线程之前竞争到锁但是没法打印进入等待,第2条线程最后打印出2并唤醒其他999条线程。线程3在最坏的情况下依然是最后一个竞争到锁,然后再次唤醒999条线程……。每次都有1000条线程参与竞争锁

以上的分析可以很明显看出来,ReentrantLock的条件变量对于提升性能是有巨大的优势的。当线程数量越多,性能差距越明显。

另外也可以用park/unpark实现,因为这两个方法也能让线程阻塞、唤醒线程。park和unpark没有对象锁的概念,也没有什么等待队列,它以线程为单位阻塞和唤醒线程。所以,想要按顺序执行,一开始让所有线程都停下来,接下来手动触发一个线程运行,并让这个线程只运行一次就结束或阻塞,并触发下一个线程运行,以此类推就能按顺序进行线程的执行。

park/unpark是最简洁的。

public class Main {
    static Logger logger = LoggerFactory.getLogger("main");
    static Thread t1;
    static Thread t2;
    static Thread t3;
    public static void main(String[] args) throws InterruptedException {
        //让线程1、2、3依次顺序打印a、b、c五次
        int loop=5;

        t1 = new Thread(() -> {
            for (int i = 0; i < loop; i++) {
                LockSupport.park();
                logger.debug("a");
                LockSupport.unpark(t2);
            }
        });
        t2 = new Thread(() -> {
            for (int i = 0; i < loop; i++) {
                LockSupport.park();
                logger.debug("b");
                LockSupport.unpark(t3);
            }
        });
        t3 = new Thread(() -> {
            for (int i = 0; i < loop; i++) {
                LockSupport.park();
                logger.debug("c");
                LockSupport.unpark(t1);
            }
        });
        t1.start();
        t2.start();
        t3.start();
        LockSupport.unpark(t1);
    }

}

文章来源:https://blog.csdn.net/weixin_52808620/article/details/135440437
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。