04-JUC进阶-LockSupport与线程中断

一、线程中断机制

1. 什么是中断机制 ?

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

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

中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

  • 若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true
  • 接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,
  • 此时究竟该做什么需要你自己写代码实现。
  • 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
  • 通过调用线程对象的 interrupt 方法将该线程的标识位设为 true ;可以在别的线程中调用,也可以在自己的线程中调用。

2. 中断的相关API方法

线程中断API

API 描述
public void interrupt() 实例方法,
实例方法 interrupt() 仅仅是设置线程的中断状态为true,不会停止线程
public static boolean interrupted() 静态方法Thread.interrupted();
判断线程是否被中断,并清除当前中断状态
这个方法做了两件事:
1 返回当前线程的中断状态
2 将当前线程的中断状态设为false

这个方法有点不好理解,因为连续调用两次的结果可能不一样。
public boolean isInterrupted() 实例方法,
判断当前线程是否被中断(通过检查中断标志位)

2.1 通过一个 volatile 变量实现

  • volatile保证了可见性,t2修改了标志位后能马上被t1看到
/**
 * volatile变量实现线程中断
 *
 * @throws InterruptedException
 */
private static void volatileStopThread() throws InterruptedException {
    new Thread(() -> {
        while (!isStop) {
            System.out.println("------- t1 run");
        }
        System.out.println("===== t1 is stop =====");
    }, "t1").start();

    // 让线程t1运行50毫秒后,中断线程运行
    Thread.sleep(10);

    new Thread(() -> {
        isStop = true;
        System.out.println("------- t2 stop: " + isStop);
    }, "t2").start();
}

2.2 通过 AtomicBoolean(原子布尔型)

/**
 * 通过AtomicBoolean变量实现线程中断
 *
 * @throws InterruptedException
 */
private static void atomicBooleanStopThread() throws InterruptedException {
    new Thread(() -> {
        while (!flag.get()) {
            System.out.println("------- t1 run");
        }
        System.out.println("===== t1 is stop =====");
    }, "t1").start();

    // 让线程t1运行50毫秒后,中断线程运行
    Thread.sleep(50);

    new Thread(() -> {
        flag.set(true);
        System.out.println("------- t2 stop: " + flag.get());
    }, "t2").start();
}

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

/**
 * 通过Thread类自带的中断api方法实现
 *
 * @throws InterruptedException
 */
private static void threadApiInterruptThread() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("------- t1 run");
        }
        System.out.println("===== t1 is stop =====");
    }, "t1");

    t1.start();

    // 让线程t1运行50毫秒后,中断线程运行
    Thread.sleep(50);

    new Thread(() -> {
        // t1线程中断标识设置为true,等待县城自我中断
        t1.interrupt();
        System.out.println("------- t2 stop: " + true);
    }, "t2").start();
}

3. 中断API源码分析

  • 实例方法interrupt(),没有返回值
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}


//Thread.java
/* 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();  //---------------------------调用了c底层原生方法
private native void setNativeName(String name);
  • 实例方法 isInterrupted,返回布尔值
/**
 * Tests whether this thread has been interrupted.  The <i>interrupted
 * status</i> of the thread is unaffected by this method.
 *
 * <p>A thread interruption ignored because a thread was not alive
 * at the time of the interrupt will be reflected by this method
 * returning false.
 *
 * @return  <code>true</code> if this thread has been interrupted;
 *          <code>false</code> otherwise.
 * @see     #interrupted()
 * @revised 6.0
 */
public boolean isInterrupted() {
    return isInterrupted(false);
}

//Thread.java
private native boolean isInterrupted(boolean ClearInterrupted);//也调用了c底层

中断API相关说明:

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

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行(即:代码自行实现中断逻辑)。
  • 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态(中断状态将被清除),并抛出一个InterruptedException异常。( 关于这一点,interrupt() 方法的注释有明确的说明,)
  • 中断不活动的线程不会产生任何影响,具体看下面案例

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

答案:否, 仅仅设置了一个中断状态为true。

