JUC学习之LockSupport和线程中断

116 阅读13分钟

1、线程中断

1.1 线程中断的定义

一个线程不应该由其他线程来强制中断或者停止,而是应该由线程自己自行停止。所以,很多的方法已经被废弃了,如Thread.stopThread。suspendThread.resume。 在Java中没有办法立即停止一个线程,然而停止线程很重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制----中断,也即中断标识协商机制。

中断是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全由程序员自己实现。 若要中断一个线程,需要手动调用该线程的interrupt方法,该方法仅仅是将线程对象的中断标识设为true,接着手动写代码不断检测当前线程的标识位。

1.2 线程中断的三个API

API签名说明
public void interrupt()实例方法:仅仅是设置线程的中断状态为true,发起协商而不是立即中断
public static boolean interrupted()判断线程是否被中断并清除当前中断状态,一共做了两件事:返回线程的中断状态,判断是否已被中断;将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
public boolean isInterrupted()判断当前线程是否被中断(检测中断标志位的值)

1.3 面试题中中断机制的考点

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

① volatile关键字

通过使用volatile关键字,让两个线程共用一个标志位isStop,第一个线程使用这个标志位用于检测判断是否需要中断,第二个线程通过对isStop标志位进行设置来协商让第一个线程中断。 以下方法的检测是将t1线程通过if(isStop)来判断是否中断,t2线程通过isStop=true对其进行协商中断

