第四章:并发编程-基础构建模块

主要介绍

1.同步容器类

2.并发容器

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

4.阻塞方法与中断方法

5.同步工具类


1.同步容器类

    同步容器类包括Vector和Hashtable,还有Collections.synchronizedXXX等工厂方法。这些类实现线程安全的方式是将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

    同步容器类的问题:

    同步容器类都是线程安全的,但是遇到复合操作比如:迭代、跳转、条件运算 ,这些复合操作在没有客户端加锁的情况下是线程安全的,但是当其他线程并发修改容器,会出现意料之外的行为。

    比如两个线程同时访问Vetor,一个调用getLast,一个deleteLast,可能出现getLast的线程抛出异常:ArrayIndexOutOfBoundsException。我们可以通过客户端加锁解决不可靠迭代的问题,但是要牺牲一些可伸缩性,降低了并发性。

    无论是在直接迭代还是for-each循环语法中,对容器迭代的标准方式是使用Iterator。然而如果有其他线程并发地修改容器,他们会表现出的行为时“及时失败”fail-fast的。会抛出ConcurrentModificationException异常。他们采用的实现方式是,将计数器的变化与容器关联起来:如果在迭代期间计数器被修改,那么hashNext或者next将抛出ConcurrentModificationException异常。这是设计上的权衡,从而降低并发修改操作的检测代码对程序性能带来的影响。

    如果不希望抛出异常,就要在迭代器上加锁,如果不希望加锁,有一种替代方法就是克隆容器,在副本上进行迭代。由于副本是封装在线程内,因此其他线程不会在迭代期间对其进行修改,这样就避免了抛出ConcurrentModificationException(在克隆过程中要对容器加锁)。但是克隆容器时存在显著的性能开销。


2.并发容器

    并发容器改进了同步容器的性能。同步容器将所有对容器的状态的访问都串行化,以实现线程安全性。代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量验证降低。

    并发容器是针对多个线程并发访问设计的,ConcurrentHashMap替代同步且三类的Map,CopyOnWriteArrayList用于在遍历操作为主要操作的情况下代替同步的List。

    java5.0新增了两种新的容器类型:Queue和BlockingQueue。

    Queue用来保存一组等待处理的元素。有几种实现,比如ConcurrentLinkedQueue,这是一个传统的先进先出队列,PriorityQueue 这是一个优先队列。Queue上的操作不会阻塞,如果队列为空,那么获取元素的操作将返回空值。 

      BlockingQueue扩展了Queue,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素的操作将一直阻塞,直到队列中出现一个可用的元素。在生产者-消费者模式中,阻塞队列是非常有用的。

    java 6引入了ConcurrentSkipListMap 和ConcurrentSkipListSet 分别作为同步的SortedMap和SortedSet的并发替代品。


    ConcurrntHashMap使用一种粒度更细的加锁机制实现更大程度的共享,这种机制叫做分段锁。这种机制中,任意数量读取操作可以并发地访问Map,执行读取操作和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。他们提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代器中对容器加锁。ConcurrentHashMap返回的迭代器具有弱一致性。弱一致性的迭代器可能容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以在迭代器被构造将修改操作反映给容器。

    CopyOnWriteArrayList用来替换同步List,它的线程安全性在于,只要正确地发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。只有当迭代操作远远多于修改操作时,才应该使用写入时复制容器。


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

    阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。

    BlockingQueue简化了生产者-消费者设计的实现过程,支持任意数量的生产者和消费者。一种常见的生产者-消费者设计模式就是线程池与工作队列的组合。

    BlockingQueue有多种实现,LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,PriorityBlockingQueue是一个按照优先级排序的队列,可以使用Comparator来比较,SynchronousQueue维护一组线程,这些线程在等待着把元素加入或者移除队列。

    举一个生产者-消费者例子,生产者任务,在某个文件层次结构中搜素符合索引标准的文件,将它们的名称放入工作队列。消费者任务,从队列中取出文件名称并对它们建立索引。

       

import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;

/**
 * 生产者
 * 
 *
 */
public class FileCrawler implements Runnable{
	
	private final BlockingQueue<File> fileQueue;
	private final FileFilter fileFilter;
	private final File root;
	
	public FileCrawler(BlockingQueue<File> fileQueue, FileFilter fileFilter, File root) {
		super();
		this.fileQueue = fileQueue;
		this.fileFilter = fileFilter;
		this.root = root;
	}

