主要介绍
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将为每一个线程返回一个唯一的到达索引号,我们可以用这些索引选举产生一个领导线程,并在下个迭代中由该领导线程执行一些特殊的工作。