同步容器类

Vector和HashTable和Collections.synchronizedXXX

都是使用监视器模式实现的。

暂且不考虑性能问题,使用同步容器类要注意:

  • 只能保证单个操作的同步。

这会引起两个问题:

第一个问题:

如果有一个功能,需要计算得到最后一个的值,有这个方法:

list.get(list.size -1)

这是一个复合操作,其实是两步的。存在并发问题。可能会导致ArrayIndexOutOfBoundsException。

如何解决:

synchronized(list)

{

list.get(list.size -1)

}

这样就占用了list的监视器锁,和list的其他方法一起达到了同步。

第二个问题:

对list进行迭代:

不论是使用for循环,还是使用iterator,都是多步操作,所以会有并发隐患。(作者说还有会导致迭代的hashcode equals containsAll等方法,也会存在这种问题。其实我查看了Vector的源代码,这些方法上是加了synchronize的)。

看看java文档的描述:由 Vector 的 iterator 和 listIterator 方法所返回的迭代器是快速失败的:如果在迭代器创建后的任意时间从结构上修改了向量(通过迭代器自身的 remove 或 add 方法之外的任何其他方式),则迭代器将抛出 ConcurrentModificationException。

解决方法,和上面一样。

并发容器

ConcurrentHashMap

CopyOnWriteArrayList

Queue接口 不可阻塞

ConcurrentLinkedQueue Queue实现非阻塞 先进先出 可并发

BlockingQueue接口 继承自Queue 可阻塞

ArrayBlockingQueue可阻塞 可并发 先进先出 有界

LinkedBlockingQueue可阻塞 可并发 先进先出 无界

PriorityQueue Queue实现非阻塞 优先级 不可并发 有界

ConcurrentHashMap

ConcurrentHashMap使用分段锁(可以理解为不是在每个Map上的get put上加锁 而是在Map的entry的put get上加锁)的策略,并没有在每个方法上使用同一个锁。

ConcurrentHashMap返回的迭代器不会返回ConcurrentModificationException。具有弱一致性。弱一致性的迭代器可以容忍并发的修改。当创建迭代器时会遍历所有的已有元素(应该是拷贝一份),并可以(但是不保证)在迭代器被构造后将修改操作反应给容器。

有一些方法,如size和isEmpty,为了保证并发的特性,被减弱了。有可能返回的是一个过期的值。这些不常用的需求被弱化了,以换取一些更加常用的功能的并发性,如get,put,containsKey,remove等。

CopyOnWriteArrayList

ArrayList 的一个线程安全的变体,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。

这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。"快照"风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。

它的迭代器不需要对数组进行加锁或复制。

串行线程封闭

阻塞队列实现的生产者消费者模式,可以达到串行线性封闭。生产者和消费者不可能同时访问流转的对象。如果生产者内部或者消费者内部不会并发的处理流转对象,那么对象在一个时间点只会有一个线程在访问和操作。

双端队列

Deque接口

BlockingDeque接口

ArrayDeque类

LinkedBlockingDeque类

同步工具类

闭锁:

CountDownLatch

注:传入1的时候可以作为开关。前提是在其他线程的第一步先执行开关的await。使用开关的countDown方法就可以打开开关。

使用Future接口(实现类为FutureTask)也可以实现闭锁。其实就是使用get()方法。这个方法wait的。

信号量:

一个计数信号量。从概念上讲,信号量维护了一个许可数量。通过初始化的时候,设置一个许可的数量。acquire()可以获得许可,在许可可用前会阻塞,每个 release() 释放一个许可,从而可能释放一个正在阻塞的获取者。

Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。例如,下面的类使用信号量控制对内容池的访问:

class Pool {

private static final MAX_AVAILABLE = 100;

private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);

public Object getItem() throws InterruptedException {

available.acquire();

return getNextAvailableItem();

}

public void putItem(Object x) {

if (markAsUnused(x))

available.release();

}

// Not a particularly efficient data structure; just for demo

protected Object[] items = ... whatever kinds of items being managed

protected boolean[] used = new boolean[MAX_AVAILABLE];

protected synchronized Object getNextAvailableItem() {

for (int i = 0; i < MAX_AVAILABLE; ++i) {

if (!used[i]) {

used[i] = true;

return items[i];

}

}

return null; // not reached

}

