一、生产者消费者模式
这个模式是我们在生产中常见的模式,大部分都是阻塞队列来构建的,因为这样是为了防止资源的崩溃,以两个人洗盘子为例,二者的劳动分工就是一种生产消费者模式:其中一个人把洗好的盘子放在盘架上,而另一个人从盘架上取出盘子并把他们烘干,在这个示例当中盘架相当于阻塞队列。如果盘架上没有盘子,那么消费者会一直等待,直到有盘子需要烘干,如果盘架放满了,那么生产者会停止清洗直到盘架上有更多的空间。虽然生产者消费者模式能够将生产者和消费者的代码彼此解耦出来,但它们的行为任然会通过共享队列间接地耦合在一起。前面也说了阻塞队列能使这项工工作更加简单,如果阻塞队列并不完全符合设计需求,那么还可以通过信号量来创建其他的阻塞数据结构,下面来看看类库中的BlockingQueue的多种实现:
- LinkedBlockingQueue FIFO(first in first out)队列,比同步LinkedList有更好的并发性
- ArrayBlovkingQueue FIFO(first in first out)队列,比同步的ArrayList有更好的并发性
- PriorityBlockingQueue是一个按优先级排序的队列,当希望按照某种顺序而不是FIFO来处理元素时,可以使用这个队列
- SynchronousQueue实际上不是一个真正的队列,因为他不会为队列中的元素维护存储空间,与其他队列不同的是,他维护一组线程,这些线程在等待着把元素加入或者移出队列。如果以上面的洗盘子为例,那就是相当于没有盘架而是将洗好的盘子直接放入下一个空闲的烘干机中,这种实现队列的方式看似很奇怪,但是由于直接交付工作,从而降低了将数据从生产者移动到消费者的延迟。因为没有存储功能,所以take和put都会一直阻塞,直到有另一个线程已经准备好参与到交付过程中,仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用此同步队列。
二、密取工作模式
java6增加了两种容器类型,Deque和BlockingDeque,他们分别是对Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了队列头和队列尾的高效插入和移除,具体实现包括ArrayDeque和LinkedBlockingDeque。这个队列适用于密取工作模式,这个模式的设计中,每个消费者都有各自的双端队列,如果一个消费者完成了自己双端队列中的全部工作,那么他可以从其他消费者双端队列的末尾秘密获取工作。这种模式在大多数情况下都是只访问,同时访问另外一个队列是从尾部的开始,进一步减少了竞争。使用的场景就是当执行某个工作时导致出现更多的工作,例如网页爬虫程序中处理一个页面时,通常发现有更多的页面需要处理。
三、同步工具类
- 闭锁:可以延迟线程的进度直到其到达终止状态
- FutureTask:通过callable来实现,相当于一种可生成结果的Runnable,有三种状态:等待状态、正在运行和完成运行
- 信号量:计数信号量用来控制同时访问某个特定资源资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。CountDownLatch就是最好的实践。
- 栅栏:栅栏和闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。CyclicBarrier就是做好的实践。
四、构建高效缓存
public interface Computable<A,V> {
V compute(A arg) throws InterruptedException;
}
1.利用HashMap和同步机制初始化缓存
public class Momoizerl<A,V> implements Computable<A,V> {
private final Map<A,V> cache=new HashMap<A,V>();
private final Computable<A,V> c;
public Momoizerl(Computable<A, V> c) {
this.c = c;
}
@Override
public synchronized V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (null==result){
result = c.compute(arg);
cache.put(arg,result);
}
return result;
}
}
说明:hashmap是不安全的数据结构,所以对这个方法加了同步锁,带来一个明显的可伸缩问题,执行效率很低
2.用CurrentHashMap代替HashMap
public class Momoizer2<A, V> implements Computable<A, V> {
private final ConcurrentHashMap<A, V> cache = new ConcurrentHashMap<A, V>();
private final Computable<A, V> c;
public Momoizer2(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (null == result) {
result = compute(arg);
cache.put(arg, result);
}
return result;
}
}
说明:有着更好的并发行为,但是有一个问题就是可能导致计算相同的值,这是缓存中不希望见到的,并且对于单次初始化的对象缓存来说,这个漏洞会带来安全风险
3.基于FutureTask的封装器
public class Momoizer3<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Momoizer3(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException {
Future<V> result = cache.get(arg);
if (null == result) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<V>(callable);
result = futureTask;
cache.put(arg, futureTask);
futureTask.run();
}
try {
return result.get();
} catch (ExecutionException e) {
throw new RuntimeException();
}
}
}
说明:表现了非常好的并发性,若结果已经计算出来,那么久立即返回,如果其他线程正在计算结果,那么新到的线程将一直等待这个结果被计算出来,但是还有一个缺陷就是任然存在两个线程计算出相同的值的漏洞,但是发生的概率已经大大降低了。原因是复合操作时在底层的Map对象上执行的,对这个对象无法通过加锁来确保原子性,可以使用CurrenthashMap中的原子操作putIfAbsent,避免这个漏洞。
4.最终实现
public class Momoizer3<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Momoizer3(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException {
while (true) {
Future<V> result = cache.get(arg);
if (null == result) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> futureTask = new FutureTask<V>(callable);
result = cache.putIfAbsent(arg, futureTask);
if (null == result) {
result = futureTask;
futureTask.run();
}
}
try {
return result.get();
} catch (CancellationException e) {
cache.remove(arg, result);
} catch (ExecutionException e) {
throw new RuntimeException();
}
}
}
}