线程的阻塞和中断

1 线程的阻塞

阻塞和非阻塞是形容多个线程之间的相互影响的,一个线程占用了临界区资源,那么其他线程必须在临界区外等待,阻塞是操作系统层面挂起在内存,释放CPU,上下文切换了,所以性能不高。如果一个线程一直占用不释放资源,那么其他需要该临界区资源的线程都必须一直等待。非阻塞就是运行多个线程同时进入临界区,只要保证不把数据修改坏就行。

  • 阻塞情况分为三种:

    • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池(阻塞队列)中。其他线程唤醒它后进入锁池。
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    • 其他阻塞:运行的线程执行sleep()或join()方法时,或者发出I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时,join()等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。
  • 如果线程被阻塞,很有可能被永久阻塞,例如调用Object.wait()、ServerSocket.accept()和DatagramSocket.receive()时,都可能永久的阻塞线程。即使发生超时,在超时期满之前持续等待也是不可行和不适当的,所以,要使用某种机制使线程更早地退出阻塞状态

2 中断
2.1 Thread.interrupt()方法

中断可以理解为线程的一个标志位属性,它表示一个线程是否被其他线程进行了中断操作。在java中,其他线程通过调用某线程的interrupt()方法对其进行中断操作,但只是改变了线程的中断状态,通知这个线程该中断了。至于这个中断状态改变后执行的操作,应该由被通知的线程自己处理。

线程处于阻塞状态

如果被中断线程处于阻塞状态(例如线程调用了Thread.sleep、thread.join、thread.wait、1.5中的condition.await、可中断的Channel上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常。

  1. 对于wait()中等待notify/notifyAll唤醒的线程,正在某一对象的等待池中。这时如果它的中断状态被改变,那么它就会抛出InterruptedException异常。这个异常不是线程抛出的,而是wait方法,对象的wait方法内部会不断检查在此对象上等待的线程的状态,如果发现哪个线程的状态被置为已中断,则会抛出InterruptedException(同时清除中断标志位,设为false),意思就是这个线程不能再等待了,意义等同于唤醒它。

    与notify()的区别是,被notify/All唤醒的线程会继续执行wait下面的语句,而在wait中被中断的线程则将控制权交给了catch语句(catch中捕获InterruptedException异常),一些正常的逻辑要被放到catch中来运行。

  2. 对于sleep()中的线程,如果调用了Thread.sleep(1 year),现在后悔了,想让它早些醒过来,调用interrupt()方法就是唯一手段,只有改变它的中断状态,让它从sleep中将控制权转到处理异常的catch语句中,然后再由catch中的处理转换到正常的逻辑。同样,对于join中的线程也可以这样处理。

  3. I/O操作阻塞的线程,I/O操作可以阻塞线程一段相当长的时间,特别是牵扯到网络应用时。例如,服务器可能需要等待一个请求(request),又或者,一个网络应用程序可能要等待远端主机的响应。

    实现InterruptibleChannel接口的通道是可中断的,如果某个线程在可中断通道上因调用某个阻塞的 I/O 操作(常见的操作有:serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write)而进入阻塞状态,而另一个线程又调用了该阻塞线程的 interrupt 方法,这将导致该通道被关闭,并且已阻塞线程接将会收到ClosedByInterruptException,并且设置已阻塞线程的中断状态。另外,如果已设置某个线程的中断状态并且它在通道上调用某个阻塞的 I/O 操作,则该通道将关闭并且该线程立即接收到 ClosedByInterruptException,并仍然设置其中断状态。

在Java程序中,最好的办法就是让线程从run()方法返回,可以设置共享变量做标志位,run时定期检查,但被阻塞时无法执行run方法,也就无法检查标志位。可以利用interrupt方法,在修改标志位之后调用interrupt方法,结束阻塞后再次运行run时检查标志位,则可以正常终止运行。

  • 如何处理InterruptedException:
    • 如果很清楚当前线程被中断后的处理方式,则在catch语句中按自己的方式处理。 通常是做好善后工作,主动退出线程。
    • 直接在方法声明中throws InterruptedException,丢给上层处理。这种方式也很常见,将中断的处置权交给具体的业务来处理
    • 重新设置中断标记位,Thread.currentThread().interrupt(),交给后续方法处理,
      原因是底层抛出InterruptedException时会清除中断标记位,捕获到异常后如果不想处理,可以重新设置中断标记位。
正在运行的线程
  • Thread.interrupt()方法不会中断一个正在运行的线程。如果线程处于正常活动状态,interrupt()会将该线程中的中断标志设置为true,被设置中断标志的线程将继续正常执行,不受影响。
其他线程
  • 还有一些阻塞方法不会响应interrupt,如等待进入synchronized段、Lock.lock()。他们不能被动的退出阻塞状态。

    synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断。
    与synchronized功能相似的reentrantLock.lock()方法也是不可中断的,即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。也可以调用reentrantLock.lockInterruptibly()方法,相当于一个超时设为无限的tryLock方法。

2.2 isInterrupted()/interrupted()
  • Thread.currentThread().isInterrupted():判断某个线程是否已被发送过中断请求。
  • Thread.interrupted():该方法调用后会将中断标示位清除,即重新设置为false。
3 Thread.interrupt()源码分析
4 小结

终止一个Java线程最好的方式,就是让run()方法主动退出。因为强制的让一个线程被动的退出是很不安全的,内部的数据不一致会对程序造成不可预知的后果。
为了能够通知一个线程需要被终止,Java提供了Thread.interrupt()方法,该方法会设置线程中断的标记位,并唤醒可中断的阻塞方法,包括Thread.sleep(),Object.wait(),nio通道的IO等待,以及LockSupport.park()。识别一个方法是否会被中断,只需要看其声明中是否会throws InterruptedException或ClosedByInterruptException。

每个Java线程都会对应一个osthread,它持有了三种条件变量,分别用于Thread.sleep(),Object.wait()和unsafe.park()。Thread.interrupt()会依次唤醒三个条件变量,以达到中断的目的。线程的同步与唤醒最终都使用了pthread_cond_wait和pthread_cond_signal这些pthread库函数。

【参考文档】
阻塞(sleep等等)区别 中断(interrupt)+ 中断的意义
Thread.interrupt()到底做了啥?
线程中断详解

</article>
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值