面试中最常被虐的地方必定有并发编程这块知识点,不管你是刚刚入门的大四萌新仍是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程当中的笔记和总结,并提供调试代码供你们玩耍java
1.sleep对象的monitor lock有释放吗?如何证实?git
2.sleep(0)是什么意思?github
3.线程如何正常关闭?面试
4.join内部是经过那个函数来实现顺序执行的?编程
请自行回顾以上问题,若是还有疑问的自行回顾上一章哦~bash
本章内容原本是要和上一章一块儿讲的,可是因为篇幅过长涉及到的内容较多,而且也有不少实战代码的调试,因此决定仍是另开一章来说比较好一些。本章主要介绍interrupt
这个API。学习完成,同窗们会对interrupt
有深刻了解,明白其内部的处理机制,同时对begin
和end
也有较为深入的认识。并发
本章代码下载less
在本章中咱们一共要讲解与interrupt
相关的三个API方法,这个API我学的时候也是费了很多劲,因此相对来讲会多写一点,同窗们先泡杯茶慢慢看呀~😄异步
老套路,直接上API接口说明socket
* @revised 6.0
* @spec JSR-51
*/
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();
}
复制代码
这么长篇幅的介绍莫怕,咱们一块儿来看这货到底在说什么鸟语。
首先,第一段句归纳了这个接口的做用中断此线程
* Interrupts this thread.
复制代码
第二段
//除非当前线程本身把本身中断了,不然的话会一直容许checkAccess()调用
//而且在调用的时候可能还会抛出SecurityException异常
* <p> Unless the current thread is interrupting itself, which is
* always permitted, the {@link #checkAccess() checkAccess} method
* of this thread is invoked, which may cause a {@link
* SecurityException} to be thrown.
复制代码
这个checkAccess()
咱们这边不作深刻探讨,咱们只要知道这是一个校验接口,打个比方,好比在中断JVM自身线程的时候,它就会去查询是否有足够权限来执行这个命令
第三段
含义就是线程在调用下列方法以后能够调用interrupt
来中断状态
*Object的wait方法
*Object的wait(long)方法
*Object的wait(long,int)方法
*Thread的sleep(long)方法
*Thread的sleep(long,int)方法
*Thread的join方法
*Thread的join(long)方法
*Thread的join(long,int)方法
*InterruptibleChannel的io操做
*Selector的wakeup方法
*其余方法
第四段
//在线程因为I/O操做而致使阻塞的时候,阻塞通道会被关闭,而后线程中断标志被设置
//为true而且同时会收到ClosedByInterruptException异常
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt * status will be set, and the thread will receive a {@link * java.nio.channels.ClosedByInterruptException}. 复制代码
这部分的内容后面会用一些篇幅来进一步说明,这里就不展开来说。
第五段
//当前线程因为调用Selector选择器方法而阻塞的时候,线程中断标志会被设置为true
//而且当前线程会立马返回
* <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return * immediately from the selection operation, possibly with a non-zero * value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.
复制代码
最后一段
//中断一个不活跃的线程不会产生任何结果
Interrupting a thread that is not alive need not have any effect.
复制代码
好到这里你们应该已经等不及上手一试了,咱们先来一个常规的中断sleep
的操做
private static class MySleep implements Runnable {
@Override
public void run() {
System.out.println("我开始睡觉了,谁也别来吵我");
try {
TimeUnit.HOURS.sleep(12);
} catch (InterruptedException e) {
System.out.println("你代码炸啦!!!");
e.printStackTrace();
}
System.out.println("起床");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MySleep());
thread.start();
//调用中断方法
thread.interrupt();
}
复制代码
输出:
我开始睡觉了,谁也别来吵我
你代码炸啦!!!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at src.com.lyf.page3.InterruptSleep$MySleep.run(InterruptSleep.java:19)
at java.lang.Thread.run(Thread.java:748)
起床
复制代码
能够看到这边咱们打算睡它个12小时,可是好景不长,因为代码炸了,咱们只能立马起床干活。好的这是一个悲伤的故事,但愿你们都不要遇到😂。
这边咱们注意到,在调用interrupt
中断了sleep
以后,咱们的线程接收到了一个java.lang.InterruptedException
异常,这里须要咱们对异常作友好处理。不管你是try起来仍是直接往外抛,可是捕获必定要作好,否则咱们对日志会出现一堆java.lang.InterruptedException
异常。
以上咱们已经基本学会了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();
}
复制代码
if (this != Thread.currentThread())
checkAccess();
复制代码
判断当前中断的线程是否是本身,不少同窗这里可能会有疑问,this
和Thread.currentThread()
难道指向的不是同一个对象吗?
咱们用测试代码来试验一下
private static class MySleep implements Runnable {
@Override
public void run() {
System.out.println("下班啦,我要睡觉了,谁也别来吵我");
try {
TimeUnit.HOURS.sleep(12);
} catch (InterruptedException e) {
System.out.println("你代码炸啦!!!");
// e.printStackTrace();
}
System.out.println("起床");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
Thread t2 = new Thread(new MySleep(), "一号");
t2.start();
t2.interrupt();
}, "二号");
t1.start();
}
复制代码
打断点咱们能够看到结果
checkAccess()
方法来进行校验。
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
复制代码
无论是调用sleep
仍是wait
咱们都会发现始终不会进入到这块到逻辑代码里面,缘由就是blocker
是null并无被设值。那么blocker
究竟是何时被设值到呢?
⚠️方法论来了
当接口内部有部分代码调试不到或者代码没法理解当时候,咱们能够回过头去看接口说明。
这里咱们再来看下这段说明
//在线程因为I/O操做而致使阻塞的时候,阻塞通道会被关闭,而后线程中断标志被设置
//为true而且同时会收到ClosedByInterruptException异常
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt * status will be set, and the thread will receive a {@link * java.nio.channels.ClosedByInterruptException}. 复制代码
这里咱们给同窗们提供一个案例来调试这块代码,代码内容以下:
//src.com.lyf.page3.InterruptNIO
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
InetSocketAddress isa = new InetSocketAddress("localhost", 8080);
SocketChannel sc1 = SocketChannel.open(isa);
SocketChannel sc2 = SocketChannel.open(isa);
Thread t1 = new Thread(new NIOBlockExample(sc2), "一号");
t1.start();
Thread t2 = new Thread(new NIOBlockExample(sc1), "二号");
t2.start();
//打断标志
t1.interrupt();
t2.interrupt();
}
复制代码
//src.com.lyf.page3.NIOBlockExample
private final SocketChannel socketChannel;
public NIOBlockExample(SocketChannel sc) {
this.socketChannel = sc;
}
@Override
public void run() {
try {
System.out.println("Waiting for read() in " + this);
socketChannel.read(ByteBuffer.allocate(1));
} catch (ClosedByInterruptException e) {
e.printStackTrace();
System.out.println("ClosedByInterruptException");
} catch (AsynchronousCloseException e) {
e.printStackTrace();
System.out.println("AsynchronousCloseException");
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
System.out.println("Exiting NIOBlocked.run() " + this);
}
复制代码
这里代码比较多建议你们对照src.com.lyf.page3.InterruptNIO
和src.com.lyf.page3.NIOBlockExample
一边调试一边喝茶来细品下面这块内容。
首先咱们建立一个NIOBlockExample线程对象,经过调用SocketChannel.read
来模拟可能产生I/O阻塞的场景,这里同窗们不须要过于关注SocketChannel.read
这个方法,咱们这边只须要知道这是一个读操做就行,这里咱们的目的是为了模拟I/O阻塞场景来调试interrupt
代码。
ServerSocket server = new ServerSocket(8080);
InetSocketAddress isa = new InetSocketAddress("localhost", 8080);
复制代码
模拟开启8080端口,而后配置套接字通道信息为本地的8080端口。
SocketChannel sc1 = SocketChannel.open(isa);
SocketChannel sc2 = SocketChannel.open(isa);
Thread t1 = new Thread(new NIOBlockExample(sc2), "一号");
t1.start();
Thread t2 = new Thread(new NIOBlockExample(sc1), "二号");
t2.start();
复制代码
//打断标志
t1.interrupt();
t2.interrupt();
复制代码
断点打在if判断条件上,咱们能够看到blocker指向的是一个AbstractInterruptibleChannel
抽象类对象,那么咱们初步能够判定,应该是AbstractInterruptibleChannel
中的某个相似的set方法来完成对blocker
对赋值工做的
java.nio.channels.spi.AbstractInterruptibleChannel
,同窗们能够经过
老套路先看他一波API说明
//可中断通道的基本实现类
/**
* Base implementation class for interruptible channels.
*
//方法在可能出现I/O阻塞的状况下必须先调用begin,而后再调用end,而且是须要在一
//个try{}finally{}里面来完成这些通道的开启和关闭
* <p> This class encapsulates the low-level machinery required to implement
* the asynchronous closing and interruption of channels. A concrete channel
* class must invoke the {@link #begin begin} and {@link #end end} methods
* before and after, respectively, invoking an I/O operation that might block
* indefinitely. In order to ensure that the {@link #end end} method is always
* invoked, these methods should be used within a
* <tt>try</tt> ... <tt>finally</tt> block:
复制代码
这里咱们了解到这个抽象类是一个可中断通道的基本实现类
,而且API说明里面也给咱们提供了调用的代码模版
* boolean completed = false;
* try {
* begin();
* completed = ...; // 执行阻塞I/O操做
* return ...; // 返回结果
* } finally {
* end(completed);
复制代码
在这里咱们能够看到以前咱们的SocketChannel.read
对应这边的应该是执行阻塞I/O操做,也就是说,在咱们中断方法以前,该类已经调用了begin
方法了。同窗们这里可能会感受很奇怪,他是怎么作到的。咱们来看下SocketChannel
的继承关系就一目了然了
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
复制代码
能够看到SocketChannel
继承了AbstractSelectableChannel
抽象类,这样一切都瓜熟蒂落了。
接下来咱们再来看下begin
究竟是何方神圣
/**
//标记一个可能发生I/O阻塞的操做的开始
* Marks the beginning of an I/O operation that might block indefinitely.
*
//该方法须要和end方法同步调用,为了实现异步阻塞通道的关闭和中断
* <p> This method should be invoked in tandem with the {@link #end end}
* method, using a <tt>try</tt> ... <tt>finally</tt> block as
* shown <a href="#be">above</a>, in order to implement asynchronous
* closing and interruption for this channel. </p>
*/
protected final void begin() {
if (interruptor == null) {
interruptor = new Interruptible() {
public void interrupt(Thread target) {
synchronized (closeLock) {
if (!open)
return;
open = false;
interrupted = target;
try {
AbstractInterruptibleChannel.this.implCloseChannel();
} catch (IOException x) { }
}
}};
}
blockedOn(interruptor);
Thread me = Thread.currentThread();
if (me.isInterrupted())
interruptor.interrupt(me);
}
复制代码
断点打在同步块入口处
interrupt
甚至尚未开始打标志,可是这时候咱们发现
blocker
已经被赋值了,这里就证实了,在阻塞I/O操做以前会初始化
blocker状态
。也就是说在尚未调用
interrupt
以前就已经调用了
begin
。咱们这时候断点打在begin,一探究竟。
begin
入口处打断点,发现这边发掉调用的是咱们最初的
open
方法,而且咱们也看到这边发起调用的是咱们的主程序
main
函数
在open函数调用的时候会初始化一个new Interruptible()对象,而且是由parent Thread 来建立的。
⚠️这里有个巨坑
当咱们把断点打在begin
以后,debug的时候会发现和咱们直接run
的结果彻底不一样,直接上图你们看下
debug状况下的结果:
Connected to the target VM, address: '127.0.0.1:52529', transport: 'socket'
Exception in thread "main" java.nio.channels.ClosedByInterruptException
at java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
at sun.nio.ch.SocketChannelImpl.connect(SocketChannelImpl.java:659)
at java.nio.channels.SocketChannel.open(SocketChannel.java:189)
at src.com.lyf.page3.InterruptNIO.main(InterruptNIO.java:50)
Disconnected from the target VM, address: '127.0.0.1:52529', transport: 'socket'
复制代码
run状况下的结果:
Waiting for read() in src.com.lyf.page3.NIOBlockExample@168b9408
Waiting for read() in src.com.lyf.page3.NIOBlockExample@740759f
一号你炸啦!!!!!!!!!!!!
二号你炸啦!!!!!!!!!!!!
java.nio.channels.ClosedByInterruptException
at java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:407)
at src.com.lyf.page3.NIOBlockExample.run(NIOBlockExample.java:26)
at java.lang.Thread.run(Thread.java:748)
一号 = ClosedByInterruptException
java.nio.channels.ClosedByInterruptException
一号 = Exiting NIOBlocked.run() src.com.lyf.page3.NIOBlockExample@168b9408
at java.nio.channels.spi.AbstractInterruptibleChannel.end(AbstractInterruptibleChannel.java:202)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:407)
at src.com.lyf.page3.NIOBlockExample.run(NIOBlockExample.java:26)
at java.lang.Thread.run(Thread.java:748)
二号 = ClosedByInterruptException
二号 = Exiting NIOBlocked.run() src.com.lyf.page3.NIOBlockExample@740759f
复制代码
为何会这样呢?为何呢?我也是调试来好久,最后发现若是断点打在begin,会致使main线程被阻塞,从而致使socket通道关闭
。也就是说咱们的断点是不能阻断main线程的运行的。光说结论没有事实可不行,这里咱们改造一下咱们的代码。
ServerSocket server = new ServerSocket(8080);
InetSocketAddress isa = new InetSocketAddress("localhost", 8080);
SocketChannel sc1 = null;
SocketChannel sc2 = null;
try {
sc1 = SocketChannel.open(isa);
sc2 = SocketChannel.open(isa);
} catch (ClosedByInterruptException e) {
}
Thread t1 = new Thread(new NIOBlockExample(sc2), "一号");
t1.start();
Thread t2 = new Thread(new NIOBlockExample(sc1), "二号");
t2.start();
//打断标志
t1.interrupt();
t2.interrupt();
复制代码
把open
方法无耻地try起来,这时候咱们再次debug就能看到结果以下:
begin
不打断点打结果图拿来对比着看
若是断点打在begin,会致使main线程被阻塞,从而致使socket通道关闭
,这里咱们能够清楚地看到
sc1
和
sc2
都为null。
好的巨坑已经给同窗们填好了终于能够放心地debug了😄
咱们如今再回到咱们的begin
来说,在open
的时候帮咱们初始化了对象以后,紧接着他调用了blockedOn(interruptor);
,鉴于不能把main阻塞的原则咱们在断点上添加过滤条件!"main".equals(Thread.currentThread().getName())
,继续在blockedOn(interruptor);
处打断点,咱们能够看到一号线程和二号线程都会进来。
blockedOn
函数帮助咱们完成了
private volatile Interruptible blocker;
的赋值,关于具体这么实现的,这里咱们再也不深刻探究,感兴趣的同窗能够自行查看System内部相关代码。
咱们既然讲了begin
,顺便咱们把java.nio.channels.spi.AbstractInterruptibleChannel#end
也讲一讲。
/**
标志I/O操做的结束
* Marks the end of an I/O operation that might block indefinitely.
*
//确保begin和end一块儿调用
* <p> This method should be invoked in tandem with the {@link #begin
* begin} method, using a <tt>try</tt> ... <tt>finally</tt> block
* as shown <a href="#be">above</a>, in order to implement asynchronous
* closing and interruption for this channel. </p>
*
* @param completed
* <tt>true</tt> if, and only if, the I/O operation completed
* successfully, that is, had some effect that would be visible to
* the operation's invoker * * @throws AsynchronousCloseException * If the channel was asynchronously closed * * @throws ClosedByInterruptException * If the thread blocked in the I/O operation was interrupted */ protected final void end(boolean completed) throws AsynchronousCloseException { blockedOn(null); Thread interrupted = this.interrupted; if (interrupted != null && interrupted == Thread.currentThread()) { interrupted = null; throw new ClosedByInterruptException(); } if (!completed && !open) throw new AsynchronousCloseException(); } 复制代码
这边给咱们提供了两种异常处理
1.AsynchronousCloseException 只有在通道被异步关闭的时候会抛出这个异常。
2.ClosedByInterruptException 线程由于I/O操做而致使Blocked的状况下会抛出这个异常。
结合咱们以前main打断点的而致使通道被关闭抛出ClosedByInterruptException
异常可知咱们的断点实际上是会致使main被blocked中断了致使的I/O异常。
关于interrupt
API咱们也研究地差很少了,这里问同窗们一个问题,咱们一旦调用interrupt
以后,线程就会立马被中断吗?
答案是不会的,咱们来看个例子
private static class MySleep implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println("我一直都在");
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MySleep(), "我是个没有感情的标签");
thread.start();
System.out.println("我开始打断你");
thread.interrupt();
System.out.println("你已经被我打断了");
}
复制代码
输出:
我开始打断你
你已经被我打断了
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at src.com.lyf.page3.InterruptIsJustSign$MySleep.run(InterruptIsJustSign.java:17)
at java.lang.Thread.run(Thread.java:748)
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
我一直都在
复制代码
咱们能够看到即便被中断了sleep
线程仍是会把剩下的逻辑执行完成,也就是说interrupt
是一个标志,线程自己是否是中断中止仍是要看线程自己是否结束。咱们的interrupt
只做用于咱们以前列出的那几类方法。
到这里咱们的interrupt
大体就介绍完了
老套路
/**
//查看线程有没有被中断,此方法不会影响线程的中断状态
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
//线程中断被忽略的时候此方法一直返回false
* <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的一个成员方法,相对来讲比较简单,可是再简单咱们也要看下究竟是怎么个用法,下面给出对应的使用案例。
private static class MyInterrupted implements Runnable {
@Override
public void run() {
System.out.println("我已经被启动了");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("我被中断了");
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyInterrupted(), "一号");
System.out.println("线程未启动时候中断标志:" + thread.isInterrupted());
thread.start();
System.out.println("线程启动时候中断标志:" + thread.isInterrupted());
thread.interrupt();
System.out.println("线程启动并调用interrupt时候中断标志:" + thread.isInterrupted());
}
复制代码
输出:
线程未启动时候中断标志:false
线程启动时候中断标志:false
线程启动并调用interrupt时候中断标志:true
我已经被启动了
我被中断了
复制代码
/**
测试当前线程是否已经被中断,这个方法会清除中断标志,也就是第一次调用是
中断以后第一次调用会返回true,除非再次中断否则返回都是false
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <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 the current thread has been interrupted;
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
复制代码
和isInterrupted
的区别就是interrupted
会清除中断状态,这个咱们用具体代码来理解一下
public static void main(String[] args) {
System.out.println("调用中断方法以前isInterrupted()标记为:" + Thread.currentThread().isInterrupted());
System.out.println("调用中断方法以前interrupted标记为:" + Thread.interrupted());
Thread.currentThread().interrupt();
System.out.println("调用中断方法以后isInterrupted()标记为,第一次输出:" + Thread.currentThread().isInterrupted());
System.out.println("interrupted()标记为,第一次输出:" + Thread.interrupted());
System.out.println("isInterrupted()标记为,第二次输出:" + Thread.currentThread().isInterrupted());
System.out.println("interrupted()标记为,第二次输出:" + Thread.interrupted());
}
复制代码
输出:
调用中断方法以前isInterrupted()标记为:false
调用中断方法以前interrupted标记为:false
调用中断方法以后isInterrupted()标记为,第一次输出:true
interrupted()标记为,第一次输出:true
isInterrupted()标记为,第二次输出:false
interrupted()标记为,第二次输出:false
复制代码
前面两段很好理解,在没有调用interrupt
中断线程以前,线程中断状态返回默认都是false,第三行和第四行也好理解表示调用中断方法以后,线程的中断状态变为true。咱们主要来看第五第六行,咱们发现调用了interrupted()
以后,线程状态编程了false,这也印证了咱们开头说的interupted会清除中断状态
。
终于把这块写出来了,本章的重点都在调试interrupt
这块内容上,虽然比较繁琐,可是同窗们若是本身也能亲手跟着个人代码一块儿去探索一下的话相信会给你们带来很大的提高。最后但愿同窗们点点赞,点点关注呀~~🙏