/**
 * 中断为true后,并不是立刻stop程序
 */
public static void m4() {
    //中断为true后,并不是立刻stop程序
    Thread t1 = new Thread(() -> {
        for (int i = 1; i <= 800; 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线程不在执行中,已经结束执行了, 此时调用 isInterrupted() 方法不起作用,中断状态标识位返回:false
    System.out.println("t1.interrupt()调用之后03: " + t1.isInterrupted());
}

运行结果:

t1.interrupt()调用之前,t1线程的中断标识默认值: false
------i: 1
------i: 2
------i: 3
......
------i: 645
t1.interrupt()调用之后01: true // ------此处中断标志位设置为了true,但是t1仍然在运行
------i: 646
......
------i: 798
------i: 799
------i: 800
t1.interrupt()调用之后02: true
t1.interrupt()调用之后03: false //中断不活动的线程不会产生任何影响,线程结束后应该是自动变为了false

3.2 中断异常代码案例:

当线程处于 waitjoinsleep 时,此时调用 interrupt() 方法,会触发 中断异常, 且会导致程序无限循环. 因为 InterruptedException,将会把中断状态清除。

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();
}

运行结果:

当调用 t1.interrupt(); 时,代码发生了异常,但 t1 依然在运行,没有结束掉

------hello Interrupt
------hello Interrupt
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.atguigu.juc.interrupt.InterruptDemo.lambda$m5$0(InterruptDemo.java:34)
	at java.lang.Thread.run(Thread.java:748)
------hello Interrupt
------hello Interrupt
------hello Interrupt
......

解决方法:

在 catch (InterruptedException e) 处添加 Thread.currentThread().interrupt(); 发生异常后,再次调用代码执行中断,程序发生异常后依然可以正常结束;

------hello Interrupt
------hello Interrupt
------hello Interrupt
------hello Interrupt
------hello Interrupt
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.atguigu.juc.interrupt.InterruptDemo.lambda$m5$0(InterruptDemo.java:34)
	at java.lang.Thread.run(Thread.java:748)
------hello Interrupt
-----isInterrupted() = true,程序结束。

3.3 对静态方法 Thread.interrupted() 的理解

public static boolean interrupted()

  • 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态这个方法做了两件事:

    • 1 返回当前线程的中断状态
    • 2 将当前线程的中断状态设为false

    注意:此方法连续调用两次的结果可能不一样

public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted()); // main---false
    System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted()); // main---false
    System.out.println("111111");
    Thread.currentThread().interrupt();///----false---> true
    System.out.println("222222");
    System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted()); // main---true
    System.out.println(Thread.currentThread().getName()+"---"+Thread.interrupted()); // main---false
}

运行结果:

main---false
main---false
111111
222222
main---true
main---false

对比 interrupted()isInterrupted() 源码

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

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

/**
 * Tests if some Thread has been interrupted.  The interrupted state
 * is reset or not based on the value of ClearInterrupted that is
 * passed.
 */
private native boolean isInterrupted(boolean ClearInterrupted);

他们在底层都调用了native方法 isInterrupted() , 只不过传入参数ClearInterrupted一个传参传了true,一个传了false

  • 静态方法interrupted() true表示清空当前中断状态。
  • 实例方法isInterrupted 则不会。

4. 总结

线程中断相关的方法:

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

    它通知目标线程中断,也就是设置目标线程的中断标志位为true,中断标志位表示当前线程已经被中断了。

  • isInterrupted() 方法也是一个实例方法

    它判断当前线程是否被中断(通过检查中断标志位)并获取中断标志

  • Thread类的静态方法 interrupted()

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

二、LockSupport是什么

LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语,其中 park()unpack() 而作用分别是阻塞线程和解除阻塞线程.

LockSupport

三、线程等待唤醒机制

1. 3种让线程等待和唤醒的方法

  • 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
  • 使用JUC包中Conditionawait()方法让线程等待,使用signal()方法唤醒线程
  • LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

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

