基础构建模块

委托是创建线程安全类的一个最有效的策略:只需要让现有的线程安全类管理所有的状态即可。

Java平台类库包含了丰富的并发基础的构建模块,例如线程安全的容器类以及各种用于协调多个相互协作的线程控制流的同步工具类(Synchronier)。

同步容器类

Vector和HashTable是早期的,这些同步是由Collections.synchronizedXxx等工厂方法创建的。

同步容器类的问题

正常是安全的,但是复合操作时,需要客户端加额外的锁来保护。Vector的getLast和deleteLast就没有加锁,理论上不是线程安全的。

比如一在迭代,另一边再删等等都会导致问题。(线程独占一般没问题)

迭代器和ConcurrentModificationException

如果不希望迭代的过程受到并发修改的影响,那么就把对象克隆一下,变成局部线程独占对象,就会避免这个问题的出现。

但是这个同样会带来性能和内存的消耗,视具体情况来用。

如果线程加锁的方式,毫无疑问会带来并发降低的问题。

隐藏迭代器

正如封装对象状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略。

容器的hashCode和equals等方法也会间接地执行迭代操作。同样,containsAll、removeAll、retainAll等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有的这些迭代可能都会抛出ConcurrentModificationException。

并发容器

Java5提供了多种并发容器来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化,以实现他们的线程安全。这种方法大大降低了并发性。

并发容器是针对多个线程并发访问设计的。在Java5.0种增加了ConcurrentHashMap,用来替换基于散列的Map,以及CopyOnWriteArrayList,用于在遍历操作为主要操作的情况下替换同步的List。

通过并发容器来代替同步容器,可以极大地提高伸缩性和降低风险。

Java5新增了两种新的容器类型Queue和BlockingQueue。Queue用来临时保存一组等待处理的元素。他提供了几种实现,包括:ConcurrentLinkedQueue,这是一个传统的先进先出队列,以及PriorityQueue,这是一个非并发的优先队列。Queue的操作上不会阻塞,如果队列为空,获取操作返回空。

BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作。在生产者和消费者模式中,这个还是挺有用的。

Java6还引入了ConcurrentSkipListMap和ConcurrentSkipListSet,分别作为同步的SortedMap和SortedSet的并发替代产品。(例如用synchronizedMap包装的TreeMap和TreeSet)。

ConcurrentHashMap

使用分段锁来实现,这是一种细颗粒度的锁。

它的实现已经加锁,所以迭代的过程中,不需要自己再来同步加锁。返回的迭代器是弱一致性的。

注意这个map的size和isEmpty方法是个估计值,它们不具有并发性。

HashTable或者你自己通过synchronizedMap进行加锁是锁住Map,而ConcurrentHashMap并没有真正锁住Map,所以性能好很多,只有在确实需要锁住整个map的时候使用HashTable或者使用synchronizedMap

额外的原子Map操作

由于ConcurrentHashMap不能被加锁来执行独占访问,因此我们无法使用客户端加锁来创建新的原子操作。

那么对于先检条件的约束也就没有了同步,可能会出问题,但是ConcurrentMap这个接口中声明了这些MAp的原子操作,如果你需要原子操作,那么可以考虑使用ConcurrentMap类了。

CopyOnWriteArrayList

用于替换已有的同步List,在某些情况下能提供更好的并发性能,并且在迭代期间不需要对容器进行加锁和复制。(类似的CopyOnWriteArraySet替换同步Set)

它的并发策略是不可变,每次改动都是使用副本,然后替换旧的。

阻塞队列和生产者-消费者模式

阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列满,put阻塞。如果队列空take阻塞。阻塞队列可以不限制大小,那么put永远不阻塞。

一种常见的生产者和消费者模式就是线程池与工作队列的组合,在Executor任务执行框架中就体现了这种模式。

生产者和消费者的数量需要根据具体情况进行配置,防止生产者防止过多任务,也要防止消费者空耗。offer在写入失败时,返回一个失败装态。在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

如果阻塞队列不方便使用的话,还可以使用信号量(Semaphore)来创建其他的阻塞结构。

在类库中包含了BlockingQueue的各种实现,其中,LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列。PriorityBlockingQueue是一个按照优先顺序排序的队列(你可通过实现Comparable方法来实现排序的判断行为)。

最后一个BlockingQueue是SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或者移出队列。当且仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时候,才适合使用同步队列。

串行线程封闭

在Java.util.concurrent中实现的各种阻塞队列都包含了足够的内部同步机制,从而安全地将对象从生产者线程发布到消费者进程。一个线程将自己的对象安全地发布给另一个线程,然后自己不再操作该对象,实现了该对象所有权的转移,这个是安全的,类似于独占了。(队列)

阻塞队列简化了对象所有权的转移,此外还可以通过ConcurrentMap的原子方法remove或者AtomicReference的原子方法compareAndSet来完成这项工作。

双向队列与工作密取

Java6增加了两种容器类型,Deque(发音为deck)和BlockingDeque,它们分别对Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和LinkedBlockingDeque。

正如阻塞队列适用于生产者消费者模式,双端队列同样适用于另一种相关模式,即工作密取(Work Stealing)。每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。密取工作模式比传统的生产者消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在多数时候,他们只是访问自己的双端队列,从而极大地减少了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是头部获取工作,因此进一步降低了队列上的竞争程度。

工作密取队列适用于既是消费者又是生产者问题--当执行某个工作时可能导致出现更多的工作。

