《Java并发编程实战》-8

图形用户界面应用程序


9.1 为什么GUI是单线程的

9.1.1 串行时间处理

9.1。2 Swing中的单线程封闭机制

Swing的单线程规则是:Swing中的组件以及模型只能在这个事件分发线程中进行创建、修改以及查询。

9.2 短时间的GUI任务

模型对象与视图对象的控制流

st=>start: EDT
op1=>operation: 鼠标点击
op2=>operation: 动作事件
op3=>operation: 动作监听者
op4=>operation: 更新表格模型
op5=>operation: 表格修改事件
op6=>operation: 表格监听者
op7=>operation: 更新表格视图

st->op1->op2->op3->op4->op5->op6->op7

9.3 长时间的GUI任务

9.3.1 取消

9.3.2 进度标识和完成标识

9.3.3 SwingWorker

9.4 共享数据模型

9.4.1 线程安全的数据模型

9.4.2 分解数据模型

如果体格数据模型必须被多个线程共享,而且由于阻塞,一致性或复杂度等原因而无法实现一个线程安全的模型时,可以考虑使用分解模型设计。

9.5 其他形式的单线程子系统

小结

所有GUI框架基本上都实现为单线程的子系统,其中所有与表现相关的代码都作为任务在事件线程中运行。由于只有一个事件线程,因此运行时间较长的任务会降低GUI程序的影响性,所以应该放在后台线程中运行。在一些辅助类(例如SwingWorkrt以及在本章中构建的BackgroundTask)中提供了对取消、进度指示以及完成指示的支持,因此对于执行时间较长的任务来说,无论在任务中包含了GUI组件还是非GUI组件,在开发时可以得到简化。

《Java并发编程实战》-7

第8章 线程池的使用


8.1 在任务与执行策略之间的隐形耦合

并非所有的任务有些任务需要明确地指明执行策略,包括:

  • 依赖性任务
  • 使用线程封闭机制的任务。
  • 对应时间敏感的任务。
  • 使用ThreadLocal的任务

在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或者被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。

8.1.1 线程饥饿死锁

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。

8.1.2 运行时间较长的任务

如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。

定任务等待资源得时间,而不是无限制地等待可以缓解执行时间较长得任务造成的影响。

8.2 设置线程池得大小

一般情况下正确地设置线程池的大小(假设有n个CPU):

  • 计算密集型: 2n+1
  • I/O密集型:2n

8.3 配置ThreadPoolExecutor

ThreadPoolExecutor的通用构造函数

public ThreadPoolExecutor(int corePoolSize,
                        int maximumPollSize,
                        long keepAliveTime,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler)
                        { ... }

线程的创建与销毁

基本大小(Core Poll Size)是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超过这个数量的线程。

线程池的最大大小(maximumPoolSize)表示可同时活动的线程数量的上限。如果某个线程的空闲事件超过了存活时间,那么将标记为可回收,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。

管理队列任务

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种:无界队列、有界队列和同步移交(Synchronous Handoff)。

对于Executor,newCachedThreadPool工厂方法时一种很好的默认选择,他能提供比固定大小的线程更好的排队性能.当需要限制当前任务的数量以满足资源管理需求时,那么可以选择固定大小的线程池,就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载问题。

8.3.3 饱和策略

每种RejectedExecutionHandler的实现包含有不用的饱和策略:

  • Abort策略:默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。
  • CallerRunPolicy 为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。
  • Discard策略:新提交的任务被抛弃。
    DiscardOldest策略:队列的是“队头”的任务,然后尝试提交新的任务。(不适合工作队列为优先队列场景)。

8.3.4 线程工厂

8.4 扩展ThreadPoolExecutor

实例:给线程池添加统计信息

8.5 递归算法的并行化

实例:谜题框架