正确使用 wait / notify

  • wait 和 notify 都必须在 synchronized 覆盖的代码块中
  • 必须先执行 wait, 再执行 notify
/**
 * 正确使用 wait / notify 实现线程的等待和唤醒
 *
 * @throws InterruptedException
 */
private static void normal() throws InterruptedException {
    new Thread(() -> {
        synchronized (object) {
            try {
                System.out.println(Thread.currentThread().getName() + " ---- t1释放锁");
                // wait方法会释放锁,给其他线程获取锁的机会
                object.wait();
                System.out.println(Thread.currentThread().getName() + " ---- t1重新获取锁");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }, "t1").start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "---- notify");
            object.notify(); // notify执行,让 t1 线程重新获取锁
        }
    }, "t2").start();
}

运行结果:

t1 ---- t1释放锁
t2---- notify
t1 ---- t1重新获取锁

错误使用—: 去掉 synchronized

/**
 * 错误方式—: 去掉 synchronized
 *
 * @throws InterruptedException
 */
private static void error1() throws InterruptedException {
    new Thread(() -> {
        try {
            System.out.println(Thread.currentThread().getName() + " ---- t1释放锁");
            // wait方法会释放锁,给其他线程获取锁的机会
            object.wait();
            System.out.println(Thread.currentThread().getName() + " ---- t1重新获取锁");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }, "t1").start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "---- notify");
        object.notify(); // notify执行,让 t1 线程重新获取锁
    }, "t2").start();
}

运行结果:

t1 ---- t1释放锁
Exception in thread "t1" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.atguigu.juc.interrupt.SynchronizedDemo.lambda$error1$2(SynchronizedDemo.java:59)
	at java.lang.Thread.run(Thread.java:748)
t2---- notify
Exception in thread "t2" java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at com.atguigu.juc.interrupt.SynchronizedDemo.lambda$error1$3(SynchronizedDemo.java:70)
	at java.lang.Thread.run(Thread.java:748)

错误使用二:把notify和wait的执行顺序对换

/**
 * 错误使用二:把notify和wait的执行顺序对换
 *
 * @throws InterruptedException
 */
private static void error2() throws InterruptedException {
    new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(2); // t1 线程先停2秒,等待t2线程执行结束
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        synchronized (object) {
            try {
                System.out.println(Thread.currentThread().getName() + " ---- t1释放锁");
                // wait方法会释放锁,给其他线程获取锁的机会
                object.wait();
                System.out.println(Thread.currentThread().getName() + " ---- t1重新获取锁");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }, "t1").start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        synchronized (object) {
            System.out.println(Thread.currentThread().getName() + "---- notify");
            object.notify(); // notify执行,让 t1 线程重新获取锁
        }
    }, "t2").start();
}

运行结果:

t2---- notify
t1 ---- t1释放锁

现象:t1 线程没有结束掉。

总结:

  • wait和notify方法必须要在同步块或者方法里面,且必须成对出现使用
  • 先wait后notify才可以,顺序不能错

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

正确使用 await / signal

  • await 和 signal 都必须在 lock() / unlock() 覆盖的代码块中
  • 必须先执行 await , 再执行 signal
/**
 * 正确使用 Lock 的 await / signal 实现对线程的等待和唤醒
 * @throws InterruptedException
 */
private static void normal() throws InterruptedException {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    new Thread(() -> {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "---- t1 释放锁");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "---- t1 重新获取锁");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }, "t1").start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "---- t2唤醒t1线程");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }, "t2").start();
}

运行结果:

t1---- t1 释放锁
t2---- t2唤醒t1线程
t1---- t1 重新获取锁

错误使用一:await 和 signal 不在 lock() / unlock() 覆盖的代码块中

报错: java.lang.IllegalMonitorStateException

错误使用二:必须先执行 signal , 再执行 await

线程没有正确的停止

4. Object和Condition使用的限制条件

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

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

5.1 概念解析

LockSupport 通过 park()unpark(thread) 方法来实现阻塞和唤醒线程的操作

官网解释

