先上结论

原理

  • join 原理:在当前线程中调用另一个线程线程 thread 的 join() 方法时,会调用该 thread 的 wait() 方法,直到这个 thread 执行完毕(JVM在 run() 方法执行完后调用 exit() 方法,而 exit() 方法里调用了 notifyAll() 方法)会调用 notifyAll() 方法主动唤醒当前线程。

源码如下:

    public final void join() throws InterruptedException {
join(0);
} /**
* 注意这个方法是同步的
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0; if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
} /**
* join方法默认参数为0,会直接阻塞当前线程
*/
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
} public final native boolean isAlive();
}
  • countDownLatch 原理:可以理解为一个计数器。在初始化 CountDownLatch 的时候会在类的内部初始化一个int的变量,每当调用 countDownt() 方法的时候这个变量的值减1,而 await() 方法就是去判断这个变量的值是否为0,是则表示所有的操作都已经完成,否则继续等待。

源码如下(源码比较少,直接全贴出来了,所有中文注释是我自己加上去的):

public static class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L; /**
* 初始化state
*/
Sync(int count) {
setState(count);
} int getCount() {
return getState();
} /**
* 尝试获取同步状态
* 只有当同步状态为0的时候返回1
*/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
} /**
* 自旋+CAS的方式释放同步状态
*/
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
} private final Sync sync; /**
* 初始化一个同步器
*/
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
} /**
* 调用同步器的acquireSharedInterruptibly方法,并且是响应中断的
*/
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
} /**
* 调用同步器的releaseShared方法去让state减1
*/
public void countDown() {
sync.releaseShared(1);
} /**
* 获取剩余的count
*/
public long getCount() {
return sync.getCount();
} public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}

区别及注意事项

  • join和countDownLatch都能实现让当前线程阻塞等待其他线程执行完毕,join使用起来更简便,不过countDownLatch粒度更细。
  • 由于CountDownLatch需要开发人员很明确需要等待的条件,否则容易造成await()方法一直阻塞。

如何使用

  • 一个简单的小例子
public class Test {
private static final Logger logger = LoggerFactory.getLogger(Test.class); public static void main(String[] args) {
long sleepTime = 5000;
try {
TestJoinThread joinThread1 = new TestJoinThread("joinThread1",sleepTime);
TestJoinThread joinThrad2 = new TestJoinThread("joinThrad2",sleepTime);
joinThread1.start();
joinThrad2.start();
joinThread1.join();
joinThrad2.join();
logger.info("主线程开始运行...");
} catch (InterruptedException e) {
logger.error("test join err!",e);
} try {
CountDownLatch count = new CountDownLatch(2);
TestCountDownLatchThread countDownLatchThread1 = new TestCountDownLatchThread(count,"countDownLatchThread1",sleepTime);
TestCountDownLatchThread countDownLatchThread2 = new TestCountDownLatchThread(count,"countDownLatchThread2",sleepTime);
countDownLatchThread1.start();
countDownLatchThread2.start();
count.await();
logger.info("主线程开始运行...");
} catch (InterruptedException e) {
logger.error("test countDownLatch err!",e);
}
} static class TestJoinThread extends Thread{ private String threadName;
private long sleepTime; public TestJoinThread(String threadName,long sleepTime){
this.threadName = threadName;
this.sleepTime = sleepTime;
} @Override
public void run() {
try{
logger.info(String.format("线程[%s]开始运行...",threadName));
Thread.sleep(sleepTime);
logger.info(String.format("线程[%s]运行结束 耗时[%s]s",threadName,sleepTime/1000));
}catch (Exception e){
logger.error("TestJoinThread run err!",e);
}
}
} static class TestCountDownLatchThread extends Thread{ private String threadName;
private long sleepTime;
private CountDownLatch countDownLatch; public TestCountDownLatchThread(CountDownLatch countDownLatch,String threadName,long sleepTime){
this.countDownLatch = countDownLatch;
this.threadName = threadName;
this.sleepTime = sleepTime;
} @Override
public void run() {
try{
logger.info(String.format("线程[%s]开始运行...",threadName));
Thread.sleep(sleepTime);
logger.info(String.format("线程[%s]运行结束 耗时[%s]s",threadName,sleepTime/1000));
countDownLatch.countDown();
}catch (Exception e){
logger.error("TestCountDownLatchThread run err!",e);
}
}
}
}

日志输出:

11:18:01.985 [Thread-1] INFO com.sync.Test - 线程[joinThrad2]开始运行...
11:18:01.985 [Thread-0] INFO com.sync.Test - 线程[joinThread1]开始运行...
11:18:06.993 [Thread-1] INFO com.sync.Test - 线程[joinThrad2]运行结束...耗时[5]s
11:18:06.993 [Thread-0] INFO com.sync.Test - 线程[joinThread1]运行结束...耗时[5]s
11:18:06.993 [main] INFO com.sync.Test - 主线程开始运行...
11:18:06.995 [Thread-2] INFO com.sync.Test - 线程[countDownLatchThread1]开始运行...
11:18:06.995 [Thread-3] INFO com.sync.Test - 线程[countDownLatchThread2]开始运行...
11:18:11.996 [Thread-2] INFO com.sync.Test - 线程[countDownLatchThread1]运行结束...耗时[5]s
11:18:11.996 [Thread-3] INFO com.sync.Test - 线程[countDownLatchThread2]运行结束...耗时[5]s
11:18:11.996 [main] INFO com.sync.Test - 主线程开始运行...

可以看到:joinThread1 和 joinThread2 同时开始执行,5s后主线程开始执行。countDownLatchThread1 和 countDownLatchThread2 也是一样的效果。

那么我上面所说的粒度更细有怎样的应用场景呢?

我对 TestCountDownLatchThread类 的 run() 方法做一点小改动:

@Override
public void run() {
try{
logger.info(String.format("线程[%s]第一阶段开始运行...",threadName);
Thread.sleep(sleepTime);
logger.info(String.format("线程[%s]第一阶段运行结束耗时[%s]s",threadName,sleepTime/1000));
countDownLatch.countDown();
logger.info(String.format("线程[%s]第二阶段开始运行...",threadName);
Thread.sleep(sleepTime);
logger.info(String.format("线程[%s]第二阶段运行结束耗时[%s]s",threadName,sleepTime/1000));
}catch (Exception e){
logger.error("TestCountDownLatchThread run err!",e);
}
}

这个时候日志输出会变成这样:

12:59:35.912 [Thread-1] INFO com.sync.Test - 线程[countDownLatchThread2]第一阶段开始运行...
12:59:35.912 [Thread-0] INFO com.sync.Test - 线程[countDownLatchThread1]第一阶段开始运行...
12:59:40.916 [Thread-0] INFO com.sync.Test - 线程[countDownLatchThread1]第一阶段运行结束 耗时[5]s
12:59:40.916 [Thread-1] INFO com.sync.Test - 线程[countDownLatchThread2]第一阶段运行结束 耗时[5]s
12:59:40.916 [main] INFO com.sync.Test - 主线程开始运行...
12:59:40.916 [Thread-0] INFO com.sync.Test - 线程[countDownLatchThread1]第二阶段开始运行...
12:59:40.916 [Thread-1] INFO com.sync.Test - 线程[countDownLatchThread2]第二阶段开始运行...
12:59:45.917 [Thread-0] INFO com.sync.Test - 线程[countDownLatchThread1]第二阶段运行结束 耗时[5]s
12:59:45.917 [Thread-1] INFO com.sync.Test - 线程[countDownLatchThread2]第二阶段运行结束 耗时[5]s

也就是说如果当前线程只需要等待其他线程一部分任务执行完毕的情况下就可以用 countDownLatch 来实现了,而 join 则实现不了这种粒度的控制。

join和countDownLatch原理及区别详解的更多相关文章

  1. HTTP POST GET 本质区别详解

    HTTP POST GET 本质区别详解 一 原理区别 一般在浏览器中输入网址访问资源都是通过GET方式:在FORM提交中,可以通过Method指定提交方式为GET或者POST,默认为GET提交 Ht ...

  2. Go语言备忘录:反射的原理与使用详解

    目录: 预备知识 reflect.Typeof.reflect.ValueOf Value.Type 动态调用 通过反射可以修改原对象 实现类似“泛型”的功能   1.预备知识: Go的变量都是静态类 ...

  3. 转-HTTP POST GET SOAP本质区别详解

    原文链接:HTTP POST GET SOAP本质区别详解 一 原理区别 一般在浏览器中输入网址访问资源都是通过GET方式:在FORM提交中,可以通过Method指定提交方式为GET或者POST,默认 ...

  4. CGI,FastCGI,PHP-CGI与PHP-FPM区别详解【转】

    CGI CGI全称是“公共网关接口”(Common Gateway Interface),HTTP服务器与你的或其它机器上的程序进行“交谈”的一种工具,其程序须运行在网络服务器上. CGI可以用任何一 ...

  5. Spring学习 6- Spring MVC (Spring MVC原理及配置详解)

    百度的面试官问:Web容器,Servlet容器,SpringMVC容器的区别: 我还写了个文章,说明web容器与servlet容器的联系,参考:servlet单实例多线程模式 这个文章有web容器与s ...

  6. Go语言备忘录(2):反射的原理与使用详解

    本文内容是本人对Go语言的反射原理与使用的备忘录,记录了关键的相关知识点,以供翻查. 文中如有错误的地方请大家指出,以免误导!转摘本文也请注明出处:Go语言备忘录(2):反射的原理与使用详解,多谢! ...

  7. DeepLearning tutorial(3)MLP多层感知机原理简介+代码详解

    本文介绍多层感知机算法,特别是详细解读其代码实现,基于python theano,代码来自:Multilayer Perceptron,如果你想详细了解多层感知机算法,可以参考:UFLDL教程,或者参 ...

  8. 基于Java的打包jar、war、ear包的作用与区别详解

      本篇文章,小编为大家介绍,基于Java的打包jar.war.ear包的作用与区别详解.需要的朋友参考下   以最终客户的角度来看,JAR文件就是一种封装,他们不需要知道jar文件中有多少个.cla ...

  9. Android中Intent传值与Bundle传值的区别详解

    Android中Intent传值与Bundle传值的区别详解 举个例子我现在要从A界面跳转到B界面或者C界面   这样的话 我就需要写2个Intent如果你还要涉及的传值的话 你的Intent就要写两 ...

随机推荐

  1. 【BFS】【并查集】【Tarjan】【LCA】Gym - 101173H - Hangar Hurdles

    给你一张地图,给你q次询问,每次问你从A点到B点,最大能移动多大的箱子. 把每个点所能容纳的最大箱子求出来(BFS,八连通,一开始将所有边界点和障碍点入队).然后从大到小排序.然后用并查集将相邻(四联 ...

  2. 用flask开发个人博客(26)—— 利用config.py配置文件动态的创建不同的Flask对象

    原文:https://blog.csdn.net/hyman_c/article/details/52877704 对配置进行封装的目的是根据不同的使用场景,给flask的app赋予不同的config ...

  3. 误改sudoers的访问权限后的修复

     sudo: /etc/sudoers is mode 0777, should be 0440"问题的解决方法 ubuntu进入单用户模式,修改sudoers权限,修改root密码 1.重 ...

  4. php根据汉字获取拼音(php基于拼音搜索实现原理)

    php根据汉字获取拼音(php基于拼音搜索实现原理) 代码一:获取字符串汉字首字母,兼容GBK和UTF-8 <?php function getfirstchar($s0){   //获取单个汉 ...

  5. 将WPF版的弹幕播放器给优化了一下

    年前较闲的时候研究了一下WPF的性能优化,练手的时将之前写的弹幕播放器给重新写了一下.年前的时间不大够,没有写完,这两天接着弄了一下,基本上弄得差不多了. 主要重写了底层的渲染算法,优化后效果还是非常 ...

  6. PatchGuard Disabled V3

    https://twitter.com/Fyyre http://www.m5home.com/bbs/thread-5893-1-1.html http://pan.baidu.com/share/ ...

  7. Linux(CentOS)下的vsftpd服务器配置-五岳之巅

    说明:VSFTPD这款软件,网上和书里有很多配置文章,但不一定适用于您的主机,不同版本默认值不一样,我现在使用的是vsftpd-2.0.5-12.el5_3.1.千万记住:修改配置文件后,必须重新启动 ...

  8. ylbtech-LanguageSamples-Delegates(委托)

    ylbtech-Microsoft-CSharpSamples:ylbtech-LanguageSamples-Delegates(委托) 1.A,示例(Sample) 返回顶部 “委托”示例 本示例 ...

  9. C++中模块(Dll)对外暴露接口的方式

    总结下C++中模块(Dll)对外暴露接口的方式: (1)导出API函数的方式这种方式是Windows中调用DLL接口的最基本方式,GDI32.dll, User32.dll都是用这种方式对外暴露系统A ...

  10. eclipse安装pydev

    eclipse是常用的用来写java代码的IDE,但是其实也可以用来写python代码,只需要配置好pydev即可. 第一步 打开eclipse,点击Help,install new sofeware ...