对于并发执行的任务,EXecutor框架是一种强大且灵活的框架。它提供了大量可调节的选项,例如创建线程和关闭线程的策略,处理队列任务的策略,处理过多任务的策略,并且提供了几个钩子方法来扩展它的行为。然而,与大多数功能强大的框架一样,其中有些设置参数并不能很好地工作,某些类型的任务需要特定的执行策略,而一些参数组合则可能产生奇怪的结果。

《Java并发编程实战》-6

取消与关闭


行为良好的软件能够完善地处理失败,关闭和取消等过程。

7.1任务取消

取消某个操作的原因:

  • 用户请求取消
  • 有时间限制的操作
  • 应用程序时间
  • 错误
  • 关闭

7.1.1 中断

在Java的API或语言规范中,并没有将中断与任何取消语言关联起来,但实际是激昂,如果在取消之外的其他操作中使用中断,那么都是不合适的,并且很难支撑起更大的应用。

public class Thread {
    // 中断目标线程
    public void  interupt() { ... }
    // 返回目标线程的中断状态
    public boolean isInterrupted() { ...}
    // 静态, 将清楚当前线程的中断状态,并返回它之前的值,这也是清楚中断状态的唯一方法
    public static boolean interrupted() { ... }
}

非阻塞状态下中断时,它的中断状态将被设置,中断操作将变得“有粘性” – 如果不触发InterruptedEXception,那么中断状态将一直保持,直到明确地清除中断状态。

调用interrupt并不意味着立刻停止目标线程正在进行的工作,而只是传递了请求中断的消息。中断操作并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己(这些时刻也被称为取消点)。

通常,中断是实现取消的最合理方式。

通过中断来取消。

class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;

    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run() {
        BigInteger p = BigInteger.ONE;
        while (!Thread.currentThread().isInterrupted()) {
            queue.put( p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
            /* 允许线程推出 */
        }
    }

    public void cancel() { interrupt(); }

}

7.1.2 响应中断

两种实用策略可以用于处理InterruptedException

  • 传递异常 (可能在执行某个特定于任务的清除操作之后),从而使你的方法也可以成为可中断的阻塞方法。
  • 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。

只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。

不可取消的任务在推出前恢复中断

public Task getNextTask(BlockingQueue<Task> queue) {
    boolean interrupted = false;
    try {
        try {
            return queue.take();
        } catch (InterruptedException e) {
            interrupted = ture;
        }
    } finally {
        if (interrupted) 
            Thread.currentThread().interrupt();
    }
}

7.1.4 示例:计时器运行

7.1.5 通过Future来实现取消

当Future.get()抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。

7.1.6 处理不可中断的阻塞

  • Java.io包中的同步Socket I/O。
  • java.io包的同步I/O。
  • Selector的异步I/O。
  • 获取某个锁。

7.1.7 采用newTaskFor来封装非标准的取消

7.2 停止基于线程的服务

对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期的方法。

7.2.1 示例:日志服务

7.2.2 关闭ExecutorService

7.2.3 “毒丸”对象

7.2.4 示例:只执行一次的服务

7.2.5 shutdownNow的局限性

7.3 处理非正常的线程终止

未捕获异常的处理

将异常写入日志的UncaughtExceptionHandler

public class UEHLogger implements THread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.SEVERE, 
        "Thread terminated with exception: " + t.getName(), e);
    }
}

在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。

7.4 JVM关闭

7.4.1 关闭钩子

7.4.2 守护线程

此外,守护线程通常不能用来代替应用程序管理中各个服务的生命周期。

7.4.3 终结器

避免使用终结器。

小结

在任务、线程、服务以及应用程序等模块中的生命周期结束问题,可能会增加它们在设计和实现时的负责性。Java并没有提供某种抢占式的机制来取消操作或者终结线程。相反,它提供了一种协作式的中断机制来实现取消操作,但这要依赖于如何构建取消操作的协议,以及能否始终遵循这些协议。通过使用FutureTask和Executor框架,可以帮助我们构建可取消的任务和服务。