class="MsoNormal" style="line-height: 150%;">构建块
在实践中,委托是创建线程安全类最有效的策略之一:只需要用已有的线程安全类来观礼所有状态即可。
平台类库包含一个并发构建块的丰富集合。比如线程安全容器和多种同步工具(synchronizer:用来调节相互协作的线程间的一些控制流)
?
同步容器
同步容器类包括2部分:一个是Vector+HashTable,一个是它们的同系容器。在JDK1.2时才被加入的同步包装类(Wrapper),这些类中Collections.synchronizedXXX工厂方法创建的,这些类通过封装它们的状态,并对每一个公共方法进行同步而实现了线程安全。
?
同步容器是线程安全的,但对于复合操作,需要额外的客户端加锁(如synchronized(){})
?
并发容器
Java5.0提供了并发容器,来改进同步容器。
同步容器通过对容器的所有状态进行串行访问,从而实现了线程安全,代价是消弱了并发性;当多个线程共同竞争容器级的锁时,吞吐量会下降。
并发容器是为了多线程并发访问而设计的,如CurrentHashMap。
并发容器替换同步容器,这种做法以很小风险带来了可扩展性显著的提高。
Java5.0添加了2个新的容器类型:Queue和BlockingQueue。Queue是用来临时保存正在等待被进一步处理的一系列元素。
JDK提供了集中实现:包括一个传统的FIFO队列——ConcurrentLinkedQueue;一个(非并发)具有优先级顺序的队列PriorityQueue。
BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作。如果队列为空,一个获取操作会一直阻塞直到队列中存在可用数据;如果队列是满的,插入操作会一直阻塞直到队列存在可用空间。
?
ConcurrentHashMap和HashMap的区别:
HashMap有可能因为hashcode没有很好的分散哈希值,元素很有可能不均衡的分部整个容器;
ConcurrentHashMap虽然和HashMap一样是哈希表,但使用完全不同的锁策略,可用提供更好的并发性、可伸缩性。
在ConcurrentHashMap以前,程序使用一个公共的锁同步每一个方法,并严格限制只能有一个线程可以同时访问容器,而ConcurrentHashMap使用一个更加细化的锁机制:分离锁。
这个机制允许更深层次的共享访问。任意数量的读线程可以并发访问Map,读者和写者也可以并发访问Map,并且有限数量的写线程还可以并发修改Map。因此,该机制为并发访问带来了更高的吞吐量,同时几乎没有损失单个线程访问的性能。
(ConcurrentHashMap没有HashMap同步Map中的独占访问锁)
?
CopyOnWriteArrayList:是同步List的并发替代品,提供了更好的并发性,并避免了在迭代期间时对容器的加锁和复制。
?
阻塞队列和生产者-消费者模式
阻塞队列(BlockingQueue)提供了可阻塞的put/take方法,它们与可定时的offer/poll等价。如果Queue已满,put方法会被阻塞直到有空间可用;如果Queue是空的,那么take会被阻塞直到有元素可用。
阻塞队列支持生产者消费者模式:该模式
分离了“识别需要完成的操作”和“执行工作”。该模式不会发现一个工作并立即处理,而是把工作放入一个任务(to-do)清单中,以备后期处理。
该模式简化了开发,因为它解除了生产者类和消费者类之间相互依赖的代码,生产者和消费者以不同的或者变化的速度生产和消费着数据,将活动解耦。
?
阻塞队列(BlockingQueue)可以使用任意数量的生产者和消费者。
最常见的生产者消费者模式设计是线程池和工作队列的结合。
如果使用有界队列,那么当队列充满时,生产者会阻塞,暂时不能生产更多的工作;从而给消费者有时间追赶速度;
阻塞队列(BlockingQueue)同样会提供了一个offer方法,如果条目不能被加入队列处理,它会返回失败状态,这使得能创建更灵活的策略来处理超负荷工作(减轻负载,序列化剩余工作条目并写入硬盘,减少生产者线程等)。
?
类库中还有一些阻塞队列(BlockingQueue)的实现:Linked BlockingQueue和Array BlockingQueue是FIFO队列;Priority BlockingQueue是优先级队列。
SynchronousQueue:不是队列,因为它不会为队列维护任何元素存储空间,它维护排队的线程清单。
?
Java6新增了2个容器:Deque(读音deck)和Blocking Deque,分别扩展了Queue和BlockingQueue。
Deque是一个双端队列,允许高效的在头尾分别插入、删除,实现它们的是Array Deque和LinkedBlocking Deque。
?
Thread提供了interrupt方法,用来中断一个线程,或者查询某线程是否已经被中断。每个线程都有一个布尔类型的属性,这个属性代表了线程的中断状态,中断线程时需要设置这个值。
中断是一种协作机制,一个线程不能迫使其他线程停止正在做的事,或者去做其他事情,当线程A中断B时,A仅仅是要求B在达到某个方便停止的关键点时停止正在做的事情。
?
Synchronizer
阻塞队列在容器类中是独一无二的:不仅作为对象的容器,而且能协调生产者线程和消费者线程之间的控制流。因为take/put会保持阻塞,直到队列进入了期望状态(不空不满)
Synchronizer是一个对象,根据本身的状态调节线程的控制流,阻塞队列,可以扮演一个Synchronizer的角色的还有:信号量、关卡、闭锁。
所有Synchronizer的特性:封闭状态(这些状态决定着线程执行到某一点时是通过还是等待);提供操作状态的方法;高效的等待Synchronizer进入期望状态的方法。
?
闭锁
延迟线程的进度直到线程到达终止状态。
闭锁工作流程:直到闭锁达到终点状态之前,门一直是闭的,没有线程可以通过,在终点状态到来的时候,门开了,允许所有线程通过。一旦锁到达终点状态,他就不能再改变状态了,所以他会永远都保持敞开状态。
CountDownLatch是一个灵活的闭锁实现,允许一个线程或多个线程等待一个事件集的发生。闭锁的状态包括一个计数器,初始化为一个正数,用来表现需要等待的事件数。CountDown方法对计数器做减法,表示一个操作已经发生,而await方法等待计数器达到0,此时所有需要等待的事件都已发生。
?
FutureTask
同样作为闭锁,FutureTask的计算通过Callable实现,等价于一个可携带结果的Runnable,并有3个状态:等待、运行、完成。
完成包括所有计算以任意的方式结束,包括正常运行结束、取消、异常,一旦FuturetTask进入完成状态,他会永远停止在这个状态。
Future.get的行为依赖于任务的状态,如果他已经完成,get可以立刻返回结果,否则会被阻塞,直到任务转入完成状态,然后返回结果或者抛出异常。
Excecutor框架利用了FuturetTask来完成异步操作。
?
信号量
计数信号量用来控制能够同时访问某特定资源的活动的数量;或同时执行某一给定操作的数量。
计数信号量可以用来实现资源池或者给一个容器设定边界。
一个信号量管理一个有效的许可集,许可的初始量通过构造函数传给信号量。活动能够获得许可(只要还有许可可用),并在使用后释放许可,如果没有了许可,则acquire会被阻塞,直到有许可可用。
Release方法向信号量返回一个许可。
信号量可以用来实现资源池,如DB连接池。
?
关卡Barrier
类似闭锁(一次性使用对象),它们都能阻塞一组线程,直到某些事件发生。
关卡与闭锁的区别:
所有线程必须同时到达关卡点,才继续处理
闭锁等待的是事件,关卡等待的是其他线程
?
CyclicBarrier允许一个给定数量的成员多次集中在一个关卡点,在并行计算中很有用:
这个算法会把一个问题拆成一系列子问题。当线程到达关卡点时,调用await,await会被阻塞,直到所有线程都达到关卡点。如果所有线程都达到了关卡点,关卡会成功突破,这样所有线程都被释放,关卡会重置以备下次使用。如果对await的调用超时或阻塞中线程被中断,那么关卡就被认为是失败的,所有对await未完成的调用都通过BrokenBarrierException终止。如果成功的突破关卡,await会为每个线程返回一个到达索引号,可以用来选举一个领导,在下次迭代中承担部分工作。
?
任务的执行
围绕执行任务来管理应用程序时,第一步要指明一个清晰的任务边界,理想情况下,任务是独立的活动,它们的工作状态并不依赖于其他的任务的状态/结果,或者边界效应.
大部分服务器应用程序都选择的任务边界点:单独的客户去请求.
应用程序内部的任务调度,存在多种可能的调度策略_最简单的是在单一线程中顺序的执行任务.
但顺序的处理几乎不能为服务器应用程序提供良好的吞吐量或快速请求,为了提供更好的响应性,可为每个服务请求创建一个新的线程.
在”中等强度”的负载水平下,”每任务每线程”方法是对顺序执行的良好改造.
但无限制的创建线程会有以下缺点:
线程生命周期的开销(创建/关闭/需要时间_带来延迟);
资源消耗;
?
因此提供了线程池框架_Executor框架
作为Executor框架的一部分,java.util.concurrent提供了一个良好的线程池实现。在Java类库中,任务执行的首要抽象不是Thread,而是Executor(只有execute一个接口)——》异步执行任务
?
为任务提交和任务执行间的解耦提供了良好的标准方法
?
Executor基于生产者——消费者模式
提交任务的是生产者,执行任务的是消费者。如果你要在你的程序中实现一个生产者消费者的设计,Executor是最简单的。Web Server是利用Executor构建的。
当看到new Thread(Runnable).start()时,最好用Executor代替Thread。
?
线程池管理是一个工作者线程的同构池,线程池与工作队列密切绑定的,Executors中静态方法创建线程池:
newFixedThreadPool():创建一个定长的线程池(每当提交任务时就创建一个线程,直到最大);
newCachedThreadPool():创建一个可缓冲的线程池。如果当前线程池长度超过了处理需要,它可以灵活回收;当需求增加时,灵活的添加线程。不会对线程池的长度做出限制;
newSingleThreadPool():创建一个单线程池的Executor。只创建一个工作者线程来执行任务,如果它异常结束,会有另一个线程代替他。Executo按FIFO顺序处理;
newScheduledThreadPool():创建定长线程池,支持定时及周期性的任务的执行。类似Timer。
?
为了解决执行服务生命周期的问题,ExecutorService接口扩展了Executor并添加了生命周期管理方法:
ExecutorService有三种状态:运行,关闭,停止
Shutdown():启动一个平缓的关闭过程:停止接受新任务,等待已经提交的任务的完成(包括尚未开始的任务);
shutdownNow():启动一个强制关闭;
调用awaitTermination等待ExecutorService到达终止状态
IsTerminated():轮询判断Executor是否终止
?
更进一步
CompletionService整合了Executor和BolckingQueue功能,可将Callable任务提交给它去执行,然后使用类似队列中take or pull 方法在结果完整可用时获得这个结果。ExecutorCompletionService是其实现。
?
取消和关闭
要做到安全、快速、可靠的停止任务或线程并不容易。Java没有提供任何机制,来安全的强迫线程停止手头工作。它提供“中断”,一个协作机制,使一个线程能够要求另一个线程停止当前的工作。
?
任务的取消
当外部代码能够在活动自然完成之前,把它更改为完成状态,那么这个活动称之为“可取消的”。
中断
每个线程都有一个布尔型的中断状态,在中断的时候,这个中断状态被设置为true。Thread包含其他用于中断线程的方法,及请求线程中断状态的方法。
阻塞库函数:Thread.sleep 。Object.await 试图监测线程何时被中断,并提前返回。
?
当线程在并不处于阻塞状态的情况下发生中断时,会设置线程的中断状态,然后一直等到被取消的活动获取中断状态,来检查是否发生了中断。所以,如果不能触发InterruptedException,中断状态会一直保持,知道有人特意去消除中断状态。
调用interrupt并不意味着必然停止目标线程正在进行的工作,它仅仅传递了一个请求中断的信息,线程会在自己下一个方便的时候中断(这些时间点称之为取消点)。
静态的interrupted要小心使用,因为它会消除并发线程的中断状态,如果调用的interrupted,并且它返回了true,必须进行处理,如抛异常。
?
中断策略
中断策略决定线程何如应对中断请求。
中断策略最有意义的是对线程级和服务级取消的规定:尽可能迅速退出。
因为每一个线程都有自己的中断策略,所以不应该中断线程,除非知道中断对这个线程意味着什么。
?
响应中断
当调用可中断的阻塞函数时,如Thread.sleep或BlockQueue.put,有2种处理InterruptedException的策略:
<!--[if !supportLists]-->1.???? <!--[endif]-->传递异常,使上层方法也成为可中断的阻塞方法;
<!--[if !supportLists]-->2.???? <!--[endif]-->保存中断状态,上层调用栈中的代码能够对其处理(如再次抛出Interrupt异常来恢复中断状态)
?
只有实现了线程中断策略的代码才可以接收中断请求
?
JVM关闭(可正常关闭或非正常关闭)
在正常的关闭中,JVM首先启动所有已注册的shutdownHook(关闭钩子:是使用Runtime.addShutDownHook注册的尚未开始的线程)
?
JVM并不保证关闭钩子的开始顺序。
?
当所有关闭钩子结束时,如果RunFinalizerOnExit为true,JVM可以选择运行Finalizer,之后停止。
?
JVM不会尝试停止或中断任务关闭时仍然运行中的应用程序线程:它们在JVM最终终止时会被强制退出。
?
如果关闭钩子或Finalizer没有完成,那么正常的关闭进程“挂起”并且JVM必须强制关闭。在强制关闭中,JVM不需要完成除了关闭JVM以外的任务事情,不会运行关闭钩子。
?
关闭钩子是线程安全的,关闭钩子可以用于服务或应用程序的清理,如删除临时文件
?
关闭钩子不应依赖于可能被应用程序或其他关闭钩子关闭的服务。因此实现方法是对所有服务使用唯一的关闭钩子,让它调用一系列的关闭行为,而非每个服务一个关闭钩子。
?
精灵线程
线程分为2种:普通线程和精灵线程。JVM启动时会创建所有的线程,除了主线程以外,其他都是精灵线程,如垃圾回收器。
当一个新的线程创建时,新线程继承了创建它的线程的后台状态,所以默认情况下,任务主线程创建的线程都是普通线程。
?
普通线程和精灵线程的区别仅仅在于退出时会发生什么:
当一个线程退出时,JVM会检查一个运行中线程的详细清单,如果仅仅剩下精灵线程,它会发起正常的退出。
当JVM停止时,所有依然存在的精灵都会被抛弃——不会执行Finalizer,也不会释放栈——JVM直接退出
?
Finalizer
回收资源时,有些资源,如文件或Socket句柄,当不再需要时,必须显示的还给OS。为此,垃圾回收器对那些具有特殊Finalizer方法的对象进行特殊对待:在回收器获得他们后,Finalizer被调用,这样就能保证将持久化的资源释放了
?
因为Finalizer可以允许在一个JVM管理的线程中,因此任何一个Finalizer访问的状态都要同步
使用finally块和显示的close方法比实现Finalizer容易的多。
?
?