Java并发编程"锁"篇3-LockSupport与线程中断

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1. 线程中断机制

1.1 什么是中断?

  • 首先一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend,Thread.resume 都已经被废弃了。
  • 其次在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制——中断
  • 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。
  • 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

1.2 中断的相关API方法

public void interrupt() 实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程
public static boolean interrupted() 静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态;这个方法做了两件事:①返回当前线程的中状态将当前线程的中断状态设为false。这个方法有点不好理解,因为连续调用两次的结果可能不一样。
public boolean isInterrupted() 实例方法,判断当前线程是否被中断(通过检查中断标志位)

1.3 如何使用中断标识停止线程?

在需要中断的线程中不断监听中断状态,一旦发生中断,就执行相应的中断处理业务。

1.3.1 方法一:通过一个volatile变量实现

因为大家都知道该关键字具有内存可见性——即一个线程修改了主内存中的该变量的值,另一个线程就会立马知道,代码演示如下:

/**
 * 通过一个volatile变量实现
 */
static volatile boolean isStop = false;

public static void m1() {
    new Thread(() -> {
        while(true) {
            if(isStop) {
                System.out.println("-----isStop = true,程序结束。");
                break;
            }
            System.out.println("------hello isStop");
        }
    },"t1").start();

    //暂停几秒钟线程
    try { 
        TimeUnit.SECONDS.sleep(1); 
    } catch (InterruptedException e) { 
        e.printStackTrace(); 
    }

    new Thread(() -> {
        isStop = true;
    },"t2").start();
}

复制代码

1.3.2 方法二:通过AtomicBoolean

代码演示如下:

/**
 * 通过AtomicBoolean
 */
 static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
 
 public static void m2() {
     new Thread(() -> {
         while(true) {
             if(atomicBoolean.get()) {
                 System.out.println("-----atomicBoolean.get() = true,程序结束。");
                 break;
             }
             System.out.println("------hello atomicBoolean");
         }
     },"t1").start();

     //暂停几秒钟线程
     try { 
         TimeUnit.SECONDS.sleep(1); 
     } catch (InterruptedException e) { 
         e.printStackTrace(); 
     }

     new Thread(() -> {
         atomicBoolean.set(true);
     },"t2").start();
 }
复制代码

1.3.3 方法三:通过Thread类自带的中断api方法实现

在这里插入图片描述

代码演示如下:

public static void m3()
    {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("-----isInterrupted() = true,程序结束。");
                    break;
                }
                System.out.println("------hello Interrupt");
            }
        }, "t1");
        t1.start();

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            t1.interrupt();//修改t1线程的中断标志位为true
        },"t2").start();
    }
复制代码

方法说明:

  • 实例方法interrupt(),没有返回值

    • public void interrupt(),实例方法,调用interrupt()方法仅仅是当前线程中打了一个终止的标记位,并不是真正立刻停止线程
    • 源码分析: 在这里插入图片描述
  • 实例方法isInterrupted,返回布尔值

    • public boolean isInterrupted(),实例方法,获取中断标志位的当前值是什么,判断当前线程是否被中断(通过检查中断标志位),默认是false

    • 源码分析: 在这里插入图片描述

1.4 当前线程的中断标识为true,是不是就立刻停止?

具体来说,当对一个线程,调用 interrupt() 时:

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已被设置中断标志的线程将继续正常运行,不受影响。所以interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

  • 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

代码演示如下:

/**
 *中断为true后,并不是立刻stop程序
 */
public static void m4() {
    //中断为true后,并不是立刻stop程序
    Thread t1 = new Thread(() -> {
        for (int i = 1; i <= 300; i++) {
            System.out.println("------i: " + i);
        }
        System.out.println("t1.interrupt()调用之后02: "+Thread.currentThread().isInterrupted());
    }, "t1");
    t1.start();

    System.out.println("t1.interrupt()调用之前,t1线程的中断标识默认值: "+t1.isInterrupted());
    try { 
        TimeUnit.MILLISECONDS.sleep(3); 
    } catch (InterruptedException e) { 
        e.printStackTrace(); 
    }
    //实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
    t1.interrupt();
    //活动状态,t1线程还在执行中
    System.out.println("t1.interrupt()调用之后01: "+t1.isInterrupted());

    try { 
        TimeUnit.MILLISECONDS.sleep(3000); 
    } catch (InterruptedException e) { 
        e.printStackTrace();
    }
    //非活动状态,t1线程不在执行中,已经结束执行了。
    System.out.println("t1.interrupt()调用之后03: "+t1.isInterrupted());
}
复制代码

面试常问例子,代码如下:

public static void m5()
    {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("-----isInterrupted() = true,程序结束。");
                    break;
                }
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();//???????  //线程的中断标志位为false,无法停下,需要再次掉interrupt()设置true
                    e.printStackTrace();
                }
                System.out.println("------hello Interrupt");
            }
        }, "t1");
        t1.start();

        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            t1.interrupt();//修改t1线程的中断标志位为true
        },"t2").start();
    }
复制代码

结论:

上述代码中如果没有在catch中将线程的中断状态再次设置为true,则线程不会停止,因为sleep方法抛出异常之后,中断标识也被清空置为false

中断只是一个协同机制,修改中断标识位仅此而已,不是立刻stop打断

1.5 静态方法Thread.interrupted()

  • 静态方法,Thread.interrupted();
  • 判断线程是否被中断,并清除当前中断状态;这个方法做了两件事:
    • 返回当前线程的中状态
    • 将当前线程的中断状态设为false
  • 这个方法有点不好理解,因为连续调用两次的结果可能不一样。

