Author:Martin
E-mail:mwdnjupt@sina.com.cn
CSDN Blog:http://blog.csdn.net/ictcamera
Sina MicroBlog ID:ITCamera
Main Reference:
《Java并发编程实战》 Brian Goetz etc 童云兰等译
《Java并发设计教程》 温绍锦
1. 同步容器
1.1. 哪些是同步容器
同步容器包括Vector和HashTable,这两者是早期JDK的一部分。此外还包括在JDK1.2添加的类似功能,这些同步容器由Collections.synchronizedXxx()等工厂方法创建,它的线程安全的机制是通过组合(适配)的方式,将原来的非线程安全的容器封装起来,使用一个锁(新建的互斥锁)将每个公有的方法都同步(详细可以看源代码了解下),使得每次只有一个线程能访问容器的状态(这个思路已经在“线程安全与对象组合”中的“在现有的犬类中添加功能”的“组合”中已经介绍)。
1.2. 同步容器的问题
复合操作原子性问题
同步容器都是线程安全的,但是常见的复合操作还是不具有原子性,因此需要客户(代码)需要加锁来保护复合操作。比如:迭代(反复访问元素,直至遍历完容器中的所有元素),跳转(根据指定顺序找到当前元素的下一个元素),条件运算(如若没有则添加-先检查执行)。例如下面所示:
publicstatic Object getLast(Vector list){ int lastIndex=list.size()-1; return list.get(lastIndex); } publicstaticvoid deleteLst(Vector list){ int lastIndex=list.size()-1; list.remove(lastIndex); } |
因此需要客户端加锁,注意锁一定要使用容器自身的锁对象,如下面所示
publicstatic Object getLast(Vector list){ synchronized (list){ int lastIndex=list.size()-1; return list.get(lastIndex); } } publicstaticvoid deleteLst(Vector list){ synchronized (list){ int lastIndex=list.size()-1; list.remove(lastIndex); } } |
上面获取最后的元素的例子中,在调用size和响应的get之间Vector的长度可能发生变化,这种风险对于对vector这种容器进行迭代时也会出现,可能出现ArrayIndexOutOfBoundsException异常,例如
For(int i=0;i<vector.size;i++){
doSomeThing(vector.get(i));
}
当然下面这种加锁法可以解决不可靠的迭代,但是要牺牲性能。
Synchronized(vector){
For(int i=0;i<vector.size;i++){
doSomeThing(vector.get(i));
}
}
容器的CurrentModificationException问题
Java5.0引入了迭代器,具有迭代器的同步容器在迭代过程中没有对容器加锁,迭代器在迭代的过程中如果被修改(内部有计数器来关联容器的迭代),那么就会出现CurrentModificationException异常,也就出现了著名的容器的CurrentModificationException问题,如下下面所示:
List<Object >.list=Collections.synchronizedList(new ArrayList);
For(O bject :obj){
doSomeThing(obj);
}
一般不会在迭代的时候将容器加锁,因为一方面可能容器规模很大,迭代时间很长会影响性能,另外如果在迭代的时候加锁,那么在调用如上面的doSomeThing时可能出现死锁。如果不希望在迭代期间对容器加锁,那么一种替代的方法就是“克隆”容器,并在副本上迭代,但是在克隆的时候任然需要加锁,如果对象很大克隆还是会影响性能。
隐藏的迭代器
加锁可以防止迭代器出现CurrentModificationException,但是需要在所有对共享容器进行迭代的地方都进行加锁,实际情况非常复杂,必须要主要隐藏的迭代调用,例如下面:
class HidenIterator{ privatefinal Set<Integer>set=new HashSet<Integer>(); publicsynchronizedvoid add(Integer i){set.add(i);} publicsynchronizedvoid delete(Integer i){set.remove(i);} publicvoid addTenThings(){ Random r=new Random(); for(int i=0;i<10;i++){ set.add(r.nextInt()); } System.out.println(set); } } |
这里打印这个set的时候会调用set的tostring方法,从而对set的迭代。这也说明了如果对象的状态(HidenIterator类set)和保护他的代码相隔比较远的话我们容易忘记使用正确的同步。一般调用容器的hashCode和equals等方法也会间接地执行迭代操作,而当容器作为另外一个容器的元素或者键值时就会调用容器的hashCode和equals等方法。同样调用容器的containsAll,removeAll,retainAll以及将容器作为构造函数的参数时,都会对容器进行迭代。未加锁的迭代均有可能出现CurrentModificationException异常。
2. 并发容器
2.1. 并发容器概述
Java5.0开始提供了并发容器,相对同步容器而言,并发容器通过一些机制改进了并发性能。因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量严重降低。因此Java5.0开始针对多线程并发访问设计,提供了并发性能较好的并发容器,引入了java.util.concurrent包。与Vector和Hashtable、Collections.synchronizedXxx()同步容器等相比,util.concurrent中引入的并发容器主要解决了两个问题:
1. 根据具体场景进行设计,尽量避免synchronized,提供并发性
2. 定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错
util.concurrent中容器在迭代时,可以不封装在synchronized中,可以保证不抛异常,但是未必每次看到的都是“最新的、当前的”数据。如果说将迭代操作包装在synchronized中,达到的是“可序列化”程度的并发安全性,那么util.concurrent中的迭代达到的是“脏读”程度。下面是对并发容器的简单介绍:
ConCurrentHashMap代替同步的Map(Collections.synchronized(new HashMap())),众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候锁住了所有的段,而ConCurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。ConCurrentHashMap也增加了对常用复合操作的支持,比如“若没有则添加”、替换、有条件的增加等。
CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是“克隆”容器对象。
ConCurrentLinkedQuerue是Query实现,是一个先进先出的队列。一般的Queue实现中操作不会阻塞,如果队列为空,那么取元素的操作将返回空。Queue一般用LinkedList实现的,因为去掉了List的随机访问需求,因此并发性更好。
BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作,如果队列为空,那么获取操作将阻塞,直到队列中有一个可用的元素。如果队列已满,那么插入操作就阻塞,直到队列中出现可用的空间。
ConcurrentSkipListMap可以在高效并发中替代SoredMap(例如用Collections.synchronzedMap包装的TreeMap)。
ConCurrentSkipListSet可以在高效并发中替代SoredSet(例如用Collections.synchronzedSet包装的TreeMap)。
2.2. ConCurrentHashMap
ConCurrentHashMap实现和HashMap一直,也采用了散列机制,但是采用了分段锁(Lock Striping)机制提供了并发性能,结果是并发环境下实现更的吞吐量,而在单线程环境下只损失非常小的性能。
ConCurrentHashMap已经增加很多常用的复合操作,比如“若没有则增加”,“若相等则移除”、“若相等则替换”等等,但是ConCurrentHashMap不能被加锁来执行独占访问,因此无法使用客户端加锁来增加新的原子操作。
ConCurrentHashMap和其他并发容易一样改进了同步容器的问题,它们提供的迭代器不会抛出CurrentModificationException异常,一次你不需要在迭代过程中加锁。ConCurrentHashMap返回迭代器具有弱的一致性而非同步容器那样“及时失败”,弱一致性的迭代器可以容忍并发修改,当创建迭代器时会创建已有的元素,可以(但是不保证)在迭代器被构造后将修改操作反映给容器。
ConCurrentHashMap弱一致性使得像size和isEmpty方法减弱了,即size和isEmpty返回的结果在计算时可能是过期了,只是一个估计值,而不是精确值,但那是这种两个方法在并发环境下用处很小,因为它们返回值总在不断变化。这些操作被弱化,换来的是对重要操作的性能优化,包括get、put、containsKey、remove等。
ConCurrentHashMap比同步容器HashTable和同步Map性能要好,除实在需要用同步容器的情况,一般都建议用ConCurrentHashMap。
2.3. CopyOnWriteArrayList
CopyOnWriteArrayList用于替代同步List,在一些情况下它提供了更好的并发性,并且在迭代期间不需要对容器进行加锁或复制(CopyOnWriteArraySet也类似)。“写入时复制(Copy-On-Write)”容器的迭代器不会抛出CurrentModificationException异常,并且返回的元素与迭代器创建时的元素完全一致,不必考虑之后修改操作带来的影响。
2.4. BlockingQueue
BlockingQueue提供了可阻塞的put和take方法,以至此定时的offer和poll方法。BlockingQueue支持生产-消费者模式。在类库中包含了BlockingQueue的多种实现,其中LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,两者分别与LinkedList和ArrayList类似,但比同步List拥有更好的并发性。
2.5. 并发容器总结
容器 | 场景 | 实现方式 | 迭代的实现方式 | 其他 |
ConcurrentHashMap | 所有并发场景 | 采用分段锁。将Hash空间分为几个Segment,每个Segment独立进行锁控制。 | 在构建迭代器时,会将当前的hash表复制到一个数组中。实际上迭代的是这个数组,而不是真正的hash表。 | 实现了ConcurrentMap中定义的几个复合操作接口 |
ConcurrentSkipListMap/ConcurrentSkipListSet | 需要排序的Map和Set的场景 | 采用CAS实现,比较复杂 | 没有CAS,也没有锁。同LinkedBlockingQueue类似, 每一次next为下一次的next和hasNext准备数据。 | 实现了ConcurrentMap中定义的几个复合操作接口 |
CopyOnWriteArrayList | 适用于迭代操作远远多于修改操作的场景 | 内部维护一个数组,每一次修改操作都重新生成新的数组 | 构建迭代器时,关联当前的内部数组。所以之后的变更不会对迭代器产生影响。 |
|
| 本质上就是CopyOnWriteArrayList,只是适配了Set接口。 |
|
| |
LinkedBlockingQueue、ArrayBlockingQueue | 生产者-消费者场景 | 和ArrayList和LinkedList类似,分别采用数组和链表结构。通过锁进行并发管理,操作前获取锁,操作完释放锁,是一种比较“古典”的并发实现方式。 | 迭代器的next操作需要获取锁,和容器的其他操作是互斥的。原理如下: T1时刻:next()返回T0缓存的结果,缓存下一个元素E T1`时刻:集合中元素E被删除。 T2时刻:hasNext返回true,next()返回E。缓存下一个元素F…. 每次next都将当前时刻的下一个元素缓存起来,作为后续hasNext和next的结果。 |
|
PriorityBlockingQueue | 生产者-消费者场景 | 内部元素根据Comparable接口排序。也是通过锁管理并发。 | 与ArrayBlockingQueue不同,创建迭代器时执行容器的toArray方法获取副本。随后就在这个副本中迭代。 |
|
3. 同步工具类
线程的wait-nofity方法是Java线程之间基础的交互工具,而同步工具类是JDK5.0引入的针对特定场景的线程交互工具。同步工具类可以是任何一个对象,只要它能根据其自身的状态来协调线程的控制流。因此,上述的并发容器中的阻塞队列也可以视为同步工具类,其他的同步工具类还包括闭锁(Latch)、栅栏(Barrier)以及信号量(Semaphore)等等。所有同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另外一些方法用于高效地等待同步工具类进入到预期状态。
3.1. 闭锁(Latch)
在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。即,一组线程等待某一事件发生,事件没有发生前,所有线程将阻塞等待;而事件发生后,所有线程将开始执行;闭锁最初处于封闭状态,当事件发生后闭锁将被打开,一旦打开,闭锁将永远处于打开状态。
闭锁CountDownLatch唯一的构造方法CountDownLatch(int count)。当闭锁上调用await()方法时挂起线程,等待闭锁计数器为0。当在闭锁上调用countDown()方法时,闭锁的计数器将减1,当闭锁计数器为0时,闭锁将打开,所有线程将通过闭锁开始执行。
为了加深理解,举例说明,比如甲、乙、丙、丁这四人相约周末早上去公园自驾游,假设四人的家里距离公园的距离相差不大,车程大概为2小时,公园开门时间为早上八点,为了使大家的行程大致,大家约定早上7点从家里出发,并且设置好闹钟,闹钟响后同时从家里出发,记录四人的出发和到达时间。这个例子中闹钟就是一个闭锁,闹钟响起大家同时从家里出发。代码如下:
publicclass LatchVisitors { publicstaticvoid main(String[] args){ finalString[] visitors =newString[] {"甲","乙","丙","丁" }; finalint visitorNum = visitors.length; final CountDownLatch startLatch =new CountDownLatch(1); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try{ //让各个游客做好出发前的工作,等闹钟想起同时出发 for(int i = 0; i < visitorNum; i++){ new Thread(new LatchVisitor(visitors[i], sdf, startLatch)).start(); } //使各个游客家的闹钟同时响起,同时从家里出发,打开闭锁 nofity Thread Date allStartDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(allStartDate) +" all to start !"); startLatch.countDown(); }finally{
} } } class LatchVisitorimplements Runnable{ final CountDownLatch start; finalString name; final SimpleDateFormatsdf; public LatchVisitor(finalString name, SimpleDateFormat sdf, final CountDownLatch start){ this.name = name; this.start = start; this.sdf = sdf;
} @Override publicvoid run(){ try{ //等待早上闹钟想起,出发去公园 wait Thread start.await(); //闹钟想起,从家里出发,模拟从家出发到公园门口 Date startDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(startDate) +" " + this.name + " begin going to the park!"); Thread.sleep((long) (Math.random() * 10000)); }catch(Throwable e){ e.printStackTrace(); }finally{ //到达公园 Date arrivedDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(arrivedDate) +" " + this.name + " arrived the park!"); } } } |
输出如下
2013-10-09 10:28:01 all to start ! 2013-10-09 10:28:01 甲 begin going to the park! 2013-10-09 10:28:01 乙 begin going to the park! 2013-10-09 10:28:01 丁 begin going to the park! 2013-10-09 10:28:01 丙 begin going to the park! 2013-10-09 10:28:05 丙 arrived the park! 2013-10-09 10:28:08 丁 arrived the park! 2013-10-09 10:28:10 乙 arrived the park! 2013-10-09 10:28:11 甲 arrived the park! |
3.2. 栅栏(Barrier)
它允许一组线程互相等待,直到到达某个公共屏障点。利用栅栏,可以使线程相互等待,直到所有线程都到达某一点,然后栅栏将打开,所有线程将通过栅栏继续执行。CyclicBarrier支持一个可选的 Runnable 参数,当线程通过栅栏时,runnable对象将被调用。构造函数CyclicBarrier(int parties, Runnable barrierAction),当线程在CyclicBarrier对象上调用await()方法时,栅栏的计数器将增加1,当计数器为parties(设定的需要相互等待的线程数)时,栅栏将打开。
同样是上面的例子,这次没有规定精确的出发时间,但是约定大家了只有四个人都到了公园后才一起进入公园,否则就在公园门口等待,记录这四人出发时间和到达时间。这时就需要设置一个栅栏来等待所有人到达公园门口的事件发生。代码如下:
publicclass BarrierVisitors { publicstaticvoid main(String[] args){ final String[] visitors =new String[] {"甲","乙","丙","丁" }; finalint visitorNum = visitors.length; final SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置栅栏,四个游客全部到达后一起进入公园 final CyclicBarrier arrived =new CyclicBarrier(visitorNum,new Runnable() { @Override publicvoid run() { Date arrivedDate =new Date(System.currentTimeMillis()); System.out.println(sdf.format(arrivedDate) +" all Arrived!"); } });
for(int i = 0; i < visitorNum; i++){ new Thread(new BarrierVisitor(visitors[i], sdf, arrived)).start(); try{ Thread.sleep(1000); }catch(InterruptedException e){ //TODO Auto-generated catch block e.printStackTrace(); } } } } class BarrierVisitorimplements Runnable{ final CyclicBarrier arrived; final String name; final SimpleDateFormatsdf; public BarrierVisitor(final String name, SimpleDateFormat sdf, final CyclicBarrier arrived){ this.name = name; this.arrived = arrived; this.sdf = sdf; } @Override publicvoid run(){ try{ //各自从家里出发,模拟从家出发到公园门口 Date startDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(startDate) +" " + this.name + " begin going to the park!"); Thread.sleep((long) (Math.random() * 10000)); }catch(Throwable e){ e.printStackTrace(); }finally{ //到达公园 Date arrivedDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(arrivedDate) +" " + this.name + " arrived the park!"); try{ arrived.await(); }catch(InterruptedException e){ e.printStackTrace(); }catch(BrokenBarrierException e){ e.printStackTrace(); } } } } |
输出如下
2013-10-09 10:38:03 甲 begin going to the park! 2013-10-09 10:38:04 乙 begin going to the park! 2013-10-09 10:38:05 丙 begin going to the park! 2013-10-09 10:38:06 丁 begin going to the park! 2013-10-09 10:38:09 乙 arrived the park! 2013-10-09 10:38:09 丙 arrived the park! 2013-10-09 10:38:12 甲 arrived the park! 2013-10-09 10:38:14 丁 arrived the park! 2013-10-09 10:38:14 all Arrived! |
如果综合上面两个约定:同时出发以及四人都到达后一起进入公园。就需要一个闭锁和一个栅栏。代码如下:
publicclass LatchBarrierVisitors{ publicstaticvoid main(String[] args){ final String[] visitors =new String[] {"甲","乙","丙","丁" }; finalint visitorNum = visitors.length; final SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置栅栏,四个游客全部到达后一起进入公园 final CyclicBarrier arrived =new CyclicBarrier(visitorNum,new Runnable() { @Override publicvoid run() { Date arrivedDate =new Date(System.currentTimeMillis()); System.out.println(sdf.format(arrivedDate) +" all Arrived!"); } }); final CountDownLatch startLatch =new CountDownLatch(1); //让各个游客做好出发前的工作,等闹钟想起就同时出发 for(int i = 0; i < visitorNum; i++){ new Thread(new LatchBarrierVisitor(visitors[i], sdf, startLatch,arrived)).start(); } //使各个游客家的闹钟同时想起,同时出家里出发 Date allStartDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(allStartDate) +" all to start !"); startLatch.countDown(); } } class LatchBarrierVisitorimplements Runnable{ final CountDownLatch start; final CyclicBarrier arrived; final String name; final SimpleDateFormatsdf; public LatchBarrierVisitor(final String name, SimpleDateFormat sdf, final CountDownLatch start,final CyclicBarrier arrived){ this.name = name; this.arrived = arrived; this.start = start; this.sdf = sdf;
} @Override publicvoid run(){ try{ //等待早上闹钟想起,出发去公园 start.await(); //闹钟想起,从家里出发,模拟从家里到公园门 Date startDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(startDate) +" " + this.name + " begin going to the park!"); Thread.sleep((long) (Math.random() * 10000)); }catch(Throwable e){ e.printStackTrace(); }finally{ //到达公园 Date arrivedDate = new Date(System.currentTimeMillis()); System.out.println(sdf.format(arrivedDate) +" " + this.name + " arrived the park!"); try{ arrived.await(); }catch(InterruptedException e){ e.printStackTrace(); }catch(BrokenBarrierException e){ e.printStackTrace(); } } } } |
输入如下:
2013-10-09 10:40:54 all to start ! 2013-10-09 10:40:54 甲 begin going to the park! 2013-10-09 10:40:54 丙 begin going to the park! 2013-10-09 10:40:54 乙 begin going to the park! 2013-10-09 10:40:54 丁 begin going to the park! 2013-10-09 10:40:55 丙 arrived the park! 2013-10-09 10:40:57 乙 arrived the park! 2013-10-09 10:40:58 甲 arrived the park! 2013-10-09 10:41:01 丁 arrived the park! 2013-10-09 10:41:01 all Arrived! |
3.3. 信号量(Semaphore)
Semaphore 是一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。说白了,Semaphore是一个计数器,在计数器不为0的时候对线程就放行,一旦达到0,那么所有请求资源的新线程都会被阻塞,包括增加请求到许可的线程,也就是说Semaphore不是可重入的。每一次请求一个许可都会导致计数器减少1,同样每次释放一个许可都会导致计数器增加1,一旦达到了0,新的许可请求线程将被挂起。缓存池也是使用此思想来实现的,比如链接池、对象池等。
任然以公园的场景举例,假设甲、乙、丙、丁四人都到公园门口后,发现公园游客已满,公园进行了限制,必须是出一个有人才能进一个有人,那么就可以用信号量来实现这样的控制,代码如下:
publicclass SemaphoreVisitors<T> { privatefinal Set<T>set; privatefinal Semaphoresem; public SemaphoreVisitors(int count){ this.set=Collections.synchronizedSet(new HashSet<T>()); this.sem=new Semaphore(count); } publicboolean letIn(T o){ boolean success=false; try{ sem.acquire(); success=set.add(o); }catch(InterruptedException e){ e.printStackTrace(); }finally{ sem.release(); } return success; } publicboolean letOut(T o){ boolean success=false; try{ sem.acquire(); success=set.remove(o); }catch(InterruptedException e){ e.printStackTrace(); }finally{ sem.release(); } return success; } } |
3.4. 同步工具类总结
闭锁用于一组线程等待(阻塞)一个外部事件的发生,这个事件发生之前这些线程阻塞,等待控制线程打开闭锁,然后这些线程同时开始执行。闭锁强调的是阻塞后的同时开始;栅栏则是一组线程相互等待,直到所有线程都到达某一点时才打开栅栏,然后线程可以继续执行,也就是说控制线程先设置一个时间点,然后这些线程各自执行,执行完等待(阻塞),直到这组线程中的所有线程执行完,然后控制线程栅栏打开,这些线程同时继续执行。栅栏强调的是各自执行完后的相互等待以及继续执行。信号量根据一个计数器控制一个结果的数量,条件满足情况下才能进行增加和移除操作,否则进行操作的线程阻塞。
工具 | 作用 | 主要方法 |
闭锁(CountDownLatch) | 类似于门。门初始是关闭的,试图进门的线程挂起等待开门。当负责开门进程将门打开后,所有等待线程被唤醒。 门一旦打开就不能再关闭了。 | CountDownLatch(int n):指定闭锁计数器 await() :挂起等待闭锁计数器为0 countDown():闭锁计数器减1 |
栅栏(CyclicBarrier) | 和闭锁有类似之处。闭锁是等待“开门”事件;栅栏是等待其他线程。例如有N个线程视图通过栅栏,此时先到的要等待,直到所有线程到到达后,栅栏开启,所有等待线程被唤醒通过栅栏。 | CyclicBarrier(int n):需要等待的线程数量 await():挂起等待达到线程数量 |
信号量(Semaphore) | 和锁的作用类似。区别是锁只允许被一个线程获取,但是信号量可以设置资源数量。当没有可用资源时,才被挂起等待。 | Semaphore(int n):指定初始的资源数量 acquire():试图获取资源。当没有可用资源时挂起 release():释放一个资源 |
场景对比:
l 闭锁场景:几个人相约去公园游玩,在家做好准备,约定在某一时刻同时出发去公园,准备工作进行的快的不能提前出门,到点出门。
l 栅栏场景:几个人相约去公园游玩,几个人续到公园门口,要等全部到达公园门口后才一起进入公园。
l 信号量场景:几个人相约去公园游玩,等大家都到公园后,发现来的太迟了,公园游客饱和,公园限制入场游客的数量。游客在门口等待,出来一人,再进入一人,只能一个一个进入。
4. 构建缓存
构建高效、可伸缩的结果缓存几乎应用与所有的服务端应用程序。主要思路就是使用并发容器来替代同步容器作为缓存空间,使用过程中还可能使用同步工具类来协调线程之间的关系,实现过程中要深入分析操作的原子性、长时间处理对性能的影响、缓存的安全性等等这几方面。