阻塞方法和中断方法

线程可能会阻塞或暂停执行,原因有多种:等待I/O操作结束、等待获得一个锁、等待从Thread.sleep方法中醒来,或是等待另一个线程的计算结果。当相应的事件发生后,那么线程的状态变为Runnable等待操作系统调度。

Thread提供了interrupt方法,用于中断线程或者查询线程是不是已经被中断。每个线程都有一个boolean类型的属性,表示线程的中断状态,当中断线程时将设置这个状态。

当在代码中调用了一个将抛出InterruptException异常的方法时,你自己的方法也就变成了一个阻塞方法,并且必须要处理对中断的响应。对于库代码来说,有两种基本选择:

  • 传递:再次抛出
  • 恢复中断:

同步工具类

同步工具类可以是任何一个对象,只要它根据自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及封闭(Latch)。在平台类库中还包含其他一些同步工具类的类,如果这些类还无法满足需要,后面的章节会提到如果建立自己的同步工具类。

所有的同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态。

闭锁

闭锁的作用就相当于一扇门:在闭锁到达结束状态之前,这扇门是一直关闭的,并且没有任何线程能够通过,当达到结束状态时,这扇门会打开并允许所有的线程通过。当闭锁达到结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。例如:

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行。(二元闭锁)
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
  • 等待某个操作的所有参与者都就绪后再继续执行。

CountDownLatch是一种灵活的闭锁实现,可以在上述各种情况中使用,它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器到达0,这就表示所有需要的等待事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器为0,或者等待中的线程中断,或者等待超时。

这里有一个简单的实例:Java 并发专题 :闭锁 CountDownLatch 之一家人一起吃个饭

FutureTask

FutureTask也可以做闭锁[Future语意:表示一种抽象的可生成结果的计算]。FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于以下3种状态:等待运行、正在运行和运行完成。“执行完成”表示计算的所有可能结束方式,包括正常结束、由于取消而结束和由于异常而结束等等。当FutureTask进入完成状态后,它将永远停止在这个状态上。

Future.get的行为取决于任务的状态。如果任务已经完成,那么get立即返回结果,否则阻塞,直到任务完成拿到结果或者抛出异常。

FutureTask在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算可以在使用计算结果之前启动。

具体的列子可以查看:Java并发编程:Callable、Future和FutureTask

信号量

计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量,用于竞态资源。计数信号量还可以用来实现某种资源池,或者对容器施加边界。

Semaphore中管理着一组虚拟的许可(Permit),许可的初始数量可通过构造函数来指定。在执行操作的时候可以先获得许可(只要还有剩余的许可),并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可(或者直到被中断或者操作超时)。release方法将返回一个许可给信号量。计算信号量的一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量可以用作互斥体(mutex),并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。

Semaphore可以用于实现资源池,例如数据库连接池。Semaphore计数初值初始化为数据库连接池的大小。

同样,你也可以使用Semaphore将任何一种容器变成有界阻塞容器,初始计数值会初始化为容器容量的最大值。add操作在向底层容器中添加一个元素之前,首先要获得一个许可。如果add失败就要立即释放许可,同样remove释放一个许可。

栅栏

栅栏类似于闭锁,它能阻塞直到某个事件发生。栅栏和闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。栅栏用于实现一些协议,例如某个步骤中的计算可以并行执行,但必须等到该步骤中的所有计算都执行完成才能进入下一个步骤。

并行迭代,将一个问题分成很多子问题,当一系列的子问题都解决之后(所有子问题线程都已经await()),此时将栅栏打开,所有子问题线程被释放,而栅栏位置可以留着下次使用。

CyclicBarrier是栅栏系统的一个实现,我们可以查看一下实例(同时比较一下与闭锁的区别):

java多线程并发系列之闭锁(Latch)和栅栏(CyclicBarrier)

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

几乎所有的服务器应用程序都会使用某种形式的缓存。重用之前的计算结果能降低延迟,提高吞吐量,但是会消耗更多的内存。

简单的缓存可能会将性能瓶颈转变成可伸缩性瓶颈,即使缓存是用于提升单线程的性能。下面我们将设计一个高效且可伸缩的缓存,用于改进一个高计算开销的函数。我们首先从简单的HashMap开始,然后分析它的并发缺陷,并讨论如何修复它。

具体的内容查看原书吧,这里就不写了。具体思想如下:

  1. 不能将长时间计算或者耗时的操作放入同步块中。
  2. 第二个程序换成了ConcurrentHashMap,去掉了同步块,但是计算可能会重复,这样也就失去了缓冲的意义。
  3. 第三个程序使用FutureTask对计算操作进行互斥执行,这样就可以保证耗时计算只能执行一次,注意这里和同步是有区别的,细细体会!但是它还有漏洞,任然存在相同计算的问题,只是概率小了很多(即使缓冲查询和计算之间没同步,没有原子性的操作)。
  4. 这是一个复合操作(“若没有就添加”)是在底层的Map对象上执行的,而这个对象无法通过加锁来保证它的原子性。使用ConcurrentHashMap的putIfAbsent来解决这个问题。

当缓存的是Future而不是值时,将导致缓存污染问题:如果某个计算被取消或者失败,那么在计算这个结果时将指明计算过程被取消或者失败。为了避免这种情况,如果Memoizer发现计算被取消,那么将把Future从缓存中移除。如果检测到RuntimeException,那么也会移除Future,这样将来的计算才有可能成功。

results matching ""

    No results matching ""