• 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的线程仅仅了解了部分线程创建和同步相关的小部分知识点,但是其实在编程过程中遇到的事情并不仅仅限于此,所以进行整理,列表如下:

    当你深入了解,你就会发现世界如此广袤,而你对世界的了解则是如此浅薄,请永远保持谦卑的态度。
  • 相关阅读:
    【Demo 0011】多媒体播放器
    【Demo 0010】事件响应链
    【Demo 0009】表视图控制器
    【Demo 0008】标签控制器
    【Demo 0007】导航控制器
    【Demo 0006】iOS常用控件
    【Demo 0005】视图控制器
    【Demo 0004】屏幕、窗体及视图基础知识
    2019.7.16考试反思
    内网 可怜与超市题解 树形dp+合并
  • 原文地址:https://www.cnblogs.com/liwxmyself/p/15411771.html
Copyright © 2020-2023  润新知