四、基础模块构建

一、同步容器类

同步容器类包括Vector和Hashtable,二者是早期JDK的一部分,此外还包括在JDK 1.2 中添加的一些功能相似的类,这些同步的封装器类是由Collections.synchronizedXxx等工厂方法创建的。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

  • 同步容器类的问题

同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。容器上常见的复合操作包括:迭代(反复访问元素,直到遍历完容器中所有元素)、跳转 (根据指定顺序找到当前元素的下一个元素)以及条件运算,例如“若没有则添加”(检査在 Map中是否存在键值K,如果没有,就加入二元组(K,V))。在同步容器类中,这些复合操作在没有客户端加锁的情况下仍然是线程安全的,但当其他线程并发地修改容器时,它们可能会表 现出意料之外的行为。

  • 迭代器与 ConcurrentModificationRxception

在使用迭代器(Iterator)遍历容器出现错误是会出现此异常。

  • 隐藏迭代器

虽然加锁可以防止迭代器抛出ConcurrentModificationException,但你必须要记住在所有对共享容器进行迭代的地方都需要加锁。实际情况要更加复杂,因为在某些情况下,迭代器会隐藏起来。比如容器的toString()方法。

二、并发容器

Java 5.0提供了多种并发容器类来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争 容器的锁时,吞吐量将严重减低。

另一方面,并发容器是针对多个线程并发访问设计的。在Java 5.0中增加了 Concurrent- HashMap,用来替代同步且基于散列的Map,以及CopyOnWriteArrayList,用于在遍历操作为 主要操作的情况下代替同步的List。在新的ConcurrentMap接口中增加了对一些常见复合操作 的支持,例如“若没有则添加”、替换以及有条件删除等。

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

Java 5.0增加了两种新的容器类型:Queue和BlockingQueue。 Queue用来临时保存一组等待处理的元素。它提供了几种实现,包括:ConcurrentLinkedQueue,这是一个传统的先进先出队列,以及PriorityQueue,这是一个(非并发的)优先队列。Queue上的操作不会阻塞,如果队列为空,那么获取元素的操作将返回空值。虽然可以用List来模拟Queue的行为——事实 上,正是通过LinkedList来实现Queue的,但还需要一个Queue的类,因为它能去掉List的随 机访问需求,从而实现更高效的并发。

BlockingQueue扩展了 Queue,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素的操作将一直阻塞,直到队列中出现一个可用的元素。如果队列已满(对于有界队列来说),那么插入元素的操作将一直阻塞,直到队列中出现可用的空间。在“生产者-消费者” 这种设计模式中,阻塞队列是非常有用的。

正如ConcurrentHashMap用于代替基于散列的同步Map, Java 6也引入了 ConcurrentSkipListMap 和 ConcurrentSkipListSet,分别作为同步的 SortedMap 和 SortedSet 的并发替代品 (例如用 synchronizedMap 包装的 TreeMap 或 TreeSet)。

  • ConcurrentHashMap

与HashMap—样,ConcurrentHashMap也是一个基于散列的Map,但它使用了一种完全不 同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一 个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更 大程度的共享,这种机制称为分段锁(Lock Striping)。在这种机制中,任意 数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地 访问Map,并且一定数量的写入线程可以并发地修改Map。ConcurrentHashMap带来的结果是, 在并发访问环境下将实现更高的吞吐量,而在单线程环境中只损失非常小的性能。

尽管有这些改进,但仍然有一些需要权衡的因素。对于一些需要在整个Map上进行计算的 方法,例如size和isEmpty,这些方法的语义被略微减弱了以反映容器的并发特性。由于size 返回的结果在计算时可能已经过期了,它实际上只是一个估计值,因此允许size返回一个近似 值而不是一个精确值。虽然这看上去有些令人不安,但事实上size和isEmpty这样的方法在并 发环境下的用处很小,因为它们的返回值总在不断变化。因此,这些操作的需求被弱化了,以 换取对其他更重要操作的性能优化,包括get、put、containsKey和remove等。

在ConcurrentHashMap中没有实现对Map加锁以提供独占访问。在Hashtable和synchronized- Map中,获得Map的锁能防止其他线程访问这个Map。在一些不常见的情况中需要这种功能, 例如通过原子方式添加一些映射,或者对Map迭代若干次并在此期间保持元素顺序相同。然 而,总体来说这种权衡还是合理的,因为并发容器的内容会持续变化。

  • 额外的原子Map操作