protected synchronized boolean markAsUnused(Object item) {

for (int i = 0; i < MAX_AVAILABLE; ++i) {

if (item == items[i]) {

if (used[i]) {

used[i] = false;

return true;

} else

return false;

}

}

return false;

}

}

将信号量初始化为 1,使得它在使用时最多只有一个可用的许可,从而可用作一个相互排斥的锁。这通常也称为二进制信号量,因为它只能有两种状态:一个可用的许可,或零个可用的许可。相当于一个不可重入的锁。

ps:我的理解:信号量+queue可以实现线程池。

CyclicBarrier

一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。

CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作 很有用。

  1. class Solver {
  2.    final int N;
  3.    final float[][] data;
  4.    final CyclicBarrier barrier;
  5.  
  6.    class Worker implements Runnable {
  7.       int myRow;
  8.  
  9.       Worker(int row) {
  10.          myRow = row;
  11.       }
  12.  
  13.       public void run() {
  14.          while (!done()) {
  15.             processRow(myRow);
  16.  
  17.             try {
  18.                barrier.await();
  19.             } catch (InterruptedException ex) {
  20.                return;
  21.             } catch (BrokenBarrierException ex) {
  22.                return;
  23.             }
  24.          }
  25.       }
  26.    }
  27.  
  28.    public Solver(float[][] matrix) {
  29.         data = matrix;
  30.         N = matrix.length;
  31.         barrier = new CyclicBarrier(N,
  32.                                     new Runnable() {
  33.                                       public void run() {
  34.                                         mergeRows(...);
  35.                                       }
  36.                                     });
  37.         for (int i = 0; i < N; ++i)
  38.           new Thread(new Worker(i)).start();
  39.  
  40.         waitUntilDone();
  41.       }
  42. }

如果任何一个线程在await的时候被中断,或者调用await超时,那么所有的线程的await方法都将终止并且抛出BrokenBarrierException(栅栏已经破碎)。

构建高效且可伸缩的结果缓存

几乎所有的服务器应用都会使用某种形式的缓存。重用之前的计算结果能降低延迟,提高吞吐量,但却要消耗更多内存。看上去简单的缓存,可能会将性能瓶颈转变成伸缩性瓶颈,即使缓存是用来提高单线程性能的。本文将开发一个高效且可伸缩的缓存,用于改进一个高计算开销的计算,我们会从HashMap开始,逐步完善功能,分析它们的并发问题,并讨论如何修改它们。

下面基于一个计算任务开始缓存的设计

public interface Computable <A, R>{

R compute(A a) throws InterruptedException;

}

public class Function implements Computable <String, BigInteger>{

@Override

public BigInteger compute(String a) throws InterruptedException {

return new BigInteger(a);

}

}

第一阶段 HashMap

public class Memorizer1<A, V> implements Computable<A, V>{

private final Computable<A, V> compute;

private final Map<A, V> cache;

public Memorizer1(Computable<A, V> compute){

this.compute = compute;

cache = new HashMap<A, V>();

}

@Override

public synchronized V compute(A a) throws InterruptedException {

V result = cache.get(a);

if(result == null){

result = compute.compute(a);

cache.put(a, result);

}

return result;

}

}

如上所示,Memorizer1将Computable实现类的计算结果缓存在Map<A, V> cache。因为HashMap不是线程安全的,为了保证并发性,Memorizer1用了个很保守的方法,对整个compute方法进行同步。这导致了Memorizer1会有很明显的可伸缩性问题,当有很多线程调用compute方法,将排一列很长的队,考虑到这么多线程的阻塞,线程状态切换,内存占用,这种方式甚至不如不使用缓存。

第二阶段 ConcurrentHashMap

public class Memorizer2<A, V> implements Computable<A, V>{

private final Computable<A, V> compute;

private final Map<A, V> cache;

public Memorizer2(Computable<A, V> compute){

this.compute = compute;

cache = new ConcurrentHashMap<A, V>();

}

@Override

public V compute(A a) throws InterruptedException {

V result = cache.get(a);

if(result == null){

result = compute.compute(a);

cache.put(a, result);

}

return result;

}

}

