前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >多线程同步控制使用示例升级版

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

作者头像
用户5166330
发布2019-04-16 15:12:17
2670
发布2019-04-16 15:12:17
举报
文章被收录于专栏:帅哥哥写代码帅哥哥写代码
需求

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

模拟需求

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

实现的思路

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

代码示例

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

代码语言:javascript
复制
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对象上的所有线程唤醒。注意点在于执行唤醒操作时,确保想要阻塞的线程已经全部阻塞了,否则执行了唤醒操作后,还有线程才执行阻塞操作,这类线程就无法被唤醒了。

代码语言:javascript
复制
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

代码语言:javascript
复制
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();输出相关信息

代码语言:javascript
复制
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锁对象。这个结论还需要验证。 验证多重加锁,最里面的锁对象进入阻塞,是否是释放外层锁对象。 主验证代码

代码语言:javascript
复制
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();
    }
}

验证线程代码

代码语言:javascript
复制
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 所有线程都获取到了一重锁。也证明了上述结论的正确性。

结语

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.08.30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 需求
  • 模拟需求
  • 实现的思路
  • 代码示例
    • 代码运行结果
      • 效率
        • 效率的验证
          • 验证代码贴图
            • 死锁
              • 死锁的延伸
                • 结语
                相关产品与服务
                腾讯云代码分析
                腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档