static volatile boolean isStop = false;  
private static void UsingVolatileToInterruptedThread() {  
    new Thread(() -> {  
        while (true) {  
            if (isStop) {  
                System.out.println("t1 \t isStop标志位被修改为true,线程中断");  
                break;            
            }  
            System.out.println("t1 ----- Using volatile to Interrupted");  
  
        }  
    },"t1").start();  
  
    try {  
        TimeUnit.MILLISECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
  
    new Thread(() -> {  
        isStop = true;  
    },"t2").start();  
}
② AtomicBoolean原子类

通过使用AtomicBoolean原子类,让两个线程共用一个AtomicBoolean,第一个线程使用这个标志位用于检测判断是否需要中断,第二个线程通过对AtomicBoolean标志位进行设置,即调用atomicBoolean.set(true)方法让第一个线程中断。
以下方法的检测是将t1线程通过atomicBoolean.get()来判断是否中断,t2线程通过atomicBoolean.set(true);对其进行协商中断。

static AtomicBoolean atomicBoolean = new AtomicBoolean(false);  
private static void UsingAtomicBooleanToInterruptedThread() {  
    new Thread(() -> {  
        while (true) {  
            if (atomicBoolean.get()) {  
                System.out.println("t1 \t atomicBoolean被修改为true,线程中断");  
                break;            
            }  
            System.out.println("t1 ----- Using AtomicBoolean to Interrupted");  
  
        }  
    },"t1").start();  
  
    try {  
        TimeUnit.MILLISECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
  
    new Thread(() -> {  
        atomicBoolean.set(true);  
    },"t2").start();  
}
③ Thread内部的Api

在前面的1.2 线程中断的三个API中介绍到的三个Api来使线程进行中断。

Ⅰ、代码案例测试

通过使用Thread内置的Api,第一个线程通过使用Thread.currentThread().isInterrupted()用于检测判断是否需要中断。第二个线程通过对Thread.currentThread().isInterrupted()标志位进行设置,即调用interrupt()方法让第一个线程中断;也可以在运行的过程中第一个线程觉得自己需要中断了,因此自己调用了interrupt()方法中断。
以下方法的检测是将t1线程通过Thread.currentThread().isInterrupted()来判断是否需要中断
2线程通过t1.interrupt()方对其进行协商中断,也可以是t1线程自身调用t1.interrupt()来进行中断。

private static void UsingThreadApiToInterruptedThread() {  
    Thread t1 = new Thread(() -> {  
        while (true) {  
            if (Thread.currentThread().isInterrupted()) {  
                System.out.println("t1 \t isInterrupted()被修改为true,线程中断");  
                break;           
            }  
            System.out.println("t1--Using Thread Interrupted Api to Interrupted");  
        }  
    }, "t1");  
    t1.start();  
  
    try {  
        TimeUnit.MILLISECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
  
    // 通过其他线程来使得t1线程中断  
    /*new Thread(() -> {  
        t1.interrupt();    
    },"t2").start();*/  
    // t1线程自己中断  
    t1.interrupt();  
}
Ⅱ、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();  
}

private native void interrupt0();
Ⅲ、isInterrupted()方法的源码分析
public boolean isInterrupted() {  
    return isInterrupted(false);  
}

private native boolean isInterrupted(boolean ClearInterrupted);

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

当对一个线程调用interrupt()时

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响。
  • 如果线程处于被阻塞状态,在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常
① 正常活动状态的线程

现有如下的代码,t1线程循环执行,经过2ms后,t2线程与t1线程协商

private static void ActiveThreadIsInterrupted() {  
    Thread t1 = new Thread(() -> {  
        for (int i = 0; i < 1000; i++) {  
            System.out.println("------" + i);  
        }  
        System.out.println("2将t1线程的中断标志位为:" + Thread.currentThread().isInterrupted());  
    }, "t1");  
    t1.start();  
    System.out.println("t1线程的中断标志位默认为:" + t1.isInterrupted());  
  
    try {  
        TimeUnit.MILLISECONDS.sleep(2);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    new Thread(() -> {  
        t1.interrupt();  
        System.out.println("将t1线程的中断标志位设置为:" + t1.isInterrupted());  
    },"t2").start();  
  
    try {  
        TimeUnit.MILLISECONDS.sleep(5);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    System.out.println("1将t1线程的中断标志位为:" + t1.isInterrupted());  
}

此时运行的结果为:

t1线程的中断标志位默认为:false
------0
------1
------2
------3
...
------279
------280
将t1线程的中断标志位设置为:true
------281
------282
...
------878
------879
1将t1线程的中断标志位为:true
------880
------881
...
------999
2将t1线程的中断标志位为:true
② 被阻塞状态的线程
private static void SleepThreadIsInterrupted() {  
    Thread t1 = new Thread(() -> {  
        while (true) {  
            if (Thread.currentThread().isInterrupted()) {  
                System.out.println("t1 \t 中断标志位:" + Thread.currentThread().isInterrupted() + "程序终止");  
                break;            
            }  
            try {  
                Thread.sleep(200);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println("----- hello ");  
        }  
    }, "t1");  
    t1.start();  
  
    try {  
        TimeUnit.SECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
  
    new Thread(() -> t1.interrupt(), "t2").start();  
}

此时运行后的结果为:

----- hello 
----- hello 
----- hello 
----- hello 
java.lang.InterruptedException: sleep interrupted
----- hello 
----- hello 
----- hello 
----- hello 
...

发现,报了异常之后,程序仍然在继续执行。 接着,我们在线程进入睡眠状态后的catch代码块中再次添加打断信号。

try {  
    Thread.sleep(200);  
} catch (InterruptedException e) {  
    e.printStackTrace();  
    Thread.currentThread().interrupt();  
}

此时运行的结果为:

----- hello 
----- hello 
----- hello 
----- hello 
java.lang.InterruptedException: sleep interrupted
----- hello 
t1 	 中断标志位:true 程序终止

Process finished with exit code 0

可以发现,虽然仍然是抛出了异常,但是程序正常执行完毕了,而不像刚刚,抛了异常,但是程序却仍在运行。 这是为什么呢? 在jdk中有关interrupt()方法的介绍如下:

中断此线程。

除非当前线程正在中断(始终允许),否则将调用此线程的checkAccess方法,这可能会导致抛出SecurityException

如果该线程阻塞的调用wait()wait(long)wait(long, int)的方法Object类,或的join()join(long)join(long, int)sleep(long)sleep(long, int),这个类的方法,那么它的中断状态将被清除,并且将收到InterruptedException

如果在InterruptibleChannel上的I/O操作中阻止该线程,则通道将关闭,线程的中断状态将被设置,线程将收到ClosedByInterruptException

如果该线程在Selector中被阻塞,则线程的中断状态将被设置,它将立即从选择操作返回,可能具有非零值,就像调用选择器的wakeup方法一样。

如果以前的条件都不成立,则将设置该线程的中断状态。

中断不活动的线程不会产生任何影响。

简单而言就是,当线程因调用wait()、join()、sleep()及对应的重载方法时,如果此时自己或其他线程调用了interrupt()方法让该线程进入阻塞状态,那么此时该次interrupt()无效(即此时的标志位的值仍然为false),且会抛出一个InterruptedException异常,同时,会让该线程从阻塞状态恢复运行状态

所以,在第一个代码演示中,线程t2中调用了t1.interrupt()时,会抛出异常,但是程序并没有终止。而在第二个代码演示中,当抛出异常之后,我们在catch(){}代码块中又添加了一次interrupt()方法,虽然这一次是自身调用的,但是效果也是一样。因为t2线程调用时已经将t1线程唤醒,因此此时再次调用时,便可以正常的将标志位的值置为true,而导致程序的终止。

1.3.3 谈谈你对静态方法Thread.interrupted()的理解

当有线程调用静态方法interrupted()时,会返回当前线程的中断状态位,并且将线程的中断状态位设置为false。其底层与isInterrupted()方法一样,都是调用了isInterrupted(boolean ClearInterrupted)方法,而区别在于interrupted()方法传入的是true,而isInterrupted()传入的是false。 代码测试案例:

private static void testStaticInterruptedMethod() {  
    System.out.println("t1 \t" + Thread.interrupted());  
    System.out.println("t1 \t" + Thread.interrupted());  
    System.out.println("------1");  
    
    Thread.currentThread().interrupt();  
    
    System.out.println("------2");  
    System.out.println("t1 \t" + Thread.interrupted());  
    System.out.println("t1 \t" + Thread.interrupted());  
}

输出的结果为:

main	false
main	false
------1
------2
main	true
main	false

可以看到,一开始没有对main线程进行中断时,两次调用的结果都是false,main线程自己中断之后,再次调用时,第一次返回的是true,但第二次返回的是false

2、线程等待唤醒机制

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

2.1.1 Object类中的wait和notify方法

① 代码测试
Ⅰ、正常运行
private static void waitAndNotifyToThread() {  
    Object o = new Object();  
  
    new Thread(() -> {  
        synchronized (o) {  
            System.out.println("---------- t1 come in");  
  
            try {  
                o.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println("---------- t1 被唤醒");  
        }  
    },"t1").start();  
    
	// 线程暂停1秒钟,确保t1线程先启动  
    try {  
        TimeUnit.SECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
  
    new Thread(() -> {  
        synchronized (o) {  
            o.notify();  
            System.out.println("---------- t2发出通知唤醒想持有o对象锁的阻塞线程");  
        }  
    },"t2").start();  
}

此时的运行结果为:

---------- t1 come in
---------- t2发出通知唤醒想持有o对象锁的阻塞线程
---------- t1 被唤醒

程序正常运行,也正常终止了。

Ⅱ、去掉同步代码块
private static void waitAndNotifyToThreadWithoutSynchronized() {  
    Object o = new Object();  
  
    new Thread(() -> {  
            System.out.println("---------- t1 come in");  
  
            try {  
                o.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println("---------- t1 被唤醒");  
    },"t1").start();  
  
    // 线程暂停1秒钟,确保t1线程先启动  
    try {  
        TimeUnit.SECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
  
    new Thread(() -> {  
            o.notify();  
            System.out.println("t2发出通知唤醒想持有o对象锁的阻塞线程");  
    },"t2").start();  
}

此时的运行结果为:

---------- t1 come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
	
Exception in thread "t2" java.lang.IllegalMonitorStateException

会报异常信息,抛出IllegalMonitorStateException异常。

Ⅲ、先notify再wait
private static void notifyBeforeWaitToThread() {  
    Object o = new Object();  
  
    new Thread(() -> {  
        try {  
            TimeUnit.SECONDS.sleep(1);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        synchronized (o) {  
            System.out.println("---------- t1 come in");  
  
            try {  
                o.wait();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println("---------- t1 被唤醒");  
        }  
    },"t1").start();  
  
    new Thread(() -> {  
        synchronized (o) {  
            o.notify();  
            System.out.println("---------- t2发出通知唤醒想持有o对象锁的阻塞线程");  
        }  
    },"t2").start();  
}

此时的运行结果为:

---------- t2发出通知唤醒想持有o对象锁的阻塞线程
---------- t1 come in

但是还有一条语句没有打印:---------- t1 被唤醒
同时,程序并没有终止。

② 小结
  • waitnotify都需要在synchronized代码块中使用,否则会报IllegalMonitorStateException异常。
  • wait必须在notify之前使用,否则wait线程不能正常被唤醒

2.1.2 Condition接口中的await和signal方法

① 代码测试
Ⅰ、正常运行
private static void awaitAndSignalToThread() {  
    Lock lock = new ReentrantLock();  
    Condition condition = lock.newCondition();  
  
    new Thread(() -> {  
        lock.lock();  
        try {  
            System.out.println("-------- t1 come in");  
            condition.await();  
            System.out.println("-------- t1被唤醒");  
        } 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();  
            System.out.println("-------- t2发出唤醒通知");  
        } finally {  
            lock.unlock();  
        }  
    },"t2").start();  
}

此时的运行结果为:

-------- t1 come in
-------- t2发出唤醒通知
-------- t1被唤醒

可以看到,在这种情况下是正常运行的。

Ⅱ、没有lock锁
private static void awaitAndSignalWithoutLock() {  
    Lock lock = new ReentrantLock();  
    Condition condition = lock.newCondition();  
  
    new Thread(() -> {  
        try {  
            System.out.println("-------- t1 come in");  
            condition.await();  
            System.out.println("-------- t1被唤醒");  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
        }  
    },"t1").start();  
  
    try {  
        TimeUnit.SECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
  
    new Thread(() -> {  
        try {  
            condition.signal();  
            System.out.println("-------- t2发出唤醒通知");  
        } finally {  
        }  
    },"t2").start();  
}

此时的运行结果为:

-------- t1 come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
Exception in thread "t2" java.lang.IllegalMonitorStateException

可以看到,此时仍然同Object类中的waitnotify方法一样会报IllegalMonitorStateException异常信息。

Ⅲ、先signal再await
private static void signalBeforeAwaitToThread() {  
    Lock lock = new ReentrantLock();  
    Condition condition = lock.newCondition();  
  
    new Thread(() -> {  
        try {  
            TimeUnit.SECONDS.sleep(1);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        lock.lock();  
  
        try {  
            System.out.println("-------- t1 come in");  
            condition.await();  
            System.out.println("-------- t1被唤醒");  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        } finally {  
            lock.unlock();  
        }  
    },"t1").start();  
  
  
    new Thread(() -> {  
        lock.lock();  
        try {  
            condition.signal();  
            System.out.println("-------- t2发出唤醒通知");  
        } finally {  
            lock.unlock();  
        }  
    },"t2").start();  
}

此时的运行结果为:

-------- t2发出唤醒通知
-------- t1 come in

程序不能正常执行

② 小结
  • awaitsignal都需要在lock锁的基础上去使用
  • signal必须跟在await的后面,否则,不能正常唤醒线程

2.1.3 LockSupport中的park等待和unpark唤醒

① LockSupport是什么

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。 LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),但与Semaphore不同的是,许可的累加上限是1。 此类与使用它的每个线程关联一个许可证(在Semaphore类的意义上)。如果许可证可用,将立即返回park,并在此过程中消耗;否则可能会阻止。如果许可证尚不可用,则致电unpark获得许可证。(与Semaphore不同,许可证不会累积。最多有一个。)

② LockSupport的主要方法
Ⅰ、阻塞

park() / park(Object locker),阻塞当前线程或者是阻塞传入的具体线程。

/**
 * 出于线程调度目的禁用当前线程,除非允许可用。
 * 如果许可证可用,则将其使用,呼叫立即返回;否则,当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一:
 *    其他一些线程以当前线程作为目标进行调用 unpark ;或者
 *    其他线程 中断 当前线程;或
 *    虚假调用(即无缘无故)返回。
 * 此方法 不会 报告哪些导致该方法返回。调用方应重新检查导致线程首先停放的条件。例如,调用方还可以确定线程在返回时的中断状态。
*/
public static void park(Object blocker) {  
    Thread t = Thread.currentThread();  
    setBlocker(t, blocker);  
    UNSAFE.park(false, 0L);  
    setBlocker(t, null);  
}

public static void park() {  
    UNSAFE.park(false, 0L);  
}
Ⅱ、唤醒

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

// 为给定线程提供许可证(如果尚不可用)。如果线程被阻塞,那么它将取消阻塞 park 。否则,保证其下一次调用 park 不会阻塞。如果给定线程尚未启动,则不保证此操作有任何效果。
public static void unpark(Thread thread) {  
    if (thread != null)  
        UNSAFE.unpark(thread);  
}
③ 代码测试
Ⅰ、正常运行
private static void lockSupportParkAndUnparkToThread() {  
    Thread t1 = new Thread(() -> {  
        System.out.println("--------------- t1线程 come in");  
        LockSupport.park();  
        System.out.println("--------------- t1线程 被唤醒");  
    }, "t1");  
    t1.start();  
  
    try {  
        TimeUnit.SECONDS.sleep(1);  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }  
  
    new Thread(() -> {  
        LockSupport.unpark(t1);  
        System.out.println("--------------- t2线程发出唤醒通知");  
    },"t2").start();  
}

此时运行的结果为:

--------------- t1线程 come in
--------------- t2线程发出唤醒通知
--------------- t1线程 被唤醒

可以发现,park()unpark()方法是不需要在锁块中使用的,可以直接使用,而且代码看上去会更简洁。

Ⅱ、先unpark再park
private static void lockSupportUnparkBeforePark() {  
    Thread t1 = new Thread(() -> {  
        try {  
            TimeUnit.SECONDS.sleep(1);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("--------------- t1线程 come in " + System.currentTimeMillis());  
        LockSupport.park();  
        System.out.println("--------------- t1线程 被唤醒 " + System.currentTimeMillis());  
    }, "t1");  
    t1.start();  
  
  
    new Thread(() -> {  
        LockSupport.unpark(t1);  
        System.out.println("--------------- t2线程发出唤醒通知");  
    },"t2").start();  
}

此时的运行结果为:

--------------- t2线程发出唤醒通知
--------------- t1线程 come in 1684032061621
--------------- t1线程 被唤醒 1684032061621

后面的数字是执行该代码时系统当前的时间 此时可以发现,程序正常运行了,而且t1线程的两条代码的执行时间一致,说明t2线程先unpark()之后,t1线程的park()已经不生效了。 上述的两点都可以发现,LockSupport比前面的Object和Condition都要好。

Ⅲ、多个unpark和park

同样是先执行unpark再执行park

private static void parksAndUnparks() {  
    Thread t1 = new Thread(() -> {  
        try {  
            TimeUnit.SECONDS.sleep(1);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("--------------- t1线程 come in " + System.currentTimeMillis());  
        LockSupport.park();  
        LockSupport.park();  
        LockSupport.park();  
        System.out.println("--------------- t1线程 被唤醒 " + System.currentTimeMillis());  
    }, "t1");  
    t1.start();  
  
  
  
    new Thread(() -> {  
        LockSupport.unpark(t1);  
        LockSupport.unpark(t1);  
        LockSupport.unpark(t1);  
        System.out.println("--------------- t2线程发出唤醒通知");  
    },"t2").start();  
}

此时运行的结果为:

--------------- t2线程发出唤醒通知
--------------- t1线程 come in 1684033025761

可以发现,此时的t1线程不能正常执行,不会被正常唤醒。 这是因为,unpark()在发通行证时,只会发一个,不会累加,因此同时多次调用unpark()和只调用一次其实都是一样的,都只有一个通行证。因此在t1线程中去调用park()的时候,第一个park()将通行证消耗了之后,其他的park()就需要重新等待通行证,因此程序就不能正常执行了。