Memorizer2比Memorizer1拥有更好的并发性,并且具有良好的伸缩性。但它仍然有一些不足——当两个线程同时计算同一个值,它们并不知道有其它线程在做同一的事,存在着资源被浪费的可能。这个不足,对于缓存的对象只提供单次初始化,会带来安全性问题。

第三阶段 ConcurrentHashMap+FutureTask 
事实上,第二阶段的功能已经符合大部分情况的功能,但是当计算时间很长导致很多线程进行同一个运算,或者缓存的对象只提供单次初始化,问题就会很棘手,在这里,我们引入FutureTask来让进行运算的线程获知是否已经有其它正在,或已经进行该运算的线程。

public class Memorizer3<A, V> implements Computable<A, V>{

private final Computable<A, V> compute;

private final Map<A, FutureTask<V>> cache;

public Memorizer3(Computable<A, V> compute){

this.compute = compute;

cache = new ConcurrentHashMap<A, FutureTask<V>>();

}

@Override

public V compute(A a) throws InterruptedException {

V f = cache.get(a);

if(f == null){

Callable<V> eval = new Callable<V>(){

public V call() throw InterruptedException{

return c.compute(arg);

}

}

FutureTask<V> ft = new FutureTask<V>(eval);

f = ft;

cache.put(a, ft);

ft.run();

}

try{

return f.get();

}cache(ExecutionException e){

throw launderThrowable(e.getCause());

}

}

}

Memorizer3缓存的不是计算的结果,而是进行运算的FutureTask。因此Memorizer3首先检查有没有执行该任务的FutureTask。如果有,则直接获得FutureTask,如果计算已经完成,FutureTask.get()方法可以立刻获得结果,如果计算未完成,后进入的线程阻塞直到get()返回结果;如果没有,则创建一个FutureTask进行运算,后续进了的同样的运算可以直接拿到结果或者等待运算完成获得结果。 
Memorizer3的实现近乎完美,但是仍然存在一个问题,当A线程判断没有缓存是,进入到cache.put(a, ft);这一步前,B线程恰好判断缓存为空,B线程创建的FutureTask会把A创建的FutureTask覆盖掉。虽然这相比Memorizer2已经是小概率事件,但是问题还是没根本解决。

第四阶段 ConcurrentHashMap + FutureTask + Map原子操作

第三阶段的ConcurrentHashMap + FutureTask由于存在"先检查再执行"的操作,会有并发问题,我们给cache使用复合操作("若没有则添加"),避免该问题。

public class Memorizer4<A, V> implements Computable<A, V>{

private final Computable<A, V> compute;

private final Map<A, FutureTask<V>> cache;

public Memorizer4(Computable<A, V> compute){

this.compute = compute;

cache = new ConcurrentHashMap<A, FutureTask<V>>();

}

@Override

public V compute(A a) throws InterruptedException {

while(true){

V f = cache.get(a);

if(f == null){

Callable<V> eval = new Callable<V>(){

public V call() throw InterruptedException{

return c.compute(arg);

}

}

FutureTask<V> ft = new FutureTask<V>(eval);

f = cache.putIfAbsent(a, ft);

if(f == null){

f = ft;

ft.run();

}

}

try{

return f.get();

}catch(CancellationException e){

cache.remove(arg, f);

}catch(ExecutionException e){

throw launderThrowable(e.getCause());

}

}

}

}

Memorizer4做了两点改进: 
1. 插入时会再次检查是否有缓存,并且这是个复合操作

f = cache.putIfAbsent(a, ft);

