Java并发系列(四)并发构建模块

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的迭代。这也说明了如果对象的状态(HidenIteratorset)和保护他的代码相隔比较远的话我们容易忘记使用正确的同步。一般调用容器的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也增加了对常用复合操作的支持,比如“若没有则添加”、替换、有条件的增加等。

CopyOnWriteArrayListCopyOnWriteArraySet分别代替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

适用于迭代操作远远多于修改操作的场景

内部维护一个数组,每一次修改操作都重新生成新的数组

构建迭代器时,关联当前的内部数组。所以之后的变更不会对迭代器产生影响。

 

CopyOnWriteArraySet

 

本质上就是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.        构建缓存

构建高效、可伸缩的结果缓存几乎应用与所有的服务端应用程序。主要思路就是使用并发容器来替代同步容器作为缓存空间,使用过程中还可能使用同步工具类来协调线程之间的关系,实现过程中要深入分析操作的原子性、长时间处理对性能的影响、缓存的安全性等等这几方面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值