由于ConcurrentHashMap不能被加锁来执行独占访问,因此我们无法使用客户端加锁来创建新的原子操作。但是,一些常见 的复合操作,例如“若没有则添加”、“若相等则移除(Remove-If-Equal)”和“若相等则替换 (Replace-If-Equal)”等,都已经实现为原子操作并且在ConcurrentMap的接口中声明。如果你需要在现有的同步Map中添加这样的功能,那么很可能就意味着应该考 虑使用 ConcurrentMap 了。

  • CopyOnWriteArrayList

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

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

阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到 有元素可用。队列可以是有界的也可以是无界的,无界队列永远都不会充满,因此无界队列上 的put方法也永远不会阻塞。

阻塞队列支持生产者-消费者这种设计模式。该模式将“找出需要完成的工作”与“执行 工作”这两个过程分离开来,并把工作项放入一个“待完成”列表中以便在随后处理,而不是 找出后立即处理。生产者-消费者模式能简化开发过程,因为它消除了生产者类和消费者类之 间的代码依赖性,此外,该模式还将生产数据的过程与使用数据的过程解耦开来以简化工作负 载的管理,因为这两个过程在处理数据的速率上有所不同。

在基于阻塞队列构建的生产者-消费者设计中,当数据生成时,生产者把数据放入队列, 而当消费者准备处理数据时,将从队列中获取数据。生产者不需要知道消费者的标识或数量, 或者它们是否是唯一的生产者,而只需将数据放入队列即可。同样,消费者也不需要知道生产 者是谁,或者工作来自何处。BlockingQueue简化了生产者-消费者设计的实现过程,它支持 任意数量的生产者和消费者。一种最常见的生产者-消费者设计模式就是线程池与工作队列的 组合,在Executor任务执行框架中就体现了这种模式。

在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并 防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

在类库中包含了 BlockingQueue 的多种实现,其中,LinkedBlockingQueueArrayBlockingQueue是FIFO队列,二者分别与LinkedList和ArrayList类似,但比同步List拥有更好的并发性 能。PriorityBlockingQueue是一个按优先级排序的队列,当你希望按照某种顺序而不是FIFO来 处理元素时,这个队列将非常有用。正如其他有序的容器一样,PriorityBlockingQueue既可以 根据元素的自然顺序来比较元素(如果它们实现了 Comparable方法),也可以使用Comparator 来比较。

最后一个BlockingQueue实现是SynchronousQueue,实际上它不是一个真正的队列,因为 它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待 着把元素加入或移出队列。

在java.util.concurrent中实现的各种阻塞队列都包含了足够的内部同步机制,从而,安全地 将对象从生产者线程发布到消费者线程。

  • 双端队列

Java 6增加了两种容器类型,Deque (发音为“deck”)和BlockingDeque,它们分别对 Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的髙. 效插入和移除。具体实现包括ArrayDequeLinkedBlockingDeque
正如阻塞队列适用于生产者-消费者模式,双端队列同样适用于另一种相关模式,即工作 密取(Work Stealing)。在生产者-消费者设计中,所有消费者有一个共享的工作队列,而在 工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的 全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。密取工作模式比传统的生 产者-消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发 生竞争。在大多数时候,它们都只是访问自己的双端队列,从而极大地减少了竞争。当工作者 线程需要访问另一个队列时,它会从队列的尾部而不是从头部获取工作,因此进一步降低了队 列上的竞争程度。
工作密取非常适用于既是消费者也是生产者问题——当执行某个工作时可能导致出现更多 的工作。例如,在网页爬虫程序中处理一个页面时,通常会发现有更多的页面需要处理。类似 的还有许多搜索图的算法,例如在垃圾回收阶段对堆进行标记,都可以通过工作密取机制来实 现髙效并行。当一个工作线程找到新的任务单元时,它会将其放到自己队列的末尾(或者在工 作共享设计模式中,放入其他工作者线程的队列中)。当双端队列为空时,它会在另一个线程 的队列队尾查找新的任务,从而确保每个线程都保持忙碌状态。

四、阻塞方法与中断方法

线程可能会阻塞或暂停执行,原因有多种:等待I/O操作结束,等待获得一个锁,等待从 Thread.sleep方法中醒来,或是等待另一个线程的计算结果。当线程阻塞时,它通常被挂起, 并处于某种阻塞状态(BLOCKED、WAITING或TIMED_WAITING)。阻塞操作与执行时间很长的普通操作的差别在于,被阻塞的线程必须等待某个不受它控制的事件发生后才能继续执行,例如等待I/O操作完成,等待某个锁变成可用,或者等待外部计算的结束。当某个外部事 件发生时,线程被置回RUNNABLE状态,并可以再次被调度执行。

