Java并发编程实战--笔记二

第5章:基础构建模块

     ConcurrentHashMap 与其他并发容器一起增强了同步容器类:他们提供的迭代器不会抛出 ConcurrentModificationException ,因此不需要在迭代过程中对容器加锁。

     ConcurrentHashMap 返回的迭代器具有弱一致性,而非 “及时失败”。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以在迭代器被构造后将修改操作反映给容器。

     尽管有这些改进,但仍然有一些需要权衡的因素。对于一些需要在整个Map 上进行计算的方法,例如 size 和 isEmpty ,这些方法的寓意被略微减弱了以反映容器的并发特性。由于 size 方法返回的结果在计算时可能已经过期了,它实际上只是一个估计值,因此允许 size 返回一个近似值而不是一个精确值。虽然这看上去有些令人不安,但事实上 size 和 isEmpty 这样的方法在并发环境下的用处很小,因为他们的返回值总是在不断变化。因此,这些操作的需求被弱化了,以换取对其他更重要操作的性能优化,包括 get、put、containsKey 和 remove等。

     与 Hashtable 和 synchronizedMap 相比,ConcurrentHashMap 有更多的优势以及更少的劣势。因此在大多数情况下,用 ConcurrentHashMap 来代替同步 Map 能进一步提高代码的可伸缩性。只有当应用程序需要加锁Map 以进行独占访问时,才能放弃使用 ConcurrentHashMap。

     “写入时复制(Copy-On-Write)” 容器的线程安全性在于,只要正确的发布一个事实不可变对象,那么访问该对象时就不需要进一步的同步。每次修改的时候,都会创建并重新发布一个新的容器副本,从而实现可变性。“写入时复制”容器的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于他不会被修改,因此在对其进行同步时只需确保数组内容的可见性。因此,多个线程可以对这个容器进行迭代,而不会彼此干扰或者修改容器的线程相互干扰。
显然,每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时。晋档跌打操作远远多于修改操作时,才应该使用“写入时复制”容器。

     在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具;它们能够意志或防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

     Thread提供了interrupt方法,用于中断线程或者查询线程是否已经被中断。每个线程都有一个布尔类型的属性,表示线程的中断状态,当中断线程时将设置这个状态。

     中断时一种协作机制。一个线程不能强制其他线程停止正在执行的操作而去执行其他的操作。当线程A中断B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作——前提是如果线程B愿意停下来。

第6章 任务执行

     任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。在Java类库中,任务执行的主要抽象不是Thread,而是Executor。

public interface Executor {
    void execute(Runnable command);
}

     虽然Executor是个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。

     Executor基于生产者–消费者模式,提交任务的操作相当于生产者(生成待完成的工作单元),执行任务的线程则相当于消费者(执行完这些工作单元)。如果要在程序中实现一个生产者–消费者模式的设计,那么最简单的方式通常就是使用Executor。

     类库提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors中的静态工厂方法之一来创建一个线程池:

     1、newFixedThreadPool:将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这是线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。

     2、newCachedThreadPool:将创建一个可缓存线程的线程池。根据任务按需创建线程,并且当任务结束后会将该线程缓存60秒,如果期间有新的任务到来,则会重用这些线程,如果没有新任务,则这些线程会被终止并移除缓存。此线程池适用于处理量大且短时的异步任务。

     3、newSingleThreadExecutor:是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。它还能确保依照任务在队列中的顺序来串行执行。

     4、newScheduledThreadPool:创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

     为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法(同时还有一些用于任务提交的便利方法),如下:

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
    //......其他用于任务提交的便利方法
}

     ExecutorService的生命周期有3种状态:运行、关闭和终止。ExecutorService在初始创建时处于运行状态。shutdown方法将执行平缓关闭的过程:不再接受新的任务,同时等待已经提交的任务执行完成——包括那些还未开始执行的任务。而shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。

     在ExecutorService关闭后提交的任务将由“拒绝执行处理器(Rejected Execution Handler)”来处理,它会抛弃任务,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务都完成后,ExecutorService将进入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过调用isTerminated来轮询ExecutorService是否已经终止。通常在调用awaitTermination之后会立即调用shutdown,从而同步关闭ExecutorService。

     Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不捕捉异常,因此当TimerTask抛出异常时,将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误的认为整个Timer都被取消了。因此已经被调度单稍微执行的TimerTask将不会再执行,新的任务也不能被调度(这个问题称为“线程泄露[Thread Leake]”)。

     CompletionService将Executor和BlockingQueue的功能融合在一起。你可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时将被封装为Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。

     Executor框架将任务提交与执行策略解耦开来,同时还支持多重不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大的好处,必须定义清晰的任务边界。

