JAVA篇:Java 多线程 (一) 线程控制

1、线程控制

关键字:wait/notify/notifyAll、join、sleep、interrupt

线程控制讨论线程在调用了start()到执行完成中间阶段的行为,包含

  1. 线程阻塞和唤醒、超时等待

  2. 线程中断机制

 

1.1 线程阻塞和唤醒、超时等待

主要讨论join(),wait()、notify()和notifyAll(),以及yield()和sleep()。以及期间cpu资源及锁资源的情况,这里的锁仅仅考虑synchronized(Object)对象锁。

1.1.1 join() 方法

join()是线程的实例方法,有两种形式,thread.join()和tread.join(long timeout)。join方法会阻塞当前线程,等待指定线程运行完毕后才会被唤醒,或者如果设置了超时,等到超时后当前线程也会被唤醒

join()方法使得当前线程休眠,释放cpu资源,但是并不会释放锁

有说法说“其底层实现是wait()方法,会释放锁。”然后我写了一个测试代码,形成了死锁。wait()方法释放锁的相关讨论在后文,在这里先讨论join()方法运行过程中的情况。

join()的前缀是线程实例。如果要描述得清楚些则需要做一些假设。譬如说由线程A和t1,t1处于运行状态,在当前线程A调用t1.join()。那么线程A会无限阻塞,直到t1运行结束。

那么A在调用了t1.join()后等待t1的期间是否会释放资源呢?我感觉释放资源这个要往细了说,释放什么资源?释放cpu资源,释放cpu资源和全部锁资源,释放cpu资源和指定锁资源。

有说法join()方法调用之后会释放锁,总不可能是释放t1持有的锁吧,所以只能理解为释放当前线程A持有的锁。但是join方法不同,它是由t1调用的,也无法预见t1会需要什么锁资源,那么A调用t1.join()只可能是任意释放一个锁,或者说更加靠谱地释放全部锁。

我按照这个思路写了测试代码,但是主线程调用了join()并未释放任何锁,然后两个子线程都无法获得锁,造成了死锁。

