@@@ 委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的
状态即可。
@@@ Java 平台类库包含了丰富的并发基础构建模块,例如线程安全的容器类以及各种用于
协调多个相互协作的线程控制流的同步工具类。
》》同步容器类
@@@ 同步容器类包括 Vector 和 Hashtable 等。这些同步的封装类是由
Collections.synchronizedXxx 等工厂方法创建的。这些类实现线程安全的方式是:将它们的
状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。
### 同步容器类的问题
@@@ 同步容器类是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。
@@@ 容器上常见的复合操作:迭代(反复访问元素,直到遍历完容器中的所有元素)、
跳转(根据指定顺序找到当前元素的下一个元素)、
条件运算
@@@ 由于同步容器类要遵守同步策略,即支持客户端加锁,因此可能会创建一些新的操作,
只要我们知道应该使用哪一个锁,那么这些新操作就与容器的其他操作一样都是原子操作。
@@@ 示例:在使用客户端加锁的 Vector 上的复合操作
---------------------------------------------------------------------------------------------------------------------------------
public static Object getLast( Vector list ){
synchronized ( list ) {
int lastIndex = list.size( ) -1 ;
return list.get( lastIndex ) ;
}
}
public static void deleteLast( Vector list ){
synchronized ( list ){
int lastIndex = list.size( ) -1 ;
return list.remove( lastIndex ) ;
}
}
-------------------------------------------------------------------------------------------------------------------------------
补充:(1)、在上面的代码的基础上,对 Vector 中的元素进行迭代时,可能会抛出
ArrayIndexOutOfBoundsException 的异常。
@@@ 我们可以通过在客户端加锁来解决不可靠迭代的问题,但要牺牲一些伸缩性。通过在
迭代期间持有 Vector 的锁,可以防止其他线程在迭代期间修改 Vector 。然而,这同样会导致
其他线程在迭代期间无法访问它,因此降低了并发性。
----------------------------------------------------------------------------------------------------------------------------------
示例:在客户端加锁的迭代
synchronized ( vector ){
for ( int i = 0 ; i < vector.size( ) ; i++){
doSomething( vector.get(i) ) ;
}
}
-----------------------------------------------------------------------------------------------------------------------------------
### 迭代器与 ConcurrentModificationException
@@@ 无论是直接迭代还是在 Java 5.0 引入的 for-each 循环语法中,对容器类进行迭代的标准
方式都是 Iterator 。
在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是
“ 及时失败 ”的。这意味着,当它们发现容器在迭代过程中被修改时,就会抛出一个
ConcurrentModificationException 异常。
@@@ 有时候开发人员并不希望在迭代期间对容器加锁。持有锁的时间越长,那么在锁上竞争
就可能越激烈,如果许多线程都在等待锁被释放,那么将极大地降低吞吐量和 CPU 的利用率。
@@@ 如果不希望在迭代期间对容器加锁,那么一种替代方法就是 “ 克隆 ” 容器,并在副本上
进行迭代。由于副本被封闭在线程内,因此其他线程不会在迭代期间对其进行修改,这样就避免
了抛出 ConcurrentModificationException (在克隆过程中仍然需要对容器加锁)。
在克隆容器时存在显著的性能开销。这种方式的好坏取决于多个因素,包括容器的大小,
在每个元素上执行的工作,迭代操作相对于其他操作的调用频率,以及在响应时间和吞吐量等
方面的需求。
### 隐藏迭代器
@@@ 虽然加锁可以防止迭代器抛出 ConcurrentModificationException ,但你必须要记住所有
对共享容器进行迭代的地方都需要加锁。实际情况要更加复杂,因为在某些情况下,迭代器会隐藏
起来。
@@@ 标准容器的 toString 方法将迭代容器,并在每个元素上调用 toString 来生成容器内容的
格式化表示。
@@@ 如果状态与保护它的同步代码之间相隔越远,那么开发人员就越容易忘记在访问状态时使用
正确的同步。
@@@ 正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施
同步策略。
@@@ 容器的 hashCode 和 equals 等方法也会间接地执行迭代操作,当容器作为另一个容器的
元素或键值时,就会出现这样的情况。同样, containsAll 、 removeAll 和 retainAll 等方法,以及
把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代器都可能抛出
ConcurrentModificationException 。
》》并发容器
@@@ 同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种方法的代价是
严重降低并发性,当多个线程竞争容器的锁时,吞吐量就严重降低。
@@@ 并发容器是针对多个线程并发访问设计的。
-------- 在 Java 5.0 中增加了 ConcurrentHashMap ,用来替代同步且基于散列的 Map
CopyOnWriteArrayList , 用于在遍历操作为主要操作的情况下代替同步的 List
在新的 ConcurrentMap 接口中增加了对一些常见复合操作的支持,例如 “ 若没有则添加 ” 、
替换以及有条件删除等。
@@@ 通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。
@@@ Java 5.0 增加了两种新的容器类型:
-------- Queue : 用来临时保存一组等待处理的元素
-------- BlockingQueue : 扩展了 Queue , 增加了可阻塞的插入和获取等操作。(有阻塞队列)
补充:
在 “ 生产者---消费者 ” 设计模式中,阻塞队列是非常有用的。
@@@ Java 6.0 也引入了 ConcurrentSkipListMap 和 ConcurrentSkipListSet , 分别作为同步
的 SortedMap 和 SortedSet 的并发替代品(例如用 synchronizedMap 包装的 TreeMap 或
TreeSet )
### ConcurrentHashMap
@@@ 同步容器类在执行每个操作期间都持有一个锁。
@@@ ConcurrentHashMap 并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程
访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁(
Lock Striping)
@@@ ConcurrentHashMap 带来的结果是,在并发访问环境下将实现更高的吞吐量,而在单线程
环境中只损失非常小的性能。
@@@ ConcurrentHashMap 与其他并发容器一起增强了同步容器类:它们提供的迭代器不会抛出
ConcurrentModificationException ,因此不需要在迭代过程中对容器加锁。
@@@ ConcurrentHashMap 中没有实现对 Map 加锁以提供独占访问。
### 额外的原子 Map 操作
@@@ 一些常见的复合操作,例如 “ 若没有则添加 ” 、 “ 若相等则移除 ” 、 “ 若相等则替换 ” 等,
都已经实现为原子操作并且在 ConcurrentMap 的接口中声明。
### CopyOnWriterArrayList
@@@ CopyOnWriterArrayList 用于替代 List , 在某些情况下它提供了更好的并发性能,并且在迭
代期间不需要对容器进行加锁或复制。(类似地,CopyOnWriterArraySet 的作用是替代同步 Set )
@@@ " 写入时复制(Copy-On-Write)"容器的迭代器保留一个指向底层基础数组的引用,这个
数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需要确保数组内容
的可见性。
每当修改容器时都会复制底层数组, 这需要一定的开销。
@@@ 仅当迭代操作远远多于其他操作时,才应该使用 “ 写入时复制 ” 容器。
》》阻塞队列和生产者---消费者模式
@@@ 阻塞队列提供了可阻塞的 put 和 take 方法,以及支持定时的 offer 和 poll 方法。
@@@ 阻塞队列支持生产者--消费者这种设计模式。
---------- 生产者----消费者模式能简化开发过程,因为它消除了生产者类和消费者类之间
的代码依赖性,此外,该模式还将生产数据的过程和使用数据的过程解耦开来以简化
工作负载的管理,因为这两个过程在处理数据的速率上有所不同。
@@@ BlockingQueue 简化了生产者--消费者设计的实现过程,它支持任意数量的生产者和
消费者。一种最常见的生产者---消费者设计模式就是线程池与工作队列的组合,在 Executor
任务执行框架中就体现了这种模式。
@@@ 阻塞队列简化了消费者程序的编码,因为 take 操作会一直阻塞直到有可用的数据。
@@@ 在一些情况下,需要调整生产者线程数量与消费者线程数量之间的比率,从而实现
更高的资源利用率(例如,在“ 网页爬虫【Web Crawler】” 或在其他应用程序中,有无穷的
工作需要完成)。
@@@ 阻塞队列中的 put 方法的阻塞特性极大地简化了生产者的编码。
@@@ 阻塞队列提供了一个 offer 方法,如果数据项不能被添加到队列中,那么将返回一个
失败状态。这样你就能够创建更多灵活的策略来处理负荷过载的情况,例如减轻负载 , 将多
余的工作项序列化写入磁盘,减少生产者线程的数量,或者通过某种方式来抑制生产者线程。
@@@ 在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并
防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。
@@@ 应该尽早地通过阻塞队列在设计中构建资源管理机制--------这将事情做的越早,就越容易。
在许多情况下,阻塞队列能使这项工作更加简单,如果阻塞队列并不完全符合设计需求,那么还
可以通过信号量(Semaphore)来创建其他的阻塞数据结构。
@@@ PriorityBlockingQueue 是一个按优先级排序的队列,当你希望按照某种顺序而不是 FIFO
来处理元素时,这个队列是非常有用的。正如其他有序的容器一样,PriorityBlockingQueue 既
可以根据元素的自然顺序来比较元素(如果它们实现了 Comparable 方法),也可以使用
Comparator 来比较。
### 示例:桌面搜索
@@@ 生产者----消费者模式能带来许多性能优势。生产者和消费者可以并发执行。如果一个是
I / O 密集型, 另一个是 CPU密集型,那么并发执行的吞吐率要高于串行执行的吞吐率。
### 串行线程封闭
@@@ 在 java.util.concurrent 中实现的各种阻塞队列都包含了足够的内部同步机制,从而安全地
将对象从生产者线程发布到消费者线程。
@@@ 对于可变对象,生产者--消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将
对象所有权从生产者交付给消费者。
线程封闭对象只能由单个线程拥有,但可以通过安全地发布该对象来 “ 转移 ” 所有权。在
转移所有权之后,也只有另一个线程能获得这个对象的访问权限,并且发布对象的线程不会再访问
它。这种安全的发布确保了对象状态对于新的所有者来说是可见的,并且由于最初的所有者不会再
访问它,因此对象将被封闭在新的线程中。新的所有者线程可以对该对象做任意修改,因此它具有
独占的访问权。
对象池利用了串行线程封闭,将对象 “ 借给 ”一个请求线程。
### 双端队列与工作密取
@@@ Java 6 增加了两种容器类型:Deque(双端队列)-----------------------> 扩展了 Queue
BlockingDeque------------------>扩展了 BlockingQueue
具体的实现包括: ArrayDeque 和 LinkedBlockingDeque
@@@ 阻塞队列适用于生产者---消费者模式;
-------- 在生产者--消费者模式中,所有消费者有一个共享的工作队列
双端队列适用于工作密取(Work Stealing)
-------- 在工作密取中,每个消费者都有各自的双端队列。
-------- 如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者
双端队列末尾秘密地获取工作。
-------- 当工作者线程需要访问另一个队列时,它会从队列的尾部而不是从队列的头部
获取工作,因此进一步降低了队列上的竞争程度。
@@@ 工作密取非常适用于既是消费者也是生产者问题-------当执行某个工作时可能导致出现
更多的工作。
》》阻塞方法和中断方法
@@@ 线程可能会阻塞或暂停执行,原因有多种:
------- 等待 I / O 操作结束
------- 等待获得一个锁
-------- 等待从 Thread.sleep 方法中醒来
-------- 等待另一个线程的计算结果
@@@ 当线程阻塞时,它通常被挂起,并处于某种阻塞状态(BLOCKED 、WAITING 或
TIMED_WAITING )。
当某个外部事件发生时,线程被置回 RUNNABLE 状态,并可以再次被调度执行。
@@@ BlockingQueue 的 put 和 take 等方法会抛出受检查异常(Checked Exception)
InterruptedException 。 当某个方法抛出 InterruptedException 时,表示该方法是一个阻塞
方法,如果这个方法被中断,那么它将努力提前结束阻塞状态。
@@@ Thread 提供了 interrupt 方法,用于中断线程或查询线程是否已经被中断。每个线程
都有一个布尔类型的属性,表示线程的中断状态,当中断线程时将设置这个状态。
@@@ 中断是一种协作机制。
-------- 最常用中断的方式就是取消这个操作。
-------- 方法对中断请求的响应度越高,就越容易及时取消那些执行时间很长的操作
@@@ 当在代码中调用了一个将抛出 InterruptedException 异常的方法时,你自己的方法也会
变成一个阻塞方法,并且必须要处理对中断的响应。对于库代码来说,有两种基本选择:
------ 传递 InterruptedException
避开这个异常通常是最明智的策略------只需把 InterruptedException 传递给方法的调用者。
传递 InterruptedException 的方法包括,根本不捕获该异常,或者捕获该异常,然后在执行某种简单
的清理工作后再次抛出这个异常。
------ 恢复中断
有时候不能抛出 InterruptedException ,例如当代码是 Runable 的一部分时。在这些情况下,
必须捕获 InterruptedException , 并通过调用当前线程上的 interrupt 方法恢复中断状态,这样在调用
栈中更高层的代码将看到引发了一个中断。
补充:
只有在一种特殊的情况中才能屏蔽中断,即对 Thread 进行扩展,并且能控制调用栈上所有更高
层的代码。
》》同步工具类
@@@ 同步工具类可以是任何一个对象,只要它根据自身的状态来协调线程的控制流。阻塞队列可以作为
同步类工具,其他类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)。
@@@ 所有的同步工具类都包含一个特定的结构化属性:它们封装了一些状态,这些状态将决定执行
同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于
高效地等待同步工具类进入到预期状态。
### 闭锁
@@@ 闭锁是一种同步工具,可以延迟线程的进度直到其到达终止状态。
@@@ 闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门是关闭的,并且没有任何线程能通过,
当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因
此这扇门将永远保持打开状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。
@@@ CountDownLatch 是一种灵活的闭锁实现,可以使一个或多个线程等待一组事件发生。
----------- 闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。
----------- countDown 方法递减计数器,表示有一个事件已经发生了,而 await 方法等待计数器达到零,这
表示所有需要等待的事件都已经发生。
----------- 如果计数器的值非零,那么 await 会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待
超时。
### FutureTask
@@@ FutureTask 也可以用做闭锁。
@@@ FutureTask 表示的计算是通过 Callable 来实现的,相当于一种可生成结果的 Runable ,并且可以
处于以下 3 种状态:等待运行(Waiting to run )
正在运行(Running)
运行完成(Completed)
@@@ " 执行完成 " 表示计算的所有可能结束方式,包括正常结束 、 由于取消而结束和由于异常而结束等。
当 FutureTask 进入完成状态后,它会永远停止在这个状态上。
@@@ FutureTask.get 的行为取决于任务的状态。如果任务已经完成,那么 get 会立即返回结果,否则 get
将阻塞直到任务进入完成状态,然后返回结果或者抛出异常。
@@@ FutureTask 将计算结果从执行计算的线程传递到获取这个结果的线程,而 FutureTask 的规范确保
了这种传递过程能实现结果的安全发布。
@@@ FutureTask 在 Executor 框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算
可以再使用计算结果之前启动。
### 信号量
@@@ 计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行
某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。
@@@ Semaphore 中管理着一组虚拟的许可(permit),许可的初始化数量可通过构造函数来指定。
在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用以后释放许可。
@@@ 计数信号量的一种简化形式是二值信号量,即初始值为 1 的 Semaphore 。二值信号量可以用做
互斥体(mutex) ,并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。
@@@ Semaphore 可以用于实现资源池,例如数据库连接池。
@@@ 在构造阻塞对象池时,一种更简单的方法是使用 BlockingQueue 来保存池的资源。
@@@ 可以使用 Semaphore 将任何一种容器变成有界阻塞容器。
### 栅栏
@@@ 闭锁是一次性对象,一旦进入终止状态,就不能被重置。(而栅栏可以被重置,以便下次使用)
@@@ 栅栏和闭锁的关键区别:
------------- 所有线程必须同时到达栅栏位置,才能继续执行。
闭锁用于等待事件,而栅栏用于等待其他线程。
------------- 栅栏用于实现一些协议,例如几个家庭在某个地方集合:“ 所有人 6:00 在麦当劳碰头,
到了以后等其他人,之后再讨论下一步要做的事情 ”
@@@ CyclicBarrier 可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常
有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。
@@@ CyclicBarrier 可以使你将一个栅栏操作传递给构造函数,这是一个 Runnable ,当成功通过
栅栏时会(在一个子线程任务中)执行它,但在阻塞线程被释放之前是不能执行的。
@@@ 另一种形式的栅栏是 Exchanger ,它是一种两方(Two-Party)栅栏,各方在栅栏位置上交换
数据。当两方执行不对称的操作时, Exchanger 会非常有用。
当两个线程对象通过 Exchanger 交换对象时, 这种交换就把这两个对象安全地发布给另一方。
@@@ 数据交换的时机取决于应用程序的响应需求。
》》构建高效且可伸缩的结果缓存