LockSupport

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

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

5.2 主要方法

5.2.1 API

LockSupport API

5.2.2 阻塞

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

调用LockSupport.park()时,发现它调用了unsafe类,并且默认传了一个0

public static void park() {
    UNSAFE.park(false, 0L);
}
  • permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为零并返回。

5.2.3 唤醒

unpark(Thread thread) , 唤醒处于阻塞状态的指定线程

/**
 * Makes available the permit for the given thread, if it
 * was not already available.  If the thread was blocked on
 * {@code park} then it will unblock.  Otherwise, its next call
 * to {@code park} is guaranteed not to block. This operation
 * is not guaranteed to have any effect at all if the given
 * thread has not been started.
 *
 * @param thread the thread to unpark, or {@code null}, in which case
 *        this operation has no effect
 */
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

调用LockSupport.unpark();时,也调用了unsafe类

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

5.3 LockSupport 代码示例

正常+无锁块要求

/**
 * 正常使用,无锁块要求
 * 先阻塞后释放,可以实现线程的阻塞和唤醒
 *
 * @throws InterruptedException
 */
private static void m1() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "---- come in");
        System.out.println(Thread.currentThread().getName() + "---- park阻塞");
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "---- 继续执行");
    }, "t1");
    t1.start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "---- come in");
        LockSupport.unpark(t1);
    }, "t2").start();
}

运行结果:

t1---- come in
t1---- park阻塞
t2---- come in
t1---- 继续执行

先唤醒后等待,LockSupport 同样支持

/**
 * 先释放后阻塞,也可以实现线程的阻塞和唤醒
 *
 * @throws InterruptedException
 */
private static void m2() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "---- come in");
        System.out.println(Thread.currentThread().getName() + "---- park阻塞");
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "---- 继续执行");
    }, "t1");
    t1.start();

    TimeUnit.SECONDS.sleep(1);

    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "---- come in");
        System.out.println(Thread.currentThread().getName() + "---- 给t1线程发通行证");
        LockSupport.unpark(t1);
    }, "t2").start();
}

运行结果:

t2---- come in
t2---- 给t1线程发通行证
t1---- come in
t1---- park阻塞
t1---- 继续执行

t1 sleep方法2秒后醒来,执行park无效,没有阻塞效果,这是因为 t2 先执行了unpark(t1)导致上面的park方法无效.

注意:park() / unpark() 必须成对出现

错误使用:连续执行 unpark(), 相当于只执行了一次 unpark()

/**
 * 连续执行 unpark(), 相当于只执行了一次 unpark()
 */
private static void error1() {
    Thread t1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "\t" + "---come in");
        System.out.println(Thread.currentThread().getName() + "\t" + "--- park1");
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "\t" + "--- park2");
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "\t" + "---被唤醒");
    }, "t1");
    t1.start();

    new Thread(() -> {
        LockSupport.unpark(t1);
        LockSupport.unpark(t1);
        System.out.println(Thread.currentThread().getName() + "\t" + "--- unpark1");
        System.out.println(Thread.currentThread().getName() + "\t" + "--- unpark2");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "t2").start();
}

运行结果:

t1	---come in
t1	--- park1
t2	--- unpark1
t2	--- unpark2
t1	--- park2

t1 没有停止,继续阻塞,这是因为 t2 连续执行了两次 unpark(t1) ,但其实只发布了一个通行证,导致 t1 的第一次 park() 被唤醒了,但是第二次 park() 就没有通行证用来唤醒它,因此 t1 阻塞。

总结:

  • Lock Support是用来创建锁和其他同步类的基本线程阻塞原语。
  • Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。归根结底,Lock Support调用的Unsafe中的native代码。
  • Lock Support提供park()unpark() 方法实现阻塞线程解除线程阻塞的过程
  • Lock Support和每个使用它的线程都有一个许可(permit) 关联。
  • 每个线程都有一个相关的 permit, permit 最多只有一个, 重复调用 unpark() 也不会积累凭证。

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 george_95@126.com