为了防止死锁,我将join()方法设置了超时,使得代码可以运行,最后的代码和结果如下:

  
 /* 测试join() */
    public void test2(){
        /* 模拟共享资源 */
        Object res = new Object();
        Object res2 = new Object();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
​
        /*创建子线程1,共享资源res*/
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(df.format(new Date())+" part11:子线程1休眠结束,尝试请求res锁");
                    synchronized (res){
                        System.out.println(df.format(new Date())+" part12:子线程1获得res锁,后进入休眠");
                        Thread.sleep(3000);
                        System.out.println(df.format(new Date())+" part13:子线程1结束休眠,并释放res锁");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
​
        Thread t1 = new Thread(r1);
​
        t1.start();
​
        /*创建子线程2,共享资源res2*/
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println(df.format(new Date())+" part21:子线程2休眠结束,尝试请求res2锁");
                    synchronized (res2){
                        System.out.println(df.format(new Date())+" part22:子线程2获得res2锁,后进入休眠");
                        Thread.sleep(3000);
                        System.out.println(df.format(new Date())+" part23:子线程2结束休眠,并释放res2锁");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
​
        Thread t2 = new Thread(r2);
​
        t2.start();
​
        /* 主线程持有锁,然后调用join */
       synchronized (res2){
            System.out.println(df.format(new Date())+" part01:主线程持有res2锁");
            synchronized (res){
                System.out.println(df.format(new Date())+" part02:主线程持有res锁");
                System.out.println(df.format(new Date())+" part03:主线程调用t1,t2的join()");
                try {
                    t1.join(10000);//有等待时限的join
                    t2.join(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(df.format(new Date())+" part04:主线程退出join,不再等待,释放锁res");
​
            }
           System.out.println(df.format(new Date())+" part05:主线程释放锁res2");
        }
​
        System.out.println(df.format(new Date())+" 测试结束。");
    }

 


结果如下,可以看到主线程调用了join方法后既没有释放res,也没有释放res2。是等待join()超时后,主线程释放了锁,两个子线程才能请求到锁继续运行。

2021-10-08 16:17:21:549 part01:主线程持有res2锁
2021-10-08 16:17:21:549 part02:主线程持有res锁
2021-10-08 16:17:21:550 part03:主线程调用t1,t2的join()
2021-10-08 16:17:24:556 part21:子线程2休眠结束,尝试请求res2锁
2021-10-08 16:17:24:556 part11:子线程1休眠结束,尝试请求res锁
2021-10-08 16:17:41:553 part04:主线程退出join,不再等待,释放锁res ##子线程没有获得锁无法运行,直到主线程join超时退出
2021-10-08 16:17:41:553 part12:子线程1获得res锁,后进入休眠
2021-10-08 16:17:41:553 part05:主线程释放锁res2
2021-10-08 16:17:41:553 测试结束。
2021-10-08 16:17:41:553 part22:子线程2获得res2锁,后进入休眠
2021-10-08 16:17:44:563 part23:子线程2结束休眠,并释放res2锁
2021-10-08 16:17:44:563 part13:子线程1结束休眠,并释放res锁

 

1.1.2 wait()、notify()和notifyAll()

wait()和notify()用于线程间的休眠与唤醒。wait(long timeout)可以设置超时,notifyAll()用于唤醒全部wait()状态中的线程。

wait()和notify()都是定义在Object的方法,因为可以认为任意一个Object都是一种资源(或者资源的一个代表)。无论是对资源加锁,对资源等待,唤醒等待该资源的其他对象,都是针对资源Object来进行的。

wait()和notify()需要配合synchronized使用,同一时间一个锁只能被一个线程持有,当持有该对象的锁的时候才可以调用该对象的wait()和notify()。当调用wait()的时候,当前线程会放弃指定对象的锁,而notify()不会。

   /* 测试:wait()和notify() */
    public void test1(){
        /* 模拟共享资源 */
        Object res = new Object();
        Object res2 = new Object();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
​
        /* 步骤1:创建子线程,调用wait() */
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
               synchronized (res2){
                    synchronized (res){
                        System.out.println(df.format(new Date())+" part11:子线程1获得res锁,调用wait()");
                        try {
                            res.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(df.format(new Date())+" part12:子线程1被唤醒,并sleep-10ms");
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(df.format(new Date())+" part13:子线程1释放res锁");
                    }
                   System.out.println(df.format(new Date())+" part14:子线程1释放res2锁");
               }
            }
        };
        Thread t1 = new Thread(r1);
        t1.start();
​
        /* 步骤2:创建与子线程1竞争res2的子线程2 */
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                synchronized (res2){
                    System.out.println(df.format(new Date())+" part21:子线程2获得res2锁,然后释放");
                }
            }
        };
        Thread t2 = new Thread(r2);
        t2.start();
​
        /* 步骤3:主线程请求锁,并调用notify() */
        try {
            System.out.println(df.format(new Date())+" part01:主线程休眠10ms");
            Thread.sleep(10);
​
            synchronized (res){
                System.out.println(df.format(new Date())+" part02:主线程获得res锁,并调用notify()");
                res.notify();
                System.out.println(df.format(new Date())+" part03:主线程休眠10ms");
                Thread.sleep(10);
                System.out.println(df.format(new Date())+" part04:主线程释放res锁");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        /* 步骤4:主线程请求锁 */
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(df.format(new Date())+" part05:主线程请求res锁");
        synchronized (res){
            System.out.println(df.format(new Date())+" part06:主线程获得res锁");
            System.out.println(df.format(new Date())+" part07:主线程释放res锁");
        }
        System.out.println("测试结束。");
​
    }

 


结果如下:

2021-10-08 15:13:54:679 part11:子线程1获得res锁,调用wait()
2021-10-08 15:13:54:680 part01:主线程休眠10ms
2021-10-08 15:13:54:697 part02:主线程获得res锁,并调用notify()
2021-10-08 15:13:54:697 part03:主线程休眠10ms
2021-10-08 15:13:54:712 part04:主线程释放res锁
2021-10-08 15:13:54:712 part12:子线程1被唤醒,并sleep-10ms
2021-10-08 15:13:54:714 part05:主线程请求res锁
2021-10-08 15:13:54:723 part13:子线程1释放res锁
2021-10-08 15:13:54:723 part14:子线程1释放res2锁
2021-10-08 15:13:54:723 part06:主线程获得res锁
2021-10-08 15:13:54:723 part07:主线程释放res锁
测试结束。
2021-10-08 15:13:54:723 part21:子线程2获得res2锁,然后释放

 

测试代码运行逻辑如下:

  • part01:主线程创建子线程t1后休眠

  • part11:子线程t1获得res锁,并且调用wait()后释放res锁,进入休眠

  • part02:主线程休眠结束,请求res锁,由于此时并没有线程占有res锁,主线程获得锁,并且调用notify()唤醒等待的子线程t1。但是虽然t1被唤醒,由于并未获得res锁,无法运行同步代码区里面的代码,仍处于请求等待阶段

  • part03:主线程在唤醒t1后,在仍占有res锁的情况下自顾自休眠sleep了,子线程t1仍在请求锁并等待

  • part04:主线程睡醒了,释放res锁

  • part12:子线程t1获得res锁,然后休眠sleep

  • part05:主线程又需要请求res锁,但是res锁被子线程占有,所以主线程等待

  • part13:子线程t1睡醒了,释放res锁

  • part06:主线程获得res锁,进入同步方法区

  • part07:主线程释放res锁

  • part21:子线程2主要用来测试res.wait()方法调用后是否会释放res2锁,显然是不会的。子线程1即使处于res.wait()阻塞状态,仍然持有res2资源锁。

1.1.3 锁

这里讨论的锁由synchronized所修饰,只有获得锁,才能进入指定的方法区执行方法。一个锁在同一时间只能被一个线程占有,其他需要申请锁的线程则会被阻塞。

针对共享资源res,所涉及的问题包含:

  1. synchronized(res)对该资源进行同步,只有获得res锁才可以进入同步方法区。

  2. 获得了res锁之后才可以调用res的wait()方法,调用wait()方法后当前线程休眠,并释放之前占有的res锁,但是不会释放其他资源的锁。

  3. 获得了res锁之后才可以调用res的notify()方法,在等待该资源的全部线程中任意唤醒一个线程。但是被唤醒线程不占有res锁,如果需要进入同步方法区,需要重新竞争res锁。

  4. notifyAll()方法则是唤醒等待该资源的全部线程,所有线程进入竞争res锁的状态。

1.1.4 wait()、sleep()、yield()、join()

Object.wait()、Thread.sleep(long timeout)、Thread.yield()、thread.join()的区别需要提及线程运行过程中的运行状态、就绪状态和阻塞状态。

就绪状态的线程只需要等待cpu资源就可以进入运行状态,阻塞状态在等待某些条件满足后才能进入就绪状态,等待进入运行状态。

阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入等待池中。进入这个状态后是不能自动唤醒的,必须依靠其他线程调用notify()/notifyAll()方法才能被唤醒。

  • 同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被其他线程占用,则JVM会把该线程放入“锁池”中。

  • 其他阻塞:通过调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、Join()等待线程终止或者超时、或者I/O处理完毕时,线程重新回到就绪状态。

 

而Object.wait()、Thread.sleep()、Thread.yield()这三种方法。

  1. Thread.yield()属于让线程从运行状态回到就绪状态,暂时让出cpu资源,与其他就绪状态线程一起等待cpu资源,也有可能会马上回到运行状态。

  2. Thread.sleep()属于强制线程休眠,只是让出了cpu的执行权,并不会释放同步资源锁,等到休眠时间超时后会重新进入就绪状态。

  3. Object.wait()则分为带时间或者不带时间两种休眠,会释放资源,等时间超时或者调用notify()/notifyAll()方法后需要重新申请所需的锁资源,仍处于阻塞状态,需要等待获取了锁才能够进入就绪状态。

  4. thread.join()方法会阻塞当前线程,等待指定线程运行完毕后才会被唤醒。有说法说“其底层实现是wait()方法,会释放资源。”然后我写了一个测试代码,形成了死锁。这个的具体描述在下面。

1.2 线程中断机制

1.2.1 中断机制

Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制。中断机制中每个线程对象都有一个中断标识位表示是否有中断请求,想要发出中断请求的线程只能将指定线程的中断标识位设置为True,指定线程可以考虑是否要检查这个标识位并且做出反应。换言之,如果我这个线程从头到尾没有检查标识位的行为,其他线程将这个中断标识位设置为True也是完全没有用的,其他线程并不能直接中断这个线程。

Thread提供了三个方法:

  1. interrupt()方法,这是Thread类的实例方法,对一个线程调用interrupt()方法表示请求终端这个线程。该方法是唯一能将中断状态设置为True的方法。

  2. isInterrupted()方法,这是Thread类的实例方法,测试线程是否已经终端,也就是测试线程中中断状态是否被设置为True,也即是是否有中断请求。

  3. Thread.interrupted(),这是Thread类的静态方法,判断线程是否被中断并清除中断状态,即先判断是否有中断请求返回True/False,然后将中断标识位重置为False。

 

1.2.2 中断请求测试代码

下面是一个线程响应中断请求的测试代码。

 /* 中断机制测试 */
    public void test3()
    {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println(df.format(new Date())+" part11:子线程1开始运行,需要中断5次才能退出!");
                int inter_times = 5;
​
                while (true){
                    //if(Thread.currentThread().isInterrupted()){//这个判断后不会将中断标识位重新置为False,所以主线程中断一次就会触发5次反应
                    if(Thread.interrupted()){
                        inter_times = inter_times-1;
                        System.out.println(df.format(new Date())+" part13:子线程1被中断一次,剩余次数:"+inter_times);
                        if(inter_times<=0){
                            System.out.println(df.format(new Date())+" part14:子线程1被成功中断,退出运行");
                            return;
                        }
                    }else{
                        System.out.println(df.format(new Date())+" part12:子线程1仍在运行");
                    }
                }
​
​
            }
        };
​
        Thread t1 = new Thread(r1);
        t1.setDaemon(true);
        t1.start();
​
​
        while (t1.isAlive()){
            System.out.println(df.format(new Date())+" part01:主线程尝试中断子线程1.....");
            t1.interrupt();
​
            /*try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
​
        }
​
        System.out.println(df.format(new Date())+"测试结束。");
​
    }

 

结果如下:

2021-10-09 14:37:02:039 part11:子线程1开始运行,需要中断5次才能退出!
2021-10-09 14:37:02:039 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:040 part12:子线程1仍在运行
2021-10-09 14:37:02:040 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:040 part13:子线程1被中断一次,剩余次数:4
2021-10-09 14:37:02:040 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:040 part13:子线程1被中断一次,剩余次数:3
2021-10-09 14:37:02:040 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:040 part13:子线程1被中断一次,剩余次数:2
2021-10-09 14:37:02:040 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:040 part13:子线程1被中断一次,剩余次数:1
2021-10-09 14:37:02:040 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:040 part13:子线程1被中断一次,剩余次数:0
2021-10-09 14:37:02:040 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:040 part14:子线程1被成功中断,退出运行
2021-10-09 14:37:02:040 part01:主线程尝试中断子线程1.....
2021-10-09 14:37:02:041测试结束。

 

1.2.3 线程所处状态以及中断

运行状态的线程,再接收到中断请求(isInterrupted()\Thread.interrupted())之后,可以选择在合适的位置对中断请求做出对应反应或者说不做反应。

但是有一部分方法,如sleep、wait、notify、join,这些方法会抛出InterruptedException,当遇到了中断请求,必须有对应的措施,可以在catch块中进行处理,也可以抛给上一次层。因为Java虚拟机在实现这些方法的时候,本身就有某种机制在判断中断标识位,如果中断了,就抛出一个InterruptedException。

1.X 参考

Java多线程8:wait()和notify()/notifyAll()

多线程面试题之为什么wait(),notify(),notifyAll()等方法都是定义在Object类中

【Java并发系列02】Object的wait()、notify()、notifyAll()方法使用

4.sleep()和wait()方法有什么区别?

Java线程的wait(), notify()和notifyAll()

阻塞和唤醒线程——LockSupport功能简介及原理浅析

Java线程状态以及 sheep()、wait()、yield() 的区别

 

Java多线程17:中断机制

JAVA中断机制

 

0、JAVA多线程编程

Java多线程编程所涉及的知识点包含线程创建、线程同步、线程间通信、线程死锁、线程控制(挂起、停止和恢复)。之前 JAVA篇:Java的线程仅仅了解了部分线程创建和同步相关的小部分知识点,但是其实在编程过程中遇到的事情并不仅仅限于此,所以进行整理,列表如下:

posted @ 2021-10-15 16:53  l.w.x  阅读(599)  评论(0编辑  收藏  举报