美文网首页程序员的日常记忆
多线程同步控制使用示例升级版

多线程同步控制使用示例升级版

作者: 一名程序猿 | 来源:发表于2018-08-30 20:39 被阅读190次

    需求

    一次想跑多个线程,但是需求是,某个线程第一个执行,其执行完所有操作之后,后续线程再跑,又指定某一个线程必须等待其余线程执行完毕之后,它在执行。

    模拟需求

    1.创建三种不同需求线程,以满足第一执行线程,最后执行线程,普通线程。(只有一个线程类,也是可以实现,这边为了方便打出日志,简化操作)
    2.创建程序入口,初始化各线程参数

    实现的思路

    1.利用java线程控制的wait、notifyAll用于实现某个线程第一个执行的需求。
    2.利用CountDownLatch用于实现某一个线程必须等待其余线程执行完毕之后,它在执行的需求。

    代码示例

    主程序代码:功能就是创建一个固定大小为6的线程池,用于执行所有的线程。不做任何限制的情况下,第一次会跑6个线程。一个线程运行完毕,会自动加入一个新的线程进行执行,直至所有线程执行完毕。

    package thread;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Mian {
    
        public static void main(String[] args) {
    
            ExecutorService executorService = Executors.newFixedThreadPool(6);
    
            List<Runnable> taskList = new ArrayList<Runnable>();
            CountDownLatch countDownLatch = new CountDownLatch(10);
            Object lock = new Object();
            
            taskList.add(new Common(countDownLatch, lock));
            taskList.add(new Common(countDownLatch, lock));
            taskList.add(new First(countDownLatch, lock));
            taskList.add(new Common(countDownLatch, lock));
            taskList.add(new Common(countDownLatch, lock));
            taskList.add(new Common(countDownLatch, lock));
            taskList.add(new Last(countDownLatch, lock));
            taskList.add(new Common(countDownLatch, lock));
            taskList.add(new Common(countDownLatch, lock));
            
            taskList.add(new Common(countDownLatch, lock));
            taskList.add(new Common(countDownLatch, lock));
            
            taskList.forEach(t -> {
                executorService.execute(t);
            });
            executorService.shutdown();
    
        }
    }
    
    

    第一个执行线程代码:首先打了对应的提示,为了模拟正常的运行,采用for循环的方式占用cpu,比sleep更符合实际操作场景,同时也做了个简单的记时操作,用于验证是否其他线程处于等待。计算完毕之后,countDownLatch的记数减一,最后再把阻塞在lock对象上的所有线程唤醒。注意点在于执行唤醒操作时,确保想要阻塞的线程已经全部阻塞了,否则执行了唤醒操作后,还有线程才执行阻塞操作,这类线程就无法被唤醒了。

    package thread;
    
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.concurrent.CountDownLatch;
    
    public class First implements Runnable {
    
        private CountDownLatch countDownLatch;
        private Object lock;
    
        public First(CountDownLatch countDownLatch, Object lock) {
            this.countDownLatch = countDownLatch;
            this.setLock(lock);
        }
    
        public First() {
        }
    
        @Override
        public void run() {
            System.out.println("进入第一个线程");
            System.out.println("进入第一个线程数值为:" + countDownLatch.getCount());
            System.out.println(
                    "进入第一个线程开始时间" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            for (int i = 0; i < 10000; i++) {
                for (int j = 0; j < 1000; j++) {
                    for (int k = 0; k < 1000; k++) {
                        for (int k2 = 0; k2 < 26000; k2++) {
                        }
                    }
                }
            }
            System.out.println(
                    "进入第一个线程结束时间" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            countDownLatch.countDown();
            System.out.println("结束进入第一个线程,此时线程记数值为:" + countDownLatch.getCount());
            synchronized (lock) {
                lock.notifyAll();
            }
        }
    
        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }
    
        public void setCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }
    
        public Object getLock() {
            return lock;
        }
    
        public void setLock(Object lock) {
            this.lock = lock;
        }
    
    }
    

    普通大众角色代码:纯粹是为了模拟需求需要的线程。代码功能,先获取到countDownLatch记数,如果是初始值,表示一个线程都还没有执行完毕,就阻塞线程,否则就继续执行。这儿有个注意点:要想使用wait方法,必须先上锁,并且上锁的对象与线程所在阻塞对象要一致(如下图一),否则会抛出java.lang.IllegalMonitorStateException异常。


    图一.png
    package thread;
    
    import java.util.concurrent.CountDownLatch;
    
    public class Common implements Runnable {
    
        private CountDownLatch countDownLatch;
    
        private Object lock;
    
        public Common(CountDownLatch countDownLatch, Object lock) {
            this.setCountDownLatch(countDownLatch);
            this.setLock(lock);
        }
    
        public Common(CountDownLatch countDownLatch) {
            this.setCountDownLatch(countDownLatch);
        }
    
        public Common() {
        }
    
        @Override
        public void run() {
            synchronized (lock) {
                long num = countDownLatch.getCount();
                if (num == 10) {
                    try {
                        System.out.println("普通线程进入阻塞");
                        lock.wait();
                        System.out.println("阻塞的线程被唤醒");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            synchronized (lock) {
                System.out.println("进入普通线程了");
                countDownLatch.countDown();
                System.out.println("目前的线程记数值为:" + countDownLatch.getCount());
            }
        }
    
        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }
    
        public void setCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }
    
        public Object getLock() {
            return lock;
        }
    
        public void setLock(Object lock) {
            this.lock = lock;
        }
    
    }
    
    

    最后一个执行线程:先获取countDownLatch记数,如果是第一个线程就阻塞,否则就往下执行;执行countDownLatch.await();输出相关信息

    package thread;
    
    import java.util.concurrent.CountDownLatch;
    
    public class Last implements Runnable {
    
        private CountDownLatch countDownLatch;
        private Object lock;
    
        public Last(CountDownLatch countDownLatch, Object lock) {
            this.countDownLatch = countDownLatch;
            this.setLock(lock);
        }
    
        public Last() {
        }
    
        @Override
        public void run() {
            synchronized (lock) {
                long num = countDownLatch.getCount();
                if (num == 10) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
            synchronized (countDownLatch) {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("进入最后一个线程,此时线程记数值为:" + countDownLatch.getCount());
            System.out.println("结束最后一个线程");
        }
    
        public CountDownLatch getCountDownLatch() {
            return countDownLatch;
        }
    
        public void setCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }
    
        public Object getLock() {
            return lock;
        }
    
        public void setLock(Object lock) {
            this.lock = lock;
        }
    
    }
    
    
    代码运行结果
    图二.png
    这个结果看起很漂亮,但是实际上这个不太符合业务场景。尤其是大众代码,正常情况下应该是并发运行。看上图代码发现: w.png

    在阻塞唤醒之后,马上又进入锁代码,可以想象基本是单线程运行了。我之所以加锁,是因为countDownLatch.getCount()是不加锁的,我不加锁获取这个值就会是乱的,因为一个线程执行了countDown,还没有执行getCount。另一个线程可能又执行了countDown,导致获取到的值是不连续了。正常场景下,各线程执行本身互不影响,更多的是并发操作,提高效率。

    效率

    针对最开始的需求,我要是把线程池固定大小设置为1,第一个执行线程放在数组第一个,最后一个线程放最后一个,感觉还是可以实现需求,只不过是全程单线程执行任务。搞这么麻烦就是为了提升效率。所以同一个需求会有很多种实现,就是效率各不相同。

    效率的验证

    1.增加整个程序运行完毕时间。(这个不是说在主程序里面代码块前后加个输出时间就ok?因为线程的运行,不影响主线程,所以直接加肯定不对。正确的做法,进入主程序加一个时间为开始时间,最后一个线程加一个时间为结束时间)
    2.增加大众线程运算时间。(直接一个输出,时间差基本可以忽略)

    验证代码贴图
    主程序加计时.png
    增加大众线程运算时间.png
    最后一个线程加计时.png
    运行结果.png

    从我实时看输出,也确如直接看代码分析一样,说是多线程实际还是单线程运行,因为基本属于全程加锁。也可以看到整个运行时间是52秒。输出效果看起还是整齐。

    去掉大众代码中的锁,因为大众代码各自运行是互不影响的。(countDownLatch.countDown()本身自带锁)
    改版大众代码.png
    改版结果图.png

    可以看到时间是8秒。效率应提升接近7倍。

    懵逼???

    从这个输出来看感觉有实现上面的需求吗?为啥最后一个线程不是最后输出呢?(线程记数值为啥不对,已经在上面‘为啥加锁’中说明了)

    解惑
    再瞧大众代码.png

    先执行的是countDownLatch.countDown();然后执行计算操作,最后执行输出操作。大家也知道,唤醒最后一个线程的条件是线程记数等于0就可以唤醒了。这么写的确是有问题。所以一定要注意,countDownLatch.countDown()操作一定是在线程所有要做的事情做完再执行。否则就不是某一个线程必须等待前面线程执行完毕后执行。所以效率的统计也是有点问题,改哈大众代码,再看一遍


    image.png
    正确下的结果.png

    改了一哈控制台字体,不然放不下。这下就可以看到最后一个线程是最后输出的(不是偶然,无论多少次都是最后输出,唯一会变化的是线程记数)


    再来一发.png
    记数没变就没变吧。最后一个线程还是最后输出了。两次时间都是7秒。效率提升是毋庸置疑的。
    到此就算完成功能演示。
    死锁

    还是咱们的大众代码,改一哈如下图


    改大众代码锁对象.png

    刚刚已经说了这儿除非各线程互有影响或者其他什么原因,理论上是不应该加锁。
    刚开始我加的锁对象是lock。改为countDownLatch,再次运行代码就会发现问题。运行结果如图:


    死锁结果.png
    不要以为我是中途代码运行时截图,实际无论你等多久程序都不会出结果,一直在等待。因为已经产生死锁。我这边就不弄工具去监测死锁了。(实际这个目前我还真不会。。。)
    产生死锁的原因
    这个是因为 最后一个线程代码.png

    锁的对象是countDownLatch
    普通大众代码锁对象依然是countDownLatch。但是countDownLatch.await();本身自带锁,进入阻塞之后不会释放countDownLatch锁对象。而普通大众代码又因为获取不到countDownLatch锁对象,所以进入不了countDownLatch.countDown();那么就导致普通对象无法使线程减一,最后一个线程也无法执行。所剩下的线程都无法继续执行。造成死锁。而我一开始锁的lock对象就没事。
    造成死锁的原因就是滥用锁。

    从刚刚分析来看,大众代码 image.png 不需要锁。
    最后一个线程 image.png

    countDownLatch.await();自带锁也不需要加锁。两个地方都不加锁,自然就不会出现死锁了。

    死锁的延伸

    刚刚产生死锁的结论是countDownLatch.await();本身自带锁,进入阻塞之后不会释放countDownLatch锁对象。这个结论还需要验证。

    验证多重加锁,最里面的锁对象进入阻塞,是否是释放外层锁对象。

    主验证代码

    package thread;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Mian2 {
    
        public static void main(String[] args) {
    
            ExecutorService executorService = Executors.newFixedThreadPool(6);
            List<Runnable> taskList = new ArrayList<Runnable>();
            Object lock = new Object();
            Object lock2 = new Object();
    
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.add(new CommonTest(lock2, lock));
            taskList.forEach(t -> {
                executorService.execute(t);
            });
            executorService.shutdown();
        }
    }
    

    验证线程代码

    package thread;
    
    public class CommonTest implements Runnable {
    
        private Object lock2;
    
        private Object lock;
    
        public CommonTest(Object lock2, Object lock) {
            this.setLock2(lock2);
            this.setLock(lock);
        }
    
        public CommonTest() {
        }
    
        @Override
        public void run() {
            System.out.println("进入线程");
            synchronized (lock) {
                System.out.println("进入一重锁");
                synchronized (lock2) {
                    try {
                        System.out.println("进入二重锁");
                        lock2.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }
    
        public Object getLock() {
            return lock;
        }
    
        public void setLock(Object lock) {
            this.lock = lock;
        }
    
        public Object getLock2() {
            return lock2;
        }
    
        public void setLock2(Object lock2) {
            this.lock2 = lock2;
        }
    
    }
    

    输出结果


    验证结果.png

    可以看到其他线程压根就进入不到一重锁,证明了多重锁的情况下,内部阻塞只会释放第一层锁。
    我们去掉一层看输出:


    去掉一层锁.png
    结果.png
    所有线程都获取到了一重锁。也证明了上述结论的正确性。
    结语

    并发操作本身就比较复杂,当时发现死锁,我也是想了许久才发现多重锁的问题。最后,本文如有不正确之处,请评论指出。

    相关文章

      网友评论

        本文标题:多线程同步控制使用示例升级版

        本文链接:https://www.haomeiwen.com/subject/bnfbiftx.html