BlockingQueue 的 put 和 take 等方法会抛出受检查异常(Checked Exception) InterruptedException, 这与类库中其他一些方法的做法相同,例如Thread.sleep。当某方法抛出InterruptedException时,表示该方法是一个阻塞方法,如果这个方法被中断,那么它将努力提前结束阻塞状态。

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

中断是一种协作机制。一个线程不能强制其他线程停止正在执行的操作而去执行其他的 操作。当线程A中断B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操 作——前提是如果线程B愿意停止下来。虽然在API或者语言规范中并没有为中断定义任何 特定应用级别的语义,但最常使用中断的情况就是取消某个操作。方法对中断请求的响应度越 高,就越容易及时取消那些执行时间很长的操作。

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

  • 传递InterruptedException。避开这个异常通常是最明智的策略——只需把 InterruptedException传递给方法的调用者。传递InterruptedException的方法包括,根本不捕获 该异常,或者捕获该异常,然后在执行某种简单的清理工作后再次抛出这个异常。

  • 恢复中断。有时候不能抛出InteirraptedException,例如当代码是Runnable的一部分时。 在这些情况下,必须捕获InterruptedException,并通过调用当前线程上的interrupt方法恢复中 断状态,这样在调用栈中更高层的代码将看到引发了一个中断,如下列程序清单。

public class TaskRunnable implements Runnable {
    @Override
    public void run() {
        try{

        } catch (InterruptedException e){
            Thread.currentThread().interrupt();
        }
    }
}

还可以采用一些更复杂的中断处理方法,但上述两种方法已经可以应付大多数情况了。然而在出现InterruptedException时不应该做的事情是,捕获它但不做出任何响应。这将使调用栈上更高层的代码无法对中断采取处理措施,因为线程被中断的证据已经丢失。只有在一种特殊 的情况中才能屏蔽中断,即对Thread进行扩展,并且能控制调用栈上所有更高层的代码。

五、同步工具类

在容器类中,阻塞队列是一种独特的类:它们不仅能作为保存对象的容器,还能协调生产者和消费者等线程之间的控制流,因为take和put等方法将阻塞,直到队列达到期望的状态 (队列既非空,也非满)。

同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(Semaphore),栅栏(Barrier) 以及闭锁(Latch)。

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

  • 闭锁

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

  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行。二元闭锁(包括两个状态)可以用来表示“资源R已经被初始化”,而所有需要R的操作都必须先在这个闭锁上等待。
  • 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。每个服务都有一个相关 的二元闭锁。当启动服务S时,将首先在S依赖的其他服务的闭锁上等待,在所有依赖 的服务都启动后会释放闭锁S,这样其他依赖S的服务才能继续执行。
  • 等待直到某个操作的所有参与者(例如,在多玩家游戏中的所有玩家)都就绪再继续执 行。在这种情况中,当所有玩家都准备就绪时,闭锁将到达结束状态。

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

在程序清单中TestHamess类给出了闭锁的两种常见用法。TestHamess创建一定数量的线程,利用它们并发地执行指定的任务。它使用两个闭锁,分别表示“起始门(Starting Gate)”和“结束门(Ending Gate)”。起始门计数器的初始值为1,而结束门计数器的初始值为 工作线程的数量。每个工作线程首先要做的值就是在启动门上等待,从而确保所有线程都就绪 后才开始执行。而每个线程要做的最后一件事情是将调用结束门的countDown方法减1,这能 使主线程高效地等待直到所有工作线程都执行完成,,因此可以统计所消耗的时间。

public class TestHarness {

