取消与中断

java曾经使用抢占式中断,如thread.stop,thread.suspend,thread.resume.但这种方式存在诸多缺陷(注释1),故选择了协作式机制,通过轮训cancelled标志来达到取消效果

取消

如果外部代码能在某个操作正常完成之前将其置入完成状态,那么这个操作就称作可取消的。

java并没有提供取消线程的方式,但我们能通过设计一个 cancelled的标志,来标明线程的状态,如下


(此中标志为线程间共享的,使用volatile保证其线程安全)

一般来说,这种方式是没有问题的,但是这种取消操作与阻塞方法相遇的时候,会产生一个严重的问题——线程将会永远的阻塞下去。

例如blockingQueue(注释1),当生产者速度超过消费者,blockingQueue.put会阻塞,如果此时使用cancel()将状态设置为取消,且同时消费者也不再从队列中拿出任务,那么生产就会一直阻塞在put上,线程也会一直阻塞下去,因为队列始终是满的,线程无法去检测cancel状态。程序如下:

中断

通常,中断是实现取消的最合理方式,每个线程都有一个boolean类型的中断状态,当中断线程时,这个状态会被设置为true。但实际上,中断操作不会真正的中断一个正在运行的线程,而只是发出一个中断请求,然后由线程来决定何时中断自己。其中分两种情况,一种是可中断的阻塞方法(sleep,wait,join等),一种是不可中断的方法。在前一种中,当它们收到中断请求时他们会抛出interruptedException,然后可以选择传递异常或者调用静态interrupted恢复中断状态,而在后一种情况中,只能轮询isINterrupted来判断状态并进一步处理。为了能当中断能够及时响应,JVM在底层进行线程调度的对标志进行检查。所以中断是能够代替和解决取消锁无法应付的无限阻塞问题。代码如下

在任务与线程分离的框架中,任务通常并不知道自身会被哪个线程调用,也就不知道调用线程处理中断的策略。所以,在任务设置了线程中断标记后,并不能确保任务会被取消。因此,有以下两条编程原则:
1)除非你知道线程的中断策略,否则不应该中断它。
这条原则告诉我们,不应该直接调用Executer之类框架中线程的interrupt方法,应该利用诸如Future.cancel(注释2)的方法来取消任务。
2)任务代码不该猜测中断对执行线程的含义。
这条原则告诉我们,一般代码遇在到InterruptedException异常时,不应该将其捕获后“吞掉”,而应该继续向上层代码抛出。 

处理不可中断的阻塞

io包中的socket I/O:阻塞I/O形式为对套接字的read、write操作。虽然read,write操作不会响应中断,但是通过关闭底层的套接字,可以使得由于使用该方法而阻塞的线程抛出socketException
io包中的同步I/O
selector的异步IO
获得某个锁:如果一个线程因等待某个锁而阻塞,那么该线程是不会理会中断,唯一的办法是使用实现lock接口的reentrantlock(注释3)对象。





停止基于线程的服务

如果你想中断一个线程,那么必须清楚他的中断策略,即是说中断线程应该由线程所有者去执行(创建线程的类)。例如要中断工作者线程,那么就应该让线程池去执行这份操作。

生产者消费者日志服务例子分析
其中使用的blockingqueue的put和take都是可中断的阻塞方法,所以他们都能够使用interrupt及时中断。但这样做存在两个问题,1:如果日志写入还在进行中,这种中断方式会让我们丢失那些未完成的部分,第二,当线程中断时,队列是满的,其他线程在调用log时被阻塞且无法接触阻塞状态。要解决这个问题。我们应该在收到中断请求标志之后,消费者take队列使其不成为满队列,让put解除阻塞状态,这个过程只能是独立的,不然就会发生如下情况:当消费者take之后使队列接触阻塞状态,但是由于静态条件,另一个消费者再次使队列变满,剩余所有的生产者依然还是无法摆脱阻塞状态,故要对终端请求标志进行枷锁,保证其独立性。


ExecutorService提供了对生命周期管理的方法。我们可以将管理线程的任务委托给ExecutorService,并让其进行管理(使用shutdown或者shutdownNow)。
使用毒丸对象关闭生产者消费者服务

注释1:
stop方法:粗暴的停止当前线程,无论当前线程处于什么状态,这意味着如果线程正进行一半时,强制退出,那么数据将无法保证其完整性。这是非常不可取的方法
suspend和resume方法:将线程挂机,但是不释放锁,因为不释放锁,所以这很显然容易导致死锁问题,应该使用wait-notify来代替suspend和resume。wait-notify属于对象级别,而suspend-resume属于thread类级别。锁是任何对象都有的,调用任何对象的wait方法将意味着线程阻塞并且该对象锁被释放。调用该对象的notify方法意味着因调用该对象notify方法而阻塞的线程会随机被唤醒一条。此外还有notifyall,意味着唤醒调用该对象wait方法的所有线程,让他们公平竞争锁并去执行。
ps:suspend和resume方法可以在任何地方调用,但是wait-notify只能在synchronized快内使用,因为锁只能在synchronized内释放和获得。如不在synchronized使用程序不会报编译错误,但是运行时会抛出 IllegalMonitorStateException 。
注释2:future.cancel方法中有一个boolean类型的参数mayinterrupifrunning,如果该参数为true,则说明该线程是能被中断的,如果为false,则意味着“如果还没运行,那么不要运行该线程”。执行任务的线程是由标准的executor创建的,它实现了一种中断策略使得任务可以通过中断被取消。
注释3
reentrantlock能够完成内置锁所无法完成的工作(中断一个正在等待获得锁的线程),提供了无条件的,可轮询,可定时,可中断的加锁方法
构造方法:reentrantlock的构造方法提供了两种公平性选择,默认创建一个非公平的锁,或者创建一个公平的锁,非公平的锁意味着,当一个线程请求这个锁时,这个锁的状态刚好变成可用,那么该线程能跳过千面所有的线程获得该锁,公平的锁意味着所有请求锁的线程只能乖乖的排在队列中。非公平的锁显然性能高于公平的锁,因为公平的锁要求每个线程都经历挂起线程和回复线程两个步骤,这些开销降低了性能
1:lock.trylock(),使用该函数来获取N个锁,如果不能同时获取,则退回并重新尝试,也可以给trylock添加时间参数,使之成为一个定时限制操作。
由于trylock的特性,他能有效的避免以下集中死锁情况(锁顺序死锁,动态锁顺序死锁),协作对象之间发生的死锁通过开放调用来解决(调用某个方法时不需要持有锁)
此外,死锁还包括资源死锁(线程池越大,发生可能性越低),线程饥饿死锁(有界队列/资源池不能与相互依赖任务一起使用),活锁(尽管没有阻塞,但是任务会重复提交,失败,返回。提交失败返回无限循环下去,例如例如同时发送两个会发生冲突的资源包,他们发生了冲突,并在一秒后重试,这很显然他们会一直发生冲突下去。改善方法是将重试时间随机化)
2:lockinterruptibly能够在获得锁的同时保持对中断的响应
3:读写锁:读写锁的加锁策略是:允许多个线程同时进行读操作,或者允许一个线程进行写操作,读写锁是一种优化措施,它自身付出了性能的代价,为了更好的在频繁读取的环境中工作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值