文章目录
声明:
本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。
本博客已标明出处,如有侵权请告知,马上删除。
3.2 方法 join 的使用
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时计算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完了再结束。比如子线程处理一个数据,主线程要取到这个数据中的值,就要用到 join() 方法了。方法 join() 的作用是等待线程对象销毁。
3.2.1 学习 join 方法前的铺垫
在介绍 join 方法之前,先来看一个实验。
-
创建一个自定义的线程类
public class MyThread extends Thread { @Override public void run() { try { int secondValue = (int) (Math.random() * 10000); System.out.println(secondValue); Thread.sleep(secondValue); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
测试类
public class Run { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); //Thread.sleep(?); System.out.println("当myThread对象执行完毕后再执行"); System.out.println("但上面代码的sleep的值写多少"); System.out.println("答案是不确定"); } }
运行结果:
当myThread对象执行完毕后再执行 但上面代码的sleep的值写多少 答案是不确定 5900
3.2.2 用 join() 方法来解决
-
创建一个自定义的线程类
public class MyThread extends Thread { @Override public void run() { try { int secondValue = (int) (Math.random() * 10000); System.out.println(secondValue); Thread.sleep(secondValue); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
测试类
public class Run { public static void main(String[] args) { try { MyThread myThread = new MyThread(); myThread.start(); myThread.join(); System.out.println("当对象 myThread 执行完毕后再执行"); } catch (InterruptedException e) { e.printStackTrace(); } } }
运行结果:
5482 当对象 myThread 执行完毕后再执行
方法 join 的作用是使所属的线程对象 x 正常执行 run() 方法中的任务,而使当前线程 z 进行无限期阻塞,等待线程 x 销毁后再继续执行线程 z 后面的代码。
方法 join 具有使线程排队运行的作用,有些类似同步的效果。join 与 synchronized 的区别是:join 内部是使用 wait() 方法进行等待的,而 synchronized 关键字是使用的是 “对象监视器” 原理做为同步。
3.2.3 方法 join 与异常
在 join 过程中,如果当前线程对象被中断,则当前线程出现异常。
示例如下:
-
创建三个自定义的线程类
public class ThreadA extends Thread { @Override public void run() { for (int i = 0; i < Integer.MAX_VALUE; i++) { String newString = new String(); Math.random(); } } }
public class ThreadB extends Thread { @Override public void run() { try { ThreadA threadA = new ThreadA(); threadA.start(); threadA.join(); System.out.println("线程B在run end处打印了"); } catch (InterruptedException e) { System.out.println("线程B在catch处打印了"); e.printStackTrace(); } } }
public class ThreadC extends Thread { private ThreadB threadb; public ThreadC(ThreadB threadb) { this.threadb = threadb; } @Override public void run() { threadb.interrupt(); } }
-
测试类
public class Run { public static void main(String[] args) { try { ThreadB b = new ThreadB(); b.start(); Thread.sleep(500); ThreadC c = new ThreadC(b); c.start(); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:
线程B在catch处打印了 java.lang.InterruptedException at java.lang.Object.wait(Native Method) at java.lang.Thread.join(Thread.java:1249) at java.lang.Thread.join(Thread.java:1323) at joinexception.ThreadB.run(ThreadB.java:9)
说明方法 join() 与 interrupt() 方法如果彼此相遇,则会出现异常。但进程按钮还是呈红色状态,原因是线程 ThreadA 还在继续运行,线程 ThreadA 并未出现异常,是正常执行的状态。
3.2.4 方法 join(long) 的使用
方法 join(long) 中的参数是设定等待的时间。
示例如下:
-
创建自定义的线程类
public class MyThread extends Thread { @Override public void run() { super.run(); try { System.out.println("begin timer=" + System.currentTimeMillis()); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
测试类
public class Test { public static void main(String[] args) { try { MyThread myThread = new MyThread(); myThread.start(); myThread.join(2000); // Thread.sleep(2000); System.out.println("end timer= " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } }
运行结果:
begin timer=1607782653815 end timer= 1607782655815
但将 main 方法中的代码改成使用 sleep(2000) 方法时,运行的效果还是等待了 2 秒,运行结果如下所示。
begin timer=1607783051218
end timer= 1607783053217
那使用 join(2000) 和使用 sleep(2000) 有什么区别呢?上面的示例中在运行效果上并没有区别,其实区别主要还是来自于这 2 个方法对同步的处理上。
3.2.5 方法 join(long) 和 sleep(long) 的区别
方法 join(long) 的功能在内部是使用 wait(long) 方法来实现的,所以 join(long) 方法具有释放锁的特点。
方法 join(long) 源代码如下:
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");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从源代码中可以了解到,当执行 wait(long) 方法后,当前线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。
而 Thread.sleep(long) 方法却不释放锁。
在下面的示例中将实验 Thread.sleep(long) 方法具有不释放锁的特点。
-
创建三个自定义的线程类
public class ThreadA extends Thread { private ThreadB b; public ThreadA(ThreadB b) { super(); this.b = b; } @Override public void run() { try { synchronized (b) { b.start(); Thread.sleep(6000); // Thread.sleep()不释放锁 } } catch (InterruptedException e) { e.printStackTrace(); } } }
public class ThreadB extends Thread { @Override public void run() { try { System.out.println(" b run begin timer=" + System.currentTimeMillis()); Thread.sleep(5000); System.out.println(" b run end timer=" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized public void bService() { System.out.println("打印 b Service timer=" + System.currentTimeMillis()); } }
public class ThreadC extends Thread { private ThreadB threadB; public ThreadC(ThreadB threadB) { super(); this.threadB = threadB; } @Override public void run() { threadB.bService(); } }
-
测试类
public class Run { public static void main(String[] args) { try { ThreadB b = new ThreadB(); ThreadA a = new ThreadA(b); a.start(); Thread.sleep(1000); ThreadC c = new ThreadC(b); c.start(); } catch (InterruptedException e) { e.printStackTrace(); } } }
运行结果:
b run begin timer=1607788792039 b run end timer=1607788797040 打印 b Service timer=1607788798039
由于线程 ThreadA 使用 Thread.sleep(long) 方法一直持有 ThreadB 对象的锁,时间达到 6 秒,所以线程 ThreadC 只有在 ThreadA 时间到达 6 秒后释放 ThreadB 的锁时,才可以调用 ThreadB 中的同步方法 synchronized public void bService()。
下面继续实验,验证 join() 方法释放锁的特点。
-
更改 ThreadA.java 类代码如下:
public class ThreadA extends Thread { private ThreadB b; public ThreadA(ThreadB b) { super(); this.b = b; } @Override public void run() { try { synchronized (b) { b.start(); b.join();// 说明join释放锁了 for (int i = 0; i < Integer.MAX_VALUE; i++) { String newString = new String(); Math.random(); } } } catch (InterruptedException e) { e.printStackTrace(); } } }
-
再次运行,结果如下
b run begin timer=1607790117470 打印 b Service timer=1607790118472 b run end timer=1607790122472
由于线程 ThreadA 释放了 ThreadB 的锁,所以线程 ThreadC 可以调用 ThreadB 中的同步方法 synchronized public void bService()。
此实验也再次说明 join(long) 方法具有释放锁的特点。
3.2.6 方法 join() 后面的代码提前运行:出现意外
针对前面章节中的代码进行测试的过程中,还可以延伸出 “陷阱式” 的结果,如果稍加不注意,就会掉进 “陷阱” 里。
示例如下:
-
创建两个自定义的线程类
public class ThreadA extends Thread { private ThreadB b; public ThreadA(ThreadB b) { super(); this.b = b; } @Override public void run() { try { synchronized (b) { System.out.println("begin A ThreadName=" + Thread.currentThread().getName() + " " + System.currentTimeMillis()); Thread.sleep(5000); System.out.println(" end A ThreadName=" + Thread.currentThread().getName() + " " + System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); } } }
public class ThreadB extends Thread { @Override synchronized public void run() { try { System.out.println("begin B ThreadName=" + Thread.currentThread().getName() + " " + System.currentTimeMillis()); Thread.sleep(5000); System.out.println(" end B ThreadName=" + Thread.currentThread().getName() + " " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
测试类
public class Run1 { public static void main(String[] args) { try { ThreadB b = new ThreadB(); ThreadA a = new ThreadA(b); a.start(); b.start(); b.join(2000); System.out.println(" main end " + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } }
程序运行后,在控制台打印结果有以下两种情况:
begin A ThreadName=Thread-1 1607792596530
end A ThreadName=Thread-1 1607792601532
main end 1607792601532
begin B ThreadName=Thread-0 1607792601532
end B ThreadName=Thread-0 1607792606532
begin A ThreadName=Thread-1 1607793705864
end A ThreadName=Thread-1 1607793710866
begin B ThreadName=Thread-0 1607793710866
end B ThreadName=Thread-0 1607793715866
main end 1607793715866
为什么出现截然不同的运行结果呢?
3.2.7 方法 join() 后面的代码提前运行:解释意外
为了查看 join() 方法在 Run1.java 类中执行的时机,创建 RunFirst.java 类文件,代码如下:
public class RunFirst {
public static void main(String[] args) {
ThreadB b = new ThreadB();
ThreadA a = new ThreadA(b);
a.start();
b.start();
System.out.println(" main end=" + System.currentTimeMillis());
}
}
程序多次运行结果,如下所示:
main end=1607794763236
begin A ThreadName=Thread-1 1607794763236
end A ThreadName=Thread-1 1607794768237
begin B ThreadName=Thread-0 1607794768237
end B ThreadName=Thread-0 1607794773238
通过多次运行 RunFirst.java 文件后,可以发现一个规律:main end 往往都是第一个打印的。所以可以完全确定地得出一个结论:方法 join(2000) 大部分是先运行的,也就是先抢到 ThreadB 的锁,然后快速进行释放。
而执行 Run1.java 文件后就会出现一些不同的运行结果: