线程中断与LockSupport

线程中断与LockSupport

Thread类下的方法

问题

  1. 三个方法了解过吗?用在哪?

  2. 如何停止一个运行中的线程?

  3. 如何中断一个运行中的线程?

中断机制

一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。

  1. 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制——中断。

  2. 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。

  3. 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

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

如何停止中断运行中的线程?

1. 通过一个volatile变量实现

public class InterruptDemo {
    
    static volatile boolean isStop = false;

    public static void main(String[] args) {

        new Thread(() -> {
           while (true) {
                if (isStop) {
                    System.out.println(Thread.currentThread().getName() + "\t isStop被修改为true,程序终止");
                    break;
                }
               System.out.println("t1 ------hello volatile");
           }
        }, "t1").start();

        try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}

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

2. 通过AtomicBoolean(原子布尔型)

public class InterruptDemo {
    
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);

    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                if (atomicBoolean.get()) {
                    System.out.println(Thread.currentThread().getName() + "\t atomicBoolean被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello atomicBoolean");
            }
        }, "t1").start();

        try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}

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

3. 通过Thread类自带的中断api方法实现

public class InterruptDemo {
    
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + "\t isInterrupted()被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello interrupt api");
            }
        }, "t1");
        t1.start();

        try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}

        new Thread(()->{
            t1.interrupt();
        },"t2").start();
    }
}

4. interrupt和interrupted源码分析

实例方法 interrupt(),可以看到在内部去调用了一个interrupt0()方法,这个方法是用native修饰的说明了是调用了C底层

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // 只是为了设置中断标志
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

/* Some private helper methods */
private native void setPriority0(int newPriority);
private native void stop0(Object o);
private native void suspend0();
private native void resume0();
private native void interrupt0();
private native void setNativeName(String name);

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

public boolean isInterrupted() {
    return isInterrupted(false);
}

// 测试某些线程是否已中断。中断状态是否重置取决于传递的 ClearInterrupted 的值。
private native boolean isInterrupted(boolean ClearInterrupted);

具体说明

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

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

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

不是,中断标识只是一个标识,不会影响线程

public class InterruptDemo02 {

    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            for(int i = 0; i < 300; i++){
                System.out.println("---------" + i);
            }
            System.out.println("t1线程调用interrupt()后的的中断标识02 \t"+Thread.currentThread().isInterrupted());
        },"t1");
        t1.start();

        System.out.println("t1线程默认的中断标识 \t"+t1.isInterrupted());
        t1.interrupt();

        try {TimeUnit.MILLISECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("t1线程调用interrupt()后的的中断标识01 \t"+t1.isInterrupted());

        try {TimeUnit.MILLISECONDS.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("t1线程调用interrupt()后的的中断标识03 \t"+t1.isInterrupted());
    }
}

阻塞案例

上方说明中提到当对一个线程,调用 interrupt() 时如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态) 在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。如果不调用就会出现死循环,下方案例演示

public class InterruptDemo03 {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + "\t中断标志位:" + Thread.currentThread().isInterrupted() + "程序停止");
                    break;
                }

                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();  // 加上这句话会抛异常停止程序,如果不加就一直死循环
                    e.printStackTrace();
                }

                System.out.println("hello");
            }
        }, "t1");
        t1.start();

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

        new Thread(() -> t1.interrupt(), "t2").start();
    }
}

阻塞案例-执行步骤

/**
 * 1. 中断标志位 默认是false
 * 2. t2 -----> t1发出了中断协商,t2调用 t1.interrupt(),中断标志位true
 * 3. 中断标志位true,正常情况下,程序停止
 * 4. 中断标志位true,异常情况下,InterruptedException,将会把中断状态清除,并且将收到InterruptedException。中断标志位false导致无限循环。 
 * 5. 在catch块中,需要再次给中断标志位设置为true,2次调用停止程序才ok
 *
 * sleep方法抛出InterruptedException后,中断标识也被清空置为false,我们在catch没有通过th.interrupt()方法再次将中断标志设置为true,这就导致无限循环了
 */

总结

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

静态方法 Thread.interrupted()

  • 判断线程是否被中断,并清除当前中断状态这个方法做了两件事:

    1. 返回当前线程的中断状态

    2. 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)

public class InterruptDemo04 {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
        System.out.println("-----1");
        Thread.currentThread().interrupt(); //中断标志位设置为true
        System.out.println("-----2");
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.interrupted());
    }
}

// 结果
main	false
main	false
-----1
-----2
main	true
main	false

源码

// 静态方法
Thread.interrupted();
// 源码
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

// 实例方法
Thread.currentThread().isInterrupted();
// 源码
public boolean isInterrupted() {
    return isInterrupted(false);
}

总结

  1. 底层都调用了native方法 isInterrupted
  2. 底层调用方法ClearInterrupted值不同,中断状态将会根据传入的Clearlnterrupted参数值确定是否重置
    • 静态方法interrupted 将会清除中断状态 (传入的参数Clearlnterrupted为true)
    • 实例方法isInterrupted 不会清除 (传入的参数Clearlnterrupted为false)

LockSupport

用于创建锁和其他同步类的基本线程阻塞原语

核心方法parkunpark

线程等待唤醒机制

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

  1. 使用object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程

  2. 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程

  3. LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

1. Object类中的wait和notify方法实现线程等待和唤醒

  1. 正常情况
public class LockSupportDemo {

    public static void main(String[] args) {
        
        Object objectLock = new Object();

        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" + "--- 被唤醒了");
        },"t1").start();

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

        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t --- 发出通知");
            }
        },"t2").start();
    }
}
// 结果
t1	 --- come in
t2	 --- 发出通知
t1	--- 被唤醒了

进程已结束,退出代码0
  1. 异常一
    wait方法和notify方法,两个都需要在同步代码块内使用
public class LockSupportDemo {

    public static void main(String[] args) {

        Object objectLock = new Object();

        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" + "--- 被唤醒了");
        },"t1").start();

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

        new Thread(() -> {
//            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t --- 发出通知");
//            }
        },"t2").start();
    }
}

// 结果
t1	 --- come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.zjh.java.LockSupportDemo.lambda$main$0(LockSupportDemo.java:18)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "t2" java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at com.zjh.java.LockSupportDemo.lambda$main$1(LockSupportDemo.java:31)
	at java.lang.Thread.run(Thread.java:748)

进程已结束,退出代码0
  1. 异常二
    notify 不能放在 wait 前面否则程序无法运行一直等待
public class LockSupportDemo {

    public static void main(String[] args) {

        Object objectLock = new Object();

        new Thread(() -> {
            // 暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1L); } 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" + "--- 被唤醒了");
        },"t1").start();
        
        new Thread(() -> {
            synchronized (objectLock) {
                objectLock.notify();
                System.out.println(Thread.currentThread().getName() + "\t --- 发出通知");
            }
        },"t2").start();
    }
}

// 结果
t2	 --- 发出通知
t1	 --- come in

总结

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

  • 先wait后notify才OK,顺序

2. Condition接口中的await后signal方法实现线程的等待和唤醒

正常情况

public class LockSupportDemo {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName() + "\t---come in");
                condition.await();
                System.out.println(Thread.currentThread().getName() + "\t ---被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        },"t1").start();

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

        new Thread(() -> {
            lock.lock();
            try
            {
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName() + "\t" + "---发出通知");
        },"t2").start();
    }
}

异常情况
同上方 wait 和 notify 一样

object 和 condition 方法使用基本一样
* 都需要先获取锁才可使用
* 都需要先等待在唤醒

3. LockSupport类中的park等待和unpark唤醒

概述

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

  • LockSupport类使用了一种名为Permit(许可) 的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),

  • permit(许可)只有两个值1和0,默认是0。0 是阻塞,1是唤醒

  • 可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1

主要方法

源码

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

public static void park() {
    UNSAFE.park(false, 0L);
}

unpark():调用unpark(thread)方法后,就会将thread线程的许可permit设置成1 (注意多次调用unpark方法,不会累加,permit值还是1) 会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

代码

可以看到下方代码是无锁的,所以避免了上面必须在锁中才能执行的问题

public class LockSupportDemo {

    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "\t---come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() +"\t---被唤醒了");
        },"t1");
        t1.start();

        new Thread(()->{
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t---发出通知");
        },"t2").start();
    }
}

LockSupport 支持先唤醒在阻塞,类似于高速公路的ETC,提前买好了通行证 unpark,到闸机处直接抬起栏杆放行了,没有park拦截了

public class LockSupportDemo {

    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName() + "\t---come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() +"\t---被唤醒了");
        },"t1");
        t1.start();

        new Thread(()->{
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t---发出通知");
        },"t2").start();
    }
}

注意:许可的上限是1,所以不要对某个线程park() 两次

总结

Lock Support是用来创建锁和其他同步类的基本线程阻塞原语。
Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。归根结
底,Lock Support调用的Unsafe中的native代码。

Lock Support提供park()unpark() 方法实现阻塞线程和解除线程阻塞的过程
Lock Support和每个使用它的线程都有一个许可(permit) 关联。
每个线程都有一个相关的permitpermit最多只有一个,重复调用unpark也不会积累凭证。

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

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

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

面试题

为什么可以突破 wait/notify 的原有调用顺序?

答:因为unpark获得了一个凭证, 之后再调用park方法, 就可以名正言顺的凭证消费, 故不会阻塞。先发放了凭证后续可以畅通无阻。

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

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

posted @ 2023-01-31 10:33  橙香五花肉  阅读(10)  评论(0编辑  收藏  举报