第五章 基础构建模块
5.1 同步容器类
这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。例如Vector和Hashtable。
5.2 并发容器
ConcurrentHashMap: 并不是使用同步容器类的方法,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。与其他并发容器类一样,迭代器不会抛出ConcurrentModificationException,因此不需要在迭代中对容器加锁。
ConcurrentMap接口加入了对常见复合操作的支持, 比如”缺少即加入(putIfAbsent)”, 替换和条件删除, 而且这些操作都是原子操作。
public interface ConcurrentMap<K, V> extends Map<K, V> { V putIfAbsent(K key, V value); boolean remove(Object key, Object value); boolean replace(K key, V oldValue, V newValue); V replace(K key, V value); }
CopyOnWriteArrayList: 用于替代同步List,并且在迭代期间不需要对容器进行加锁或复制,
写入时复制容器的安全性在于:多个线程可以对该容器进行迭代, 并且不会受到另一个或者多个想要修改容器的线程带来的干涉, 迭代的时候返回的元素严格与创建的时候一致, 不会考虑后续的修改.。
在每次CopyOnWriteArrayList改变时都需要对底层数组进行一次复制, 因此当容器比较大时, 不是很合适, 只有当容器迭代操作的频率远远高于对容器修改的频率, 写入时复制容器是一个合适的选择。
5.3 阻塞队列和生产者-消费者模式
Queue
BlockingQueue提供了可阻塞的put和take方法, 他们与可定时的offer和poll是等价的. 如果Queue已经满了, put方法会被阻塞直到有空间可用; 如果queue是空的, 那么take方法会被阻塞, 直到有元素可用. queue的长度可以有限, 也可以无限.
可以使用BlockingQueue的offer方法来处理这样一种场景: 如果条目不能被加入到队列里, 它会返回一个失败状态. 这样可以创建更多灵活的策略来处理超负荷工作, 比如减轻负载, 序列化剩余工作条目并写入硬盘, 减少生产者线程, 或者其他方法儿子生产者线程.
在类库中包含了BlockingQueue的多种实现,其中,LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,二者分别与LinkedList和ArrayList相似,但比同步的List拥有更好的并发性能。PriorityBlockingQueue是一个按优先级排序的队列,而不是FIFO。
SynchronousQueue
SynchronousQueue是一种BlockingQueue的实现,维护了一个没有存储空间的queue, 如果用洗盘子来比喻的话, 可以认为没有盘子架, 直接将洗好的盘子放到烘干机中. 因为是直接移交, 这样可以减少数据在生产者和消费者移动的延迟.
因为SynchronousQueue没有存储能力, 所以除非另一个线程已经准备好参与移交工作, 否则put和take会一直阻止, 这类队列只有在消费者充足的时候比较合适, 他们总是为下一个任务做好准备.
Deque
Deque(BlockingDeque)是一个双端队列是对Queue和BlockingQueue的扩展, 允许高效的在头和尾分别进行插入和删除, 其实现有ArrayDeque和LinkedBlockingDeque。
双端队列采用的是一种窃取的工作模式, 其原理是每一个消费者都有一个自己的双端队列, 如果一个消费者完成了自己的双端队列中的全部工作, 它可以偷取其他消费者的双端队列中末尾的任务. 由于消费者不会共享同一个队列, 因此相对于传统的生产者-消费者模式具有更高的可伸缩性. 而且即使一个工作者要访问另一个队列, 也是从末尾截取, 这样可以进一步降低对队列的争夺
5.4 同步工具类
1.闭锁CountDownLatch
它可以延迟线程的进度直到其到达中止状态。相当于一扇门,在闭锁到达结束状态之前,没有任何线程能通过,当到达结束状态时,会打开并允许所有的线程通过。当到达结束状态后,将不会再改变状态,永远打开。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行
import java.util.concurrent.CountDownLatch; /** * 在计时测试中使用CountDownLatch来启动和停止线程 * @author cream * */ public class TestHarness { public long timeTasks(int nThreads,final Runnable task) throws InterruptedException{ final CountDownLatch startGate = new CountDownLatch(1); final CountDownLatch endGate = new CountDownLatch(nThreads); for(int i=0;i<nThreads;i++){ Thread t = new Thread(){ public void run() { try{ startGate.await();//每个线程首先在启动门上等待,从而确保所有线程都就绪后才开始执行 try{ task.run();//执行本线程的任务 }finally { endGate.countDown();//任务结束最后将计数器减1 } }catch(InterruptedException ignored){} } }; t.start(); } long start = System.nanoTime();//记录开始时间 startGate.countDown();//所有线程就绪后释放,表示开始 endGate.await(); long end = System.nanoTime(); return end-start; } }
上面的程序是体现了闭锁的两种常用用法,能使主线程高效的等待直到所右工作线程都执行完成,因此可以统计所消耗的时间。
启动门将使得主线程能够同时释放所右工作线程,而结束门则使主线程能够等待最后一个县城执行完成,而不是顺序的等待每个县城执行完成。
2.FutureTask
FutureTask的计算是通过Callable实现的, 它等价于一个可以携带结果的Runnable, 并且有三个状态:
等待运行, 正在运行和运行完成。
运行完成有三种情况:
正常结束, 取消结束和异常结束
一旦FutureTask进入完成状态, 它会永远停止这个状态上。
FutureTask.get()的行为依赖于任务的状态, 如果它已经完成, get可以立即返回结果, 否则会被阻塞,直到任务转入完成状态, 然后会返回结果或者抛出异常.
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; /** * 使用FutureTask来提前加载稍后需要的数据 * @author cream * */ public class Preloader { private final FutureTask<ProductInfo> future = new FutureTask<>(new Callable<ProductInfo>(){ public ProductInfo call() throws Exception { return loadProductInfo();//加载任务 } }); private final Thread thread = new Thread(future); public void start(){ thread.start(); } public ProductInfo get() throws Exception{ return future.get(); } }
创建了一个FutureTask,其中包含从数据库加载信息的任务。由于在构造函数或静态初始化方法中启动线程并不是一种好方法,因此提供了一个start方法来启动线程。当程序需要ProductInfo时,可以调用get方法,如果数据已经加载,那么将返回这些数据,否则将等待数据加载完成后再返回。
3.信号量Semaphore
信号量用来控制能够同时访问某特定资源的活动的数量或者同时执行某一给定操作的数量
技术信号量可以用来实现资源池或者给一个容器设定边界。
一个Semaphore管理一个有效的许可集,许可的初始量通过构造函数来指定。活动能够获得许可, 并在使用之后释放许可, 如果已经没有可用的许可了, 那么acquire会被阻塞,直到有可用的为止(或者直到被中断或者操作超时),release方法向信号量返回一个许可。
一个初始值为1的Semaphore可以用来充当mutex(互斥锁)。
import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Semaphore; /** * 使用Semaphore为容器设置边界 * @author cream * * @param <T> */ public class BoundedHashSet <T>{ private final Set<T> set; private final Semaphore sem; public BoundedHashSet(int n) { set = Collections.synchronizedSet(new HashSet<T>()); sem = new Semaphore(n); } public boolean add(T element) { try { sem.acquire(); //请求信号量 } catch (InterruptedException e) { e.printStackTrace(); } boolean result = false; try { result = set.add(element); }finally { if(!result) sem.release(); } return result; } public void remove(T o) { boolean result = set.remove(o); if (result) { sem.release(); //返回信号量 } } }上述代码中,信号量的计数值会初始化为容器容量的最大值。add操作在向底层容器中添加一个元素之前,首先要获得一个许可。如果add操作没有添加任何元素,那么会立刻释放许可。同样,remove操作释放一个许可,使更多的元素能够添加到容器中。
4. 栅栏Barrier
栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的区别在于,所有线程必须同时到达栅栏位置,才能继续执行。
Barrier允许一个给定数量的成员多次集中在一个栅栏位置,这在并行迭代算法中非常有用, 这个算法会把一个问题拆分成一系列相互独立的子问题, 当线程到达栅栏位置时, 调用await, await将会阻塞所有线程到达栅栏位置,直到所有线程到达关卡点。
package ThreadDemo; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class Cellular { private CyclicBarrier cb; private Worker[] workers; public Cellular() { int count = Runtime.getRuntime().availableProcessors(); workers = new Worker[count]; for (int i = 0; i < count; i++) { workers[i] = new Worker(); } cb = new CyclicBarrier(count, new Runnable() { public void run() { System.out.println("the workers is all end..."); } }); } public void start() { for (Worker worker : workers) { new Thread(worker).start(); } } private class Worker implements Runnable { public void run() { System.out.println("working..."); try { cb.await();//在这里线程阻塞,等待其他线程。 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } } public static void main(String[] args) { Cellular c = new Cellular(); c.start(); } }另一种形式的栅栏是Exchanger,它是一种两方栅栏,各方在栅栏位置上交换数据。类似于一手交钱一手交货。