    public long timeTasks(int nThreads, final Runnable task) throws InterruptedException {
        final CountDownLatch startGate = new CountDownLatch(1);
        final CountDownLatch endCate  = new CountDownLatch(nThreads);
        for (int i = 0; i < nThreads; i++){
            Thread t = new Thread(){
              public void run(){
                  try {
                      startGate.await();

                      try{
                          task.run();
                      }finally {
                          endCate.countDown();
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
            };

            t.start();
        }
        long start = System.nanoTime();
        startGate.countDown();
        endCate.await();
        long end = System.nanoTime();
        return end - start;
    }

}
  • FutureTask

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

Future.get的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则 get将阻塞直到任务进入完成状态,然后返回结果或者抛出异常。FutureTask将计算结果从执行计算的线程传递到获取这个结果的线程,而FutureTask的规范确保了这种传递过程能实现结果的安全发布。

FutureTask在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算, 这些计算可以在使用计算结果之前启动。程序清单中的Preloader就使用了FutureTask来 执行一个高开销的计算,并且计算结果将在稍后使用。通过提前启动计算,可以减少在等待结 果时需要的时间。

public class Preloader {

    private final FutureTask<ProdunctInfo> future = new FutureTask<>(new Callable<ProdunctInfo>() {
        @Override
        public ProdunctInfo call() throws Exception {

            return loadProductInfo();
        }
    });

    private final Thread thread = new Thread(future);

    public void start() {
        thread.start();
    }

    public ProdunctInfo get() {
        try{

            ProdunctInfo produnctInfo = future.get();
            System.out.println("执行成功");
            return produnctInfo;
        } catch (ExecutionException | InterruptedException e){
            e.printStackTrace();
        }
        return null;
    }


    public ProdunctInfo loadProductInfo(){
        return new ProdunctInfo();
    }


    public static void main(String[] args) {
        Preloader preloader = new Preloader();

        preloader.start();
        
        ProdunctInfo produnctInfo = preloader.get();
        System.out.println(produnctInfo.getClass());

    }

}
  • 信号量

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

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

Semaphore可以用于实现资源池,例如数据库连接池。我们可以构造一个固定长度的资源 池,当池为空时,请求资源将会失败,但你真正希望看到的行为是阻塞而不是失败,并且当池非空时解除阻塞。如果将Semaphore的计数值初始化为池的大小,并在从池中获取一个资源 之前首先调用acquire方法获取一个许可,在将资源返回给池之后调用release释放许可,那么 acquire将一直阻塞直到资源池不为空。在构造阻塞对象池时,一种更简单的方法是使用BlockingQueue来保存池的资源。)

同样,你也可以使用Semaphore将任何一种容器变成有界阻塞容器,如程序清单中的BmmdedHashSet所示。信号量的计数值会初始化为容器容量的最大值。add操作在向底层容 器中添加一个元素之前,首先要获取一个许可。如果add操作没有添加任何元素,那么会立刻释放许可。同样,remove操作释放一个许可,使更多的元素能够添加到容器中。底层的Set实 现并不知道关于边界的任何信息,这是由BoundedHashSet来处理的。

public class BoundedHashSet<T> {
    private final Set<T> set;
    private final Semaphore sem;


    public BoundedHashSet(int bound){
        this.set = Collections.synchronizedSet(new HashSet<>());
        sem = new Semaphore(bound);                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      
    }

    public boolean add(T o) throws InterruptedException {
        sem.acquire(); // 阻塞直到有许可
        boolean wasAdded = false;
        try{
            wasAdded = set.add(o);
            return wasAdded;
        }
        finally {
            if (!wasAdded){
                sem.release(); // 返回一个许可给信号量
            }
        }
    }

    public boolean remove(Object o){
        boolean wasRemoved = set.remove(o);
        if (wasRemoved) {
            sem.release(); // 返回一个许可给信号量
        }
        return wasRemoved;
    }
}
  • 栅栏

栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁 的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。栅栏用于实现一些协议,例如几个家庭决定在某个地方集合:“所有人 6:00在麦当劳碰头,到了以后要等其他人,之后再讨论下一步要做的事情。”

CyclicBarrier可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。如果对await 的调用超时,或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的await调 用都将终止并抛出BrokenBarrierException。如果成功地通过栅栏,那么await将为每个线程返 回一个唯一的到达索引号,我们可以利用这些索引来“选举”产生一个领导线程,并在下一次 迭代中由该领导线程执行一些特殊的工作。CyclicBarrier还可以使你将一个栅栏操作传递给构 造函数,这是一个Runnable,当成功通过栅栏时会(在一个子任务线程中)执行它,但在阻塞 线程被释放之前是不能执行的。

另一种形式的栅栏是Exchanger,它是一种两方(Two-Party)栅栏,各方在栅栏位置上交换数据。当两方执行不对称的操作时,Exchanger会非常有用,例如当一个线程向缓 冲区写入数据,而另一个线程从缓冲区中读取数据。这些线程可以使用Exchanger来汇合,并 将满的缓冲区与空的缓冲区交换。当两个线程通过Exchanger交换对象时,这种交换就把这两 个对象安全地发布给另一方。


猜你喜欢

转载自blog.csdn.net/qq_27870421/article/details/90582817