《Java 多线程编程核心技术》笔记——第3章 线程间通信(三)

声明:

本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

3.2 方法 join 的使用

在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时计算,主线程往往将早于子线程结束之前结束。这时,如果主线程想等待子线程执行完了再结束。比如子线程处理一个数据,主线程要取到这个数据中的值,就要用到 join() 方法了。方法 join() 的作用是等待线程对象销毁

3.2.1 学习 join 方法前的铺垫

在介绍 join 方法之前,先来看一个实验。

  1. 创建一个自定义的线程类

    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();
            }
        }
    }
    
  2. 测试类

    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() 方法来解决

  1. 创建一个自定义的线程类

    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();
            }
        }
    }
    
  2. 测试类

    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 过程中,如果当前线程对象被中断,则当前线程出现异常。

示例如下:

  1. 创建三个自定义的线程类

    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();
        }
    }
    
    
  2. 测试类

    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) 中的参数是设定等待的时间

示例如下:

  1. 创建自定义的线程类

    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();
            }
        }
    }
    
  2. 测试类

    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) 方法具有不释放锁的特点。

  1. 创建三个自定义的线程类

    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();
        }
    
    }
    
  2. 测试类

    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() 方法释放锁的特点。

  1. 更改 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();
            }
        }
    }
    
  2. 再次运行,结果如下

       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() 后面的代码提前运行:出现意外

针对前面章节中的代码进行测试的过程中,还可以延伸出 “陷阱式” 的结果,如果稍加不注意,就会掉进 “陷阱” 里。

示例如下:

  1. 创建两个自定义的线程类

    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();
            }
        }
    }
    
  2. 测试类

    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 文件后就会出现一些不同的运行结果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值