if(f == null){

f = ft;

ft.run();

这里考虑到了一种情况,如果正在运行的FutureTask被终止,那进行该运算的所有请求都会出问题,始料未及的遭遇CancellationException异常。Memorizer4的compute操作是一个循环,当在get()阻塞的线程catch到CancellationException异常,则会再一次申请一个创建FutureTask的机会。

至此,整个设计过程就结束了。我们得到了一个在极端环境下依然能够保证高效且可伸缩运行的结果缓存。

Java并发编程实战 第5章 构建基础模块的更多相关文章

  1. Java并发编程实战 第14章 构建自定义的同步工具

    状态依赖性 定义:只有满足特定的状态才能继续执行某些操作(这些操作依赖于固定的状态,这些状态需要等待别的线程来满足). FutureTask,Semaphroe,BlockingQueue等,都是状态 ...

  2. java并发编程实战学习(3)--基础构建模块

    转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...

  3. Java并发编程实战---第六章:任务执行

    废话开篇 今天开始学习Java并发编程实战,很多大牛都推荐,所以为了能在并发编程的道路上留下点书本上的知识,所以也就有了这篇博文.今天主要学习的是任务执行章节,主要讲了任务执行定义.Executor. ...

  4. Java并发编程实战 第16章 Java内存模型

    什么是内存模型 JMM(Java内存模型)规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对其他线程可见. JMM为程序中所有的操作定义了一个偏序关系,称为Happens-Be ...

  5. 【java并发编程实战】第一章笔记

    1.线程安全的定义 当多个线程访问某个类时,不管允许环境采用何种调度方式或者这些线程如何交替执行,这个类都能表现出正确的行为 如果一个类既不包含任何域,也不包含任何对其他类中域的引用.则它一定是无状态 ...

  6. Java并发编程实战 第8章 线程池的使用

    合理的控制线程池的大小: 下面内容来自网络.不过跟作者说的一致.不想自己敲了.留个记录. 要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析: 任务的性质:CPU密集型任务.IO ...

  7. JAVA并发编程实战---第三章:对象的共享(2)

    线程封闭 如果仅仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭,它是实现线程安全性的最简单的方式之一.当某个对象封闭在一个线程中时,这种方法将自动实现线程安全性,即使被封闭的对象本生不是线 ...

  8. 那些年读过的书《Java并发编程实战》一、构建线程安全类和并发应用程序的基础

    1.线程安全的本质和线程安全的定义 (1)线程安全的本质 并发环境中,当多个线程同时操作对象状态时,如果没有统一的状态访问同步或者协同机制,不同的线程调度方式和不同的线程执行次序就会产生不同的不正确的 ...

  9. java并发编程实战:第二章----线程安全性

    一个对象是否需要是线程安全的取决于它是否被多个线程访问. 当多个线程访问同一个可变状态量时如果没有使用正确的同步规则,就有可能出错.解决办法: 不在线程之间共享该变量 将状态变量修改为不可变的 在访问 ...

随机推荐

  1. 第九章 SpringCloud之Zuul路由

    ############Zuul简单使用################ 1.pom.xml <?xml version="1.0" encoding="UTF-8 ...

  2. IPython4_Notebook

    目录 目录 前言 系统软件 Setup IPython Setup IPython Setup Notebook 临时指定镜像源 Install pyreadline Install pyzmq In ...

  3. Jenkins持续集成环境部署

    一.下载Jenkins Jenkins下载地址:https://jenkins.io/download/ 这里我们下载的是jenkins.war 二.启动Jenkins 在Linux下启动Jenkin ...

  4. 取长文本 READ_TEXT

    ****取长文本  FORM GET_TEXT USING TDID TDNAME. SELECT SINGLE mandt tdobject tdname tdid tdspras    INTO  ...

  5. 通过BDC批量修改物料文档信息 MM02

    *&---------------------------------------------------------------------* *& Report  ZMM_03 * ...

  6. Web02_HTML&CSS

    HTML 表单标签属性介绍 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> ...

  7. Unity中的动画系统和Timeline(3) 混合树和动画匹配

    混合树 前面我们通过在Animation界面添加单独的动作来控制动画,这样做比较麻烦,每个单独的属性,比如站立,奔跑等,都需要单独的代码来控制.现在我们可以通过使用混合树,其基本思想是将相近的动画混合 ...

  8. GIS学习之栅格数据

    栅格数据用一个规则格网来描述与每一个格网单元位置相对应的空间现象特征的位置和取值.在概念上,空间现象的变化由格网单元值的变化来反映.地理信息系统中许多数据都用栅格格式来表示.栅格数据在许多方面是矢量数 ...

  9. 用linux主机做网关搞源地址转换(snat)

    一.原理图  二.环境 外网  A:192.168.100.20 (vmnet1) 网关  B:192.168.100.10 (vmnet1)     192.168.200.10 (vmnet2) ...

  10. sublime3配置php开发环境

    Sublime3 3143 1.用包管理器安装SublimeLinter ctrl+shift+p Package Control: install package SublimeLinter 注意: ...