1.同步容器
同步容器类包含两部分,一个是Vector,一个是Hashtable.还有同步包装类(wrapper),这些类是由Collections.synchronizedXX工厂方法创建的。这些类通过封装它们的状态,并对每一个公共方法进行同步而实现了线程安全,这样一次只有一个线程能访问容器的状态。
同步容器出现的问题:
同步容器虽然是线程安全的,但是对于符合操作,可能需要额外的加锁进行保护。通常的符合操作如:迭代(直到获取容器最后一个元素),导航(navigation,根据一定的顺序找下一个元素),缺少即加入(put-if-absent,检查map中是否有关键字,无则加入),在同步容器中,这些符合操作即使没有锁的保护,也是线程安全的,但是当其他线程能够并发修改容器的时候,他们就不会安全了。
public static Object getLast(Vecttor list){ int lastIndex = list.size() - 1; return list.get(lastIndex); } public static void deketeLast(Vecttor list){ int lastIndex = list.size() - 1; return list.remove(lastIndex); } ---------- for (int i = 0; i < vector.size() ; i++){ doSomething(vector.get(i)); }
这两个方法看起来没有什么危害,无论多少线程同事调用,也不能破坏,但是对于这些方法的调用者来说,情况就不一样,当A线程调用get,从一个包含10的Vector中获取最后一个元素,线程B调用delete删除Vector中的最后一个元素,那么在调get方法时就会出现一个ArrayIndexOutofBoundsException异常。虽然保持了Vector规约的一致性,但是这不是调用者期望的结果。
为了避免上面的问题出现,可以通过对它的对象自身进行加锁,保护它的每一个方法,通过后的容器的锁,就可以是get与delete称为原子操作,可以确保Vector的大小在调用size和get之间不会发生变化。解决下面的问题:
public static Object getLast(Vectro list) { synchronized (list) { int lastIndex = list.size() - 1 ; return last.get(lastIndex); } } public static void deleteLast(Vectro list) { synchronized (list) { int lastIndex = list.size() - 1 ; return last.remove(lastIndex); } } --------这样处理,完全阻止了其他线程在这期间访问它,会削弱并发性---------- synchronized (list){ for(int i = 0 ;i < vectro.size() ; i++){ doSomeThing(vector.get(i)); } }
迭代器与ConcurrentModificationException
对Collection进行迭代的标准方式是Iterator,虽然它也是同步容器,但是它也没有处理在冰法的时候会出现的问题。因此也需要对Iterator容器进行加锁,因此当冰法修改的时候,也是“及时失败”,当一个线程修改,另一个线程获取却已修改或删除,则会抛出ConcurrentModificationException
因此解决以上办法也是需要再对迭代期间持有一个容器锁:
List<Widget> widgetList = Collections.synchronizedList(new ArrayList<Widget>()); for(Widget w : widgetList){ doSomthing(w); }
上面的处理方式,当很多线程访问的时候,就会出现阻塞,因为一个线程必须等待另一线程,会破坏程序的可伸缩性,也可以使用替代办法“复制容器”,因为复制是线程限制(thread-confined)的,没有其他线程能够在迭代期间对其进行修改,这样就消除了ConcurrentModificationException发生的可能性(在复制期间任然要对自己加锁),复制容器会造成额外的性能开销。
2并发容器
同步容器通过对容器的所有状态进行串行访问,从而实现线程安全,这样的代价是削弱并发行,当多个线程共同竞争容器锁时,吞吐量就会降低。
并发容器市委多线程并发访问而设计的,如ConcurrentHashMap来替代同步的哈希Map[hashMap不是同步],当多数操作为读取操作时,CopyOnWriteArrayList【同步】是List【不同步】相应的同步实现,新的ConcurrentMap接口加入常见的符合操作的支持,比如“缺少即加入”、替换和条件删除。
用并发容器替换同步容器,这种做法以很小的风险带来可扩展性显著的提高。
Queue:临时保存正在等待被进一步处理的一系列元素,包括传统的FIFO队列ConcurrentLinkedQueue【同步的】;Queue的操作不会阻塞,如果队列是空的,则获取元素操作会返回空。尽管可以使用List来模拟Queue行为,如LinkedList【不同步】实现了Queue,但是忽略List的随机访问需求的话,使用Queue能够得到更高校的并发实现。
BlickQueue扩展了Queue,增加了可阻塞的插入和获取操作,如果队列是空的,一个获取操作会一直阻塞知道队列中存在可用元素,如果队列是满的,插入操作会一直阻塞知道队列有可用空间。阻塞队列在生产者与消费者设计中非常有用。
PriorityQueue一个非并发具有优先级顺序的队列。
像ConcurrentHashMap,作为哈希Map的一个并发替代品,ConcurrentSkipListMap和ConcurrentSkipListSet,用来作为同步的SortMap和SortSet的并发替代品(例如synchronizedMap包装的TreeMap或TreeSet)
Concurrent HashMap
之前的同步方式处理都是使用一个公共锁同步每一个方法,并严格限制只能有一个现场同时访问容器,而ConcurrentHashMap使用了一个更加细化的锁机制,叫分离锁,允许更深层次的共享访问,任意数量的读现场可并发访问Map,读和写也可以并发访问map,它进一步优化了同步容器类,提供了不会抛出及时失败的ConcurrentModificationException的迭代器,而是具有弱一致性。弱一致性的迭代器可以容许并发修改,当迭代器被创建时,它会遍历已有的元素,并可以感应到在迭代器被创建后,对容器的修改。 因此可以在大多数情况下使用ConcurrentHashMap取代同步的Map[HashTable,SynchronizedMap].但是当程序需要再独占访问中加锁,它就无法胜任了。
CopyOnWriteArrayList
是同步List的一个并发替代品。它提供了更好的并发行,并避免了在迭代器件对容器加锁和复制。等价的CopyOnWriteArraySet是同步Set的一个并发替代品。
”写入时复制“:容器的线程安全来源于:只要有效的不可变对象被正确发布,那么访问它就不需要更多的同步,在每次需要修改时,它们会创建并重新发布一个新的容器拷贝,用以实现可变性。“写入时复制(copy-on-write)”容器的迭代保护一个底层基础数组的引用,这个数组作为迭代器的起点,永远不会被改变。在每次容器复制基础数组时需要一定的开销,特别是容器比较大的时候,当对容器迭代操作的频率远大于对容器修改的频率时,使用“写入时复制”是一个很好的选择。
3.阻塞队列和生产者-消费者模式
阻塞队列BlockingQueue【LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列】。与LinkedList和ArrayList相似,但是却拥有比同步List更好的并发性能。BlockingQueue实际是按照生产者与消费者的方式实现的。当没有数据的时候,会等待put放入数据然后再take获取,当数据满了的时候,会等待有空隙的时候再放入数据。
PriorityBlockingQueue是一个按照优先级顺序排序的列表,当不希望安装FIFO的顺序处理时,可以比较元素本身的自然顺序(如Comparable,也可以使用一个Comparator进行排序)。
SynchronousQueue它不会为队列元素维护任何存储空间。维护的是一个排队的线程清单。这些线程等待元素加入队列或移出队列。举个例子,前面的生产者与消费者模式:使用洗盘子,当洗盘子的人(生产者)将盘子洗好放在盘子架上,当放满就先等待,当存放盘子的人(消费者)将盘子架上的盘子都拿完了,就会等待生产者生产,而这里就相当于省略了盘子架,而是生产者直接生产了然后交给消费者,因此可以将SynchronousQueue看做总是为下一个任务做好准备。
下面是一个例子:
生产者:
public class FileCrawler implements Runnable{ private final BlockingDeque<File> fileQueue; private final File root; private FileFilter fileFilter; public FileCrawler(BlockingDeque<File> fileQueue, FileFilter fileFilter, File root) { this.fileQueue = fileQueue; this.root = root; this.fileFilter = fileFilter; } @Override public void run() { try { crawl(root); } catch (Exception e) { Thread.currentThread().interrupt(); } } private void crawl(File root) throws InterruptedException { File[] entries = root.listFiles(fileFilter); if (entries != null) { for (File file : entries) { if (file.isDirectory()) { crawl(file); } else if (!alreadyIndex(file)) { System.out.println("添加进去"); fileQueue.put(file); } } } } private boolean alreadyIndex(File file) { return fileQueue.contains(file); } }
消费者:
public class Indexer implements Runnable{ private final BlockingDeque<File> queue ; public Indexer(BlockingDeque<File> queue) { this.queue = queue; } @Override public void run() { while (true) { try { System.out.println("拿出来"); indexFile(queue.take()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } private void indexFile(File file) { queue.remove(file); } }
测试:
public class Test { public static void main(String[] args) { File[] roots = new File[]{new File("E:/workspace/Test/bin"),new File("E:/workspace/Test/src")}; BlockingDeque<File> queue = new LinkedBlockingDeque<File>(); FileFilter fileFilter = new FileFilter() { public boolean accept(File pathname) { return true; } }; for (File root : roots) { new Thread(new FileCrawler(queue,fileFilter,root)).start(); } for (int i = 0; i < roots.length; i++) { new Thread(new Indexer(queue)).start(); } } }
3.双端队列和窃取工作
Deque(发英deck)和BlockingDeque,它们分别拓展了Queue和BlockingQueue,是一个双端队列。允许高校地在头和为分别进行插入和移除。实现它们的是ArrayDeque和LinkedBlockiingDeque.
正如阻塞队列适用于生产者-消费者模式一样,双端队列使他们自身与一种叫做“窃取工作”的模式向关联。在“窃取工作”的设计中,每一个消费者都有一个自己的双端队列,如果一个消费者完成了自己双端队列的全部工作,它可以都去其他消费者的双端队列中的末尾任务。因为工作者现场并不会竞争一个共享的任务队列。所以窃取工作模式比传统的生产者-消费者更具有可伸缩性。大多数情况下,它们访问自己的双端队列,减少竞争,当一个工作者必须访问另一个队列时,它会从尾部截取,而不是从头部,从而进一步降低了对双端队列的争夺。
4.阻塞和可中断的方法
线程可能会因为集中原因被阻塞或暂停:等待I/O操作借宿,等待获取一个锁,等待从Thread.sleep中唤醒,或等待拧一个线程计算结果。当一个线程阻塞,通常会被挂起,将状态设置为Blocked,WATING或Time_WAITING.阻塞的线程必须等待一个事件的发生才能继续进行,并且这个事件是超越了它自己控制的。
BlockingQueue的put和take方法会抛出一个受检查的IInterruptedException,Thread.sleep也会抛出该异常,当这个时候是告诉你这个方法是一个可阻塞方法,进一步看,如果它被中断,可以提前借宿阻塞状态。
Threa提供了interrupt这个方法用于中断一个现场,每一个现场都有一个布尔类型的属性,表示线程中断状态,中断线程需要设置这个值。中断是一种写作机制,处理方式有两种:
1.传递,可以将其抛给调用者
2.恢复中断,当你不能讲异常抛出,如代码是Runnable中的run的一部分,必须捕获InterruptedException,并且在当前线程中调用interrupt从中断中恢复。
public class TaskRunnable implements Runnable{ BlockingQueue<Task> queue; public void run(){ try{ processTask(queue.take()); }catch(InterruptedException e){ //恢复中断状态 Thread.currentThread().interrupt(); } } }
5.Synchronizer
Synchronizer是一个对象,它根据本身的状态调节线程的控制力路。阻塞队列可以扮演一个Synchronizer的角色,所有的Synchronizer都享有类似的结构特性,他们封装状态,而这些状态决定者线程执行到某一点时是通过还是被迫等待,它们还提供操作状态的方法,以及高效等待Synchronizer进入期望状态的方法中。
闭锁
闭锁是一种Synchronizer,它可以延迟线程的进度知道线程到达终点状态。一个闭锁工作起来就像一道大门,知道闭锁到达终点状态之前一直都是关闭的,没有线程能够通过。当终点状态到来时,们开了,允许所有线程都通过。一旦闭锁到达终点状态,它就不能再改变状态了,所以永远会保持敞开状态。闭锁可以用来确保特定活动指导其他活动完成后才发生。使用场景:
1.确保一个计算不会执行,指导所需要的资源全部被初始化。一个二院闭锁(两个状态)可以用来表达‘资源R已经被初始化’,并且所有需要用到R的活动受限都要在闭锁中等待。
2.确保一个服务不会开始,指导它依赖的其他服务都已经开始。每一个服务都会包含一个相关的二元闭锁,开启服务S会首先等待闭锁S中锁依赖的其他服务,在启动结束后,会释放闭锁S。这样所有依赖S的服务也就可以开始处理了。
3.等待:直到活动的所有部分都为继续处理做好充分准备,比如在多玩家的游戏中的所有吧玩家是否准备就绪。这样的闭锁就会等所有玩家准备就绪时,大道终点状态。
CountDownLatch是一个闭锁实现,允许一个或多个线程等待一个事件集的发生。闭锁的状态包括一个计数器。初始化一个正数,用来表现需要等待的事件数。countDown方法对计数器做减操作,表示一个事件已经发生了。而await方法等待计数器达到0,此时所有需要等待的事件都已经发生,如果计数器入口值为非0,await会一直阻塞知道计数器为0.或者等待线程中断以及超时。
public class CountDownLatchDemo { final static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) throws InterruptedException { CountDownLatch latch=new CountDownLatch(2);//两个工人的协作 Worker worker1=new Worker("zhang san", 5000, latch); Worker worker2=new Worker("li si", 8000, latch); worker1.start();// worker2.start();// latch.await();//等待所有工人完成工作 System.out.println("all work done at "+sdf.format(new Date())); } static class Worker extends Thread{ String workerName; int workTime; CountDownLatch latch; public Worker(String workerName ,int workTime ,CountDownLatch latch){ this.workerName=workerName; this.workTime=workTime; this.latch=latch; } public void run(){ System.out.println("Worker "+workerName+" do work begin at "+sdf.format(new Date())); doWork();//工作了 System.out.println("Worker "+workerName+" do work complete at "+sdf.format(new Date())); latch.countDown();//工人完成工作,计数器减一 } private void doWork(){ try { Thread.sleep(workTime); } catch (InterruptedException e) { e.printStackTrace(); } } } }
结果:
FutureTask同样可以作为闭锁,它的计算是通过Callable实现的的,它等价于一个可携带结果的Runnable.并且由三个状态:等待、运行,完成。
完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。一旦FutureTask进入完成状态,它会永远停止在这个状态上。
Future.get的行为依赖于任务的状态,如果它已经完成,get可以立刻得到返回结果。否则会被阻塞知道任务转入完成状态,然后返回结果或抛出异常。FutureTask把计算的结果从运行计算的线程传送到需要这个结果的线程;FutureTask的规约保证了这种传递建立在结果的安全发布基础之上。Executor框架利用FutureTask来完成异步任务,并可以用来计算任何潜在的耗时计算,而且可以在真正需要计算结果之前就启动它们开始计算。
单独使用Runnable时: 无法获得返回值 单独使用Callable时: 无法在新线程中(new Thread(Runnable r))使用,只能使用ExecutorService Thread类只支持Runnable FutureTask: 实现了Runnable和Future,所以兼顾两者优点 既可以使用ExecutorService,也可以使用Thread public interface Future<V> Future 表示异步计算的结果。 Future有个get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。 FutureTask是为了弥补Thread的不足而设计的,它可以让程序员准确地知道线程什么时候执行完成并获得到线程执行完成后返回的结果(如果有需要)。 FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,它等价于可以携带结果的Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。 Future 主要定义了5个方法: 1)boolean cancel(boolean mayInterruptIfRunning):试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用 cancel 时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此任务的线程。此方法返回后,对 isDone() 的后续调用将始终返回 true。如果此方法返回 true,则对 isCancelled() 的后续调用将始终返回 true。 2)boolean isCancelled():如果在任务正常完成前将其取消,则返回 true。 3)boolean isDone():如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。 4)V get()throws InterruptedException,ExecutionException:如有必要,等待计算完成,然后获取其结果。 5)V get(long timeout,TimeUnit unit) throws InterruptedException,ExecutionException,TimeoutException:如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 public class FutureTask<V> extends Object implements Future<V>, Runnable FutureTask类是Future 的一个实现,并实现了Runnable,所以可通过Excutor(线程池) 来执行,也可传递给Thread对象执行。如果在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。 Executor框架利用FutureTask来完成异步任务,并可以用来进行任何潜在的耗时的计算。一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。 构造方法摘要 FutureTask(Callable<V> callable) 创建一个 FutureTask,一旦运行就执行给定的 Callable。 FutureTask(Runnable runnable, V result) 创建一个 FutureTask,一旦运行就执行给定的 Runnable,并安排成功完成时 get 返回给定的结果 。 参数: runnable - 可运行的任务。 result - 成功完成时要返回的结果。 如果不需要特定的结果,则考虑使用下列形式的构造:Future<?> f = new FutureTask<Object>(runnable, null) 下面的例子模拟一个会计算账的过程,主线程已经获得其他帐户的总额了,为了不让主线程等待 PrivateAccount类的计算结果的返回而启用新的线程去处理, 并使用 FutureTask对象来监控,这样,主线程还可以继续做其他事情, 最后需要计算总额的时候再尝试去获得privateAccount 的信息。 package test; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * * @author Administrator * */ @SuppressWarnings("all") public class FutureTaskDemo { public static void main(String[] args) { // 初始化一个Callable对象和FutureTask对象 Callable pAccount = new PrivateAccount(); FutureTask futureTask = new FutureTask(pAccount); // 使用futureTask创建一个线程 Thread pAccountThread = new Thread(futureTask); System.out.println("futureTask线程现在开始启动,启动时间为:" + System.nanoTime()); pAccountThread.start(); System.out.println("主线程开始执行其他任务"); // 从其他账户获取总金额 int totalMoney = new Random().nextInt(100000); System.out.println("现在你在其他账户中的总金额为" + totalMoney); System.out.println("等待私有账户总金额统计完毕..."); // 测试后台的计算线程是否完成,如果未完成则等待 while (!futureTask.isDone()) { try { Thread.sleep(500); System.out.println("私有账户计算未完成继续等待..."); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("futureTask线程计算完毕,此时时间为" + System.nanoTime()); Integer privateAccountMoney = null; try { privateAccountMoney = (Integer) futureTask.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("您现在的总金额为:" + totalMoney + privateAccountMoney.intValue()); } } @SuppressWarnings("all") class PrivateAccount implements Callable { Integer totalMoney; @Override public Object call() throws Exception { Thread.sleep(5000); totalMoney = new Integer(new Random().nextInt(10000)); System.out.println("您当前有" + totalMoney + "在您的私有账户中"); return totalMoney; } }
Semaphore(信号量)当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
Semaphore维护了当前访问的个数,提供同步机制,控制同时访问的个数。在数据结构中链表可以保存“无限”的节点,用Semaphore可以实现有限大小的链表。另外重入锁 ReentrantLock 也可以实现该功能,但实现上要复杂些。
下面的Demo中申明了一个只有5个许可的Semaphore,而有20个线程要访问这个资源,通过acquire()和release()获取和释放访问许可。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; public class TestSemaphore { public static void main(String[] args) { // 线程池 ExecutorService exec = Executors.newCachedThreadPool(); // 只能5个线程同时访问 final Semaphore semp = new Semaphore(5); // 模拟20个客户端访问 for (int index = 0; index < 20; index++) { final int NO = index; Runnable run = new Runnable() { public void run() { try { // 获取许可 semp.acquire(); System.out.println("Accessing: " + NO); Thread.sleep((long) (Math.random() * 10000)); // 访问完后,释放 semp.release(); System.out.println("-----------------"+ semp.availablePermits()); } catch (InterruptedException e) { e.printStackTrace(); } } }; exec.execute(run); } // 退出线程池 exec.shutdown(); } }
CyclicBarrier(关卡):类似于闭锁,能够帮忙阻塞一组线程,知道某些事情发生。关卡与闭锁同步点:所有线程必须同时到达关卡点才能继续处理,闭锁等待的是事件,关卡等待的是其他线程。
1、类说明: 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。 2、使用场景: 需要所有的子任务都完成时,才执行主任务,这个时候就可以选择使用CyclicBarrier。 3、常用方法: await public int await() throws InterruptedException, BrokenBarrierException 在所有参与者都已经在此 barrier 上调用 await方法之前,将一直等待。如果当前线程不是将到达的最后一个线程,出于调度目的,将禁用它,且在发生以下情况之一前,该线程将一直处于休眠状态: 最后一个线程到达;或者 其他某个线程中断当前线程;或者 其他某个线程中断另一个等待线程;或者 其他某个线程在等待 barrier 时超时;或者 其他某个线程在此 barrier 上调用 reset()。 如果当前线程: 在进入此方法时已经设置了该线程的中断状态;或者 在等待时被中断 则抛出 InterruptedException,并且清除当前线程的已中断状态。如果在线程处于等待状态时 barrier 被 reset(),或者在调用 await 时 barrier 被损坏,抑或任意一个线程正处于等待状态,则抛出 BrokenBarrierException 异常。 如果任何线程在等待时被 中断,则其他所有等待线程都将抛出 BrokenBarrierException 异常,并将 barrier 置于损坏状态。 如果当前线程是最后一个将要到达的线程,并且构造方法中提供了一个非空的屏障操作,则在允许其他线程继续运行之前,当前线程将运行该操作。如果在执行屏障操作过程中发生异常,则该异常将传播到当前线程中,并将 barrier 置于损坏状态。 返回: 到达的当前线程的索引,其中,索引 getParties() - 1 指示将到达的第一个线程,零指示最后一个到达的线程 抛出: InterruptedException - 如果当前线程在等待时被中断 BrokenBarrierException - 如果另一个 线程在当前线程等待时被中断或超时,或者重置了 barrier,或者在调用 await 时 barrier 被损坏,抑或由于异常而导致屏障操作(如果存在)失败。 4、相关实例 赛跑时,等待所有人都准备好时,才起跑: public class CyclicBarrierTest { public static void main(String[] args) throws IOException, InterruptedException { //如果将参数改为4,但是下面只加入了3个选手,这永远等待下去 //Waits until all parties have invoked await on this barrier. CyclicBarrier barrier = new CyclicBarrier(3); ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit(new Thread(new Runner(barrier, "1号选手"))); executor.submit(new Thread(new Runner(barrier, "2号选手"))); executor.submit(new Thread(new Runner(barrier, "3号选手"))); executor.shutdown(); } } class Runner implements Runnable { // 一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point) private CyclicBarrier barrier; private String name; public Runner(CyclicBarrier barrier, String name) { super(); this.barrier = barrier; this.name = name; } @Override public void run() { try { Thread.sleep(1000 * (new Random()).nextInt(8)); System.out.println(name + " 准备好了..."); // barrier的await方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。 barrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(name + " 起跑!"); } }
关卡转载地址:http://www.itzhai.com/the-introduction-and-use-of-cyclicbarrier.html
6.为计算结果建立高效、可伸缩的高效缓存
下面我们开发一个高效、可伸缩缓存,为一个昂贵的函数保存计算结果,我们从一个最明显的方案开始,然后进行优化:
尝试使用HashMap和同步初始化缓存:
public interface Computable<A,V> { V compute(A arg) throws InterruptedException ; } public class ExpensiveFunction implements Computable<String, BigInteger>{ public BigInteger compute(String arg) { return new BigInteger(arg); } } public class Memoizerl<A,V> implements Computable<A,V>{ private final Map<A, V> cache = new HashMap<A, V>(); private final Computable<A, V> c; public Memoizerl(Computable<A, V> c) { this.c = c; } public synchronized V compute(A arg) throws InterruptedException { V result = cache.get(arg); if (result == null) { result = c.compute(arg); cache.put(arg, result); } return result; } }
上面的处理方式一次只有一个线程能够执行compute,如果另外一个现场正忙着计算结果,则其他调用compute的线程可能被阻塞。者不是我们希望通过缓存得到的性能优化结果。
下面我们使用ConcurrentHashMap取代HashMap,应为ConcurrentHashMap是线程安全的,不需要再底层Map进行同步,这样减少了同步compute带来的冗余代码:
......前两个与上面一样.....
public class Memoizerl<A,V> implements Computable<A,V>{ private final Map<A, V> cache = new ConcurrentHashMap<A, V>(); private final Computable<A, V> c; public Memoizerl(Computable<A, V> c) { this.c = c; } public V compute(A arg) throws InterruptedException { V result = cache.get(arg); if (result == null) { result = c.compute(arg); cache.put(arg, result); } return result; } }
上面存在问题:如果一个线程启动了一个开销很大的计算,而其他线程并不知道这个计算在进行中,可能会重复这个计算。
我们希望当查找f(27)时,当另一个线程已经在查找处理了,那么它要是能够判断最有效的方法就是等待另一个线程处理直到结束,然后只需要动动嘴唇“f(27)是多少呢”。要达到这种效果:FutureTask就可以,它代表了一个计算的过程,可能已经借宿,可能正在运行中,只需要FutureTask.get只要结果可用就立即返回,否则一直阻塞知道被算出来。
用FutureTask记录包装器:
public class Memoizerl<A,V> implements Computable<A,V>{ private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>(); private final Computable<A, Future<V>> c; public Memoizerl(Computable<A, Future<V>> c) { this.c = c; } public V compute(final A arg) throws InterruptedException, ExecutionException { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { @Override public V call() throws Exception { return (V) c.compute(arg); } }; FutureTask<V> ft = new FutureTask<>(eval); f = ft; cache.put(arg, ft); ft.run(); } return f.get(); } }
虽然它展现了非常好的并发性,但是如果新到的线程请求的是其他线程正在计算的结果,它会耐心等待,而两个线程同时计算相同的值。因为if里面的代码是非原子的,因此当两个线程几乎同一时间调用compute计算相同的值,双方都没有在缓存中找到期望的值,则都会开始计算。产生原因:符合操作运行在底层map,不能加锁使它原子化。
缓存一个Future而不是一个值,带来了缓存污染的可能性。如果如果一个计算被取消或者失败,那么未来尝试对这个值进行计算都会表现为失败或取消。为了避免这个结果,当Memoizer发现计算被取消,就会把Furure从缓存中移除,如果发现RuntimeException,则也会移除Future,这样新请求中的计算才会成功。
而Memoizer也会存在缓存过期的问题,但是这些可以通过FutrueTask的一个子类来完成,它会为每一个结果关联一个过期时间,并周期性地烧苗缓存中过期的访问。(但是也不能解决缓存清理不能解决的问题,把旧的计算移除,给新的移出空间)
public class Memoizerl<A,V> implements Computable<A,V>{ private finalConcurrentHashMap<A, Future<V>>cache = new ConcurrentHashMap<A, Future<V>>(); private final Computable<A, Future<V>> c; public Memoizerl(Computable<A, Future<V>> c) { this.c = c; } public V compute(final A arg) throws InterruptedException, ExecutionException { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { @Override public V call() throws Exception { return (V) c.compute(arg); } }; FutureTask<V> ft = new FutureTask<>(eval); f = ft; cache.putIfAbsent(arg, ft); ft.run(); } return f.get(); } }