第31讲心得
该讲介绍了Guarded Suspension模式。
- 一个对象 GuardedObject,内部有一个成员变量——受保护的对象,以及两个成员方法——get(Predicate<T> p)和onChanged(T obj)方法。其中get() 方法用来实现等待,参数 p 就是用来描述这个前提条件的;onChanged() 方法可以 fire 一个事件,而这个事件往往能改变前提条件 p 的计算结果。
class GuardedObject<T>{ //受保护的对象 T obj; final Lock lock = new ReentrantLock(); final Condition done = lock.newCondition(); final int timeout=2; //保存所有GuardedObject final static Map<Object, GuardedObject> gos=new ConcurrentHashMap<>(); //静态方法创建GuardedObject static <K> GuardedObject create(K key){ GuardedObject go=new GuardedObject(); gos.put(key, go); return go; } static <K, T> void fireEvent(K key, T obj){ GuardedObject go=gos.remove(key); if (go != null){ go.onChanged(obj); } } //获取受保护对象 T get(Predicate<T> p) { lock.lock(); try { //MESA管程推荐写法 while(!p.test(obj)){ done.await(timeout, TimeUnit.SECONDS); } }catch(InterruptedException e){ throw new RuntimeException(e); }finally{ lock.unlock(); } //返回非空的受保护对象 return obj; } //事件通知方法 void onChanged(T obj) { lock.lock(); try { this.obj = obj; done.signalAll(); } finally { lock.unlock(); } } }
第33讲心得
该讲介绍了Thread-Per-Message模式。
- Thread-Per-Message 模式,简言之就是为每个任务分配一个独立的线程。最经典的应用场景是网络编程里服务端的实现,服务端为每个客户端请求创建一个独立的线程,当线程处理完请求后,自动销毁,这是一种最简单的并发处理网络请求的方法。
- Thread-Per-Message 模式如果使用线程池方案就会增加复杂度。Thread-Per-Message 模式在 Java 领域并不是那么知名,根本原因在于 Java 语言里的线程是一个重量级的对象,为每一个任务创建一个线程成本太高,尤其是在高并发领域,基本就不具备可行性。
- 对于一些并发度没那么高的异步场景,例如定时任务,采用 Thread-Per-Message 模式是完全没有问题的。
第34讲心得
该讲介绍了 Worker Thread 模式。
- 如何去实现 Worker Thread 模式呢?你很容易就能想到用阻塞队列做任务池,然后创建固定数量的线程消费阻塞队列中的任务。其实你仔细想会发现,这个方案就是 Java 语言提供的线程池。
- 线程池有很多优点,例如能够避免重复创建、销毁线程,同时能够限制创建线程的上限等等。
- Java 的线程池既能够避免无限制地创建线程导致 OOM,也能避免无限制地接收任务导致 OOM。只不过后者经常容易被我们忽略。所以强烈建议你用创建有界的队列来接收任务。当请求量大于有界队列的容量时,就需要合理地拒绝请求。如何合理地拒绝呢?这需要你结合具体的业务场景来制定,即便线程池默认的拒绝策略能够满足你的需求,也同样建议你在创建线程池时,清晰地指明拒绝策略。
- 使用线程池过程中,还要注意一种线程死锁的场景。如果提交到相同线程池的任务不是相互独立的,而是有依赖关系的,那么就有可能导致线程死锁。。这种问题通用的解决方案是为不同的任务创建不同的线程池。最后再次强调一下:提交到相同线程池中的任务一定是相互独立的,否则就一定要慎重。
第35讲心得
本讲介绍了两阶段终止模式。
- Java 语言的 Thread 类中曾经提供了一个 stop() 方法,用来终止线程,可是早已不建议使用了,原因是这个方法用的就是一剑封喉的做法,被终止的线程没有机会料理后事。
- 前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式。顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程 T1 向线程 T2发送终止指令,而第二阶段则是线程 T2响应终止指令。
- Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态,而实际上线程也可能处在休眠状态,也就是说,我们要想终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。如何做到呢?这个要靠 Java Thread 类提供的 interrupt() 方法,它可以将休眠状态的线程转换到 RUNNABLE 状态。
-
线程转换到 RUNNABLE 状态之后,我们如何再将其终止呢?RUNNABLE 状态转换到终止状态,优雅的方式是让 Java 线程自己执行完 run() 方法,所以一般我们采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。这个过程其实就是我们前面提到的第二阶段:响应终止指令。
-
综合上面这两点,我们能总结出终止指令,其实包括两方面内容:interrupt() 方法和线程终止的标志位。
-
按照两阶段终止模式,我们首先需要做的就是将线程 rptThread 状态转换到 RUNNABLE,做法很简单,只需要在调用 rptThread.interrupt() 就可以了。线程 rptThread 的状态转换到 RUNNABLE 之后,如何优雅地终止呢?下面的示例代码中,我们选择的标志位是线程的中断状态:Thread.currentThread().isInterrupted() ,需要注意的是,我们在捕获 Thread.sleep() 的中断异常之后,通过 Thread.currentThread().interrupt() 重新设置了线程的中断状态,因为 JVM 的异常处理会清除线程的中断状态。
class Proxy { boolean started = false; //采集线程 Thread rptThread; //启动采集功能 synchronized void start(){ //不允许同时启动多个采集线程 if (started) { return; } started = true; rptThread = new Thread(()->{ while (!Thread.currentThread().isInterrupted()){ //省略采集、回传实现 report(); //每隔两秒钟采集、回传一次数据 try { Thread.sleep(2000); } catch (InterruptedException e){ //重新设置线程中断状态 Thread.currentThread().interrupt(); } } //执行到此处说明线程马上终止 started = false; }); rptThread.start(); } //终止采集功能 synchronized void stop(){ rptThread.interrupt(); } }