	@Override
	public void run() {
		try {
			crawl(root);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}
	
	private void crawl(File root) throws InterruptedException{
		File[] entries = root.listFiles(fileFilter);
		if(entries != null){
			for(File entry : entries){
				if(entry.isDirectory()){
					crawl(entry);
				}else {
					fileQueue.put(entry);
				}
			}
		}
	}

}
import java.io.File;
import java.util.concurrent.BlockingQueue;

/**
 * 消费者
 * 
 *
 */
public class Indexer implements Runnable{
	
	private final BlockingQueue<File> fileQueue;
	

	public Indexer(BlockingQueue<File> fileQueue) {
		this.fileQueue = fileQueue;
	}

	@Override
	public void run() {
		try {
			while(true){
				indexFile(fileQueue.take());
			}
		} catch (Exception e) {
			Thread.currentThread().interrupt();
		}
	}

	private void indexFile(File file) {
		System.out.println("已经被消费:"+file.getName());
		fileQueue.remove(file);
	}

}
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 生产者消费者测试,使用阻塞队列
 *
 */
public class WindowMain {
	
	public static void main(String[] args) {
		
		BlockingQueue<File> fileQueue = new LinkedBlockingQueue<>(100);
		FileFilter fileFilter = new FileFilter() {
			@Override
			public boolean accept(File file) {
				return true;
			}
		};
		File root = new File("XX");
		new Thread(new FileCrawler(fileQueue,fileFilter,root)).start();
		for (int i = 0 ; i< 10; i++){
			new Thread(new Indexer(fileQueue)).start();
		}
	}

}

java6 新增了两种容器类型:Deque 和 BlockingDeque,分别对Queue 和 BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体包含ArrayDeque和LinkedBlokingDeque。

    双端队列适用于工作密取模式。每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。工作密取适合既是消费者又是生产者问题。


4.阻塞方法和中断方法

    线程可能会阻塞或暂停执行,原因有很多:等待IO操作结束,等待获得一个锁,等待从Thread.sleep方法中醒来,或者是等待另一个线程的计算结果,当线程阻塞时,它通常会被挂起,并处于某种阻塞状态。

    线程提供了interrupt方法,用于中断线程或者查询线程是否已经被中断。

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

  •     传递InterruptedException。
  •     恢复中断,捕获了异常后再通过Thread.currentThread().interrupt恢复被中断的状态

5.同步工具类


    阻塞队列可以作为同步工具类,其他的同步工具类包括信号量Semaphore、栅栏Barrier以及闭锁Latch。

    闭锁

     闭锁可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门,在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。

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

       

/**
 * 闭锁
 * 并发执行指定数量的线程,所有的线程都在同一起跑线上,等待所有的线程执行后再统计用时
 *
 */
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();//计数器递减
						}
					} catch (Exception e) {
					}
				}
			};
			t.start();
		}
		
		long start = System.nanoTime();
		startGate.countDown();
		endGate.await();
		long end = System.nanoTime();
		return end - start;
	}
	
	public static void main(String[] args) throws InterruptedException {
		TestHarness testHarness = new TestHarness();
		long time = testHarness.timeTasks(5, new Runnable() {
			
			@Override
			public void run() {
				System.out.println("线程执行");
			}
		});
		System.out.println(time);
	}

}

FutureTask也可以用做闭锁。FutureTask表示的计算是通过Callbale来实现的,相当于一种可生成计算结果的Runnable。Future.get的行为取决于任务的状态,如果任务已经完成,那么get会立即返回结果。否则get会阻塞直到任务进入完成状态。


信号量

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

Semaphore 中管理着一组虚拟的许可,许可的初始数量可以通过构造方法来执行,在执行操作时可以先获取许可,并且在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可。release方法将返回一个许可给信号量。

/**
 * 信号量为容器设置边界
 * @param <T>
 */
public class BoundedHashSet<T>{
	
	private final Set<T> set;
	
	private final Semaphore sem;
	
	public BoundedHashSet(int bound){
		this.set = Collections.synchronizedSet(new HashSet<T>());
		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;
	}

}

栅栏

    栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。

    CyclicBarrier可以使一定数量的参与方反复在栅栏位置集合,它在并行迭代算法中非常有用。

    这种算法通常将一个问题拆分成一系列相互独立的子问题。当线程达到栅栏位置时将调用await方法,这个方法将阻塞直到所有的线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏被重置以便下次使用。如果对await的调用超时或者await阻塞的线程被中断,那么栅栏被认为是打破了,所有阻塞的await调用都将被终止并抛出BrokeBarrierException。如果成功通过栅栏,那么await将为每一个线程返回一个唯一的到达索引号,我们可以用这些索引选举产生一个领导线程,并在下个迭代中由该领导线程执行一些特殊的工作。






猜你喜欢

转载自blog.csdn.net/dxh0823/article/details/80015236