在这里插入图片描述

即该方法首先将当前线程的中断状态返回,接着将该线程的中断状态设置为false,代码演示如下:

// 作用是测试当前线程是否被中断(检查中断标志),返回一个 *boolean* 并清除中断状态,
// 第二次再调用时中断状态已经被清除,将返回一个 *false* 。
public static void main(String[] args)
    {
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println("111111");
        Thread.currentThread().interrupt();///----false---> true
        System.out.println("222222");
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted());
    }
复制代码

运行结果如下:

> main---false

main---false 111111 222222 main---true main---false
复制代码

静态方法跟实例方法都会返回中断状态,两者对比

在这里插入图片描述

综上所以,静态方法interrupted会清除中断状态(传入的参数ClearInterrupted为true),实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)。

1.6 总结

线程中断相关的方法:

  • interrupt()方法是一个实例方法

    • 它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了。
  • isInterrupted()方法也是一个实例方法

    • 它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志 
  • Thread类的静态方法interrupted() 

    • 返回当前线程的中断状态(boolean类型)且将当前线程的中断状态设为false,此方法调用之后会清除当前线程的中断标志位的状态(将中断标志置为false了),返回当前值并清零置false

2. LockSupport

2.1 概念

LockSupport简介: LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,官网对于该类的解释如下图所示,LockSupport中的park()unpark()的作用分别是阻塞线程和解除阻塞线程

在这里插入图片描述

3. 线程等待和唤醒方法

3.1 三种让线程等待和唤醒的方法

3.1.1 方式一:使用Object中的wait()方法让线程等待

使用Object中的notify()方法唤醒线程。 代码演示如下:

public class LockSupportDemo {

    static Object objectLock=new Object();

    public static void main(String[] args) {//main方法,一切程序的入口
        new Thread(() -> {
            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
             synchronized (objectLock){
                 objectLock.notify();
                 System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
             }
        }, "B").start();
    }
}
复制代码

运行结果如下:

> A ------come in

B ------通知 A ------被唤醒
复制代码

特殊情况:如果先通知,再阻塞呢? 修改上述的代码如下:

public class LockSupportDemo {

    static Object objectLock=new Object();

    public static void main(String[] args) {//main方法,一切程序的入口
        new Thread(() -> {
            //为了让B线程先通知,A线程再阻塞
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (objectLock){
                System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
                try {
                    objectLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
        }, "A").start();

        new Thread(() -> {
             synchronized (objectLock){
                 objectLock.notify();
                 System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
             }
        }, "B").start();
    }
}
复制代码

运行结果如下:

> B ------通知

A ------come in //A线程一直在阻塞,等待唤醒
复制代码

小总结 :

  • wait和notify方法必须要在同步块或者方法里面且成对出现使用
  • 先wait后notify才OK

3.1.2 方式二:使用JUC包中Condition

使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程 。 传统的synchronized和Lock实现等待唤醒通知的约束:

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中
  • 必须要先等待后唤醒,线程才能够被唤醒

3.1.3 方式三:LockSupport类

可以阻塞当前线程以及唤醒指定被阻塞的线程。 通过park()unpark(thread)方法来实现阻塞和唤醒线程的操作,LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。

主要方法 : 在这里插入图片描述

  • 阻塞:park()/park(Object blocker)阻塞当前线程/阻塞传入的具体线程

    在这里插入图片描述

    permit默认是0,所以一开始调用park()方法当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒, 然后会将permit再次设置为O并返回。

  • 唤醒:unpark(Thread thread)唤醒处于阻断状态的指定线程 在这里插入图片描述
    调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程, 即之前阻塞中的LockSupport.park()方法会立即返回

代码演示1:正常+无锁块要求

public class LockSupportDemo1 {
    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
            LockSupport.park();//被阻塞......等待许可证
            System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
        }, "A");
        a.start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread b = new Thread(() -> {
            LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"\t"+"------唤醒线程a");
        }, "B");
        b.start();
    }
}
复制代码

运行结果如下:

> A ------come in

B ------唤醒线程a A ------被唤醒
复制代码

代码演示2:之前错误的先唤醒后等待,LockSupport照样支持

public class LockSupportDemo2 {
    public static void main(String[] args) {
        Thread a = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
            LockSupport.park();//被阻塞......等待许可证
            System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
        }, "A");
        a.start();

        Thread b = new Thread(() -> {
            LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"\t"+"------唤醒线程a");
        }, "B");
        b.start();
    }
}
复制代码

运行结果如下:

> B ------唤醒线程a

A ------come in A ------被唤醒
复制代码

sleep方法3秒后醒来,执行park方法无效,没有阻塞效果,解释如下:先执行了unpark(t1)导致上面的park方法形同虚设无效,时间一样

3.2 重点说明

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语 LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。

归根结底,LockSupport调用的Unsafe中的native代码。 LockSupport提供park()unpark()方法实现阻塞线程和解除线程阻塞的过程。 LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0, 调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成0,同时park立即返回。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。 每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。

形象的理解:线程阻塞需要消耗凭证(permit),这个凭证最多只有1个

当调用park方法时

  • 如果有凭证,则会直接消耗掉这个凭证然后正常退出;
  • 如果无凭证,就必须阻塞等待凭证可用;

unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效

面试题

  1. 为什么可以先唤醒线程后阻塞线程?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

  1. 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证; 而调用两次park却需要消费两个凭证,证不够,不能放行。

参考资料

Java并发编程知识体系
Java并发编程的艺术
Java多线程编程核心技术
Java并发实现原理 JDK源码剖析

Guess you like

Origin juejin.im/post/7075306555423326244