第7章 取消与关闭

     对中断操作的正确理解是:它并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被称为取消点)。有些方法,例如wait、sleep和join等,将严格的处理这种请求,当它们收到中断请求或者在开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。设计良好的方法可以完全忽略这种请求,只要它们能使调用代码对中断请求进行某种处理。设计糟糕的方法可能会屏蔽中断请求,从而导致调用栈中国的其它代码无法对中断请求做出响应。

     在使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态。如果在调用interrupted时返回了true,那么除非你想屏蔽这个中断,否则必须对它进行处理——可以抛出InterruptedException,或者通过再次调用interrupt来恢复中断状态,如下代码所示:

public class TaskRunnable implements Runnable {
    BlockingQueue<Task> queue;
    public void run() {
        try {
            processTask(queue.take());
        } catch (InterruptedException e) {
            // restore interrupted status
            Thread.currentThread().interrupt();
        }
    }
    void processTask(Task task) {
        // Handle the task
    }
    interface Task {
    }
}

     任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。无论任务把中断视为取消,还是其他某个中断响应操作,都应该小心地保存执行线程的中断状态。如果除了将 InterruptedException 传递给调用者外还需要执行其他操作,那么应该在捕获 InterruptedException 之后恢复中断状态:

Thread.currentThread().interrupt();

     由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。
只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。

     与其他的封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能停止工作者线程。相反,服务应该提供生命周期方法,服务就可以关闭所有的线程了。这样,当应用程序关闭服务时,服务就可以关闭所有的线程了。在ExecutorService中提供了shutdown和shutdownNow等方法,同样,在其他拥有线程的服务方法中也应该提供类似的关闭机制。

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

     幂等(idempotent、idempotence):在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

     未检查异常也叫RuntimeException(运行时异常)。

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

     当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提供任何异常处理器,那么默认的行为是将栈追踪信息输出到System.err。

     这些异常处理器中,只有一个将会被调用——JVM首先搜索每个线程的异常处理器,若没有,则搜索该线程的ThreadGroup的异常处理器。ThreadGroup中的默认异常处理器实现是将处理工作逐层委托给上层的ThreadGroup,直到某个ThreadGroup的异常处理器能够处理该异常,否则一直传递到顶层的ThreadGroup。顶层ThreadGroup的异常处理器委托给默认的系统处理器(如果默认的处理器存在,默认情况下为空),否则把栈信息输出到System.err。

// UncaughtExceptionHandler接口
public interface UncaughtExceptionHandler {
    void uncaughtException(Thread t, Throwable e);
}

     要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。(与所有的线程操控一样,只有线程的所有者能够改变线程的UncaughtExceptionHandler。)标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个try-finally代码块来接收通知,因此当线程结束时,将有新的线程来代替它。

     令人困惑的是,只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Funture.get封装在ExecutionException中重新抛出。

     守护线程

     有时候,你希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍JVM的关闭。在这种情况下就需要使用守护线程(Daemon Thread)。

     在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此默认情况下,主线程创建的所有线程都是普通线程。

     普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃——既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。

     我们应尽可能少地使用守护线程——很少有操作能够在不进行清理的情况下被安全地抛弃。特别是,如果在守护线程中执行可能包含I/O操作的任务,那么将是一种危险的行为。守护线程最好用于执行“内部”任务,例如周期性地从内存的缓存中移出逾期的数据。

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

Java并发编程实战pdf及案例源码下载:
http://download.csdn.net/detail/xunzaosiyecao/9851028

个人微信公众号:
这里写图片描述

作者:jiankunking 出处:http://blog.csdn.net/jiankunking

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页