前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基础构建块

基础构建块

作者头像
SuperHeroes
发布2018-05-31 14:00:50
5870
发布2018-05-31 14:00:50
举报
文章被收录于专栏:云霄雨霁云霄雨霁

同步容器类

同步容器类包括Vector和Hashtable, 还包括JDK1.2以后添加的一些功能相似的类,这些同步的封装器类是由Collections.synchronizedXxxd等工厂方法创建的。这些类实现线程安全的方法是:将它们的状态封装起来,并对每一个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。

同步容器类的问题

   同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。容器上常见的复合操作有:迭代、跳转以及条件运算(例如“若没有则添加”)。在同步容器中,这些复合操作在没有客户端加锁的情况下仍然是线程安全的,但当其他线程并发地修改容器时,他们可能会出现意料之外的行为。这时需要加锁机制。

代码语言:javascript
复制
synchronized(vector){
    for(int i=0; i<vector.size(); i++)
        doSomething(vector.get(i));
    }
}

上面代码在对一个同步容器迭代的时候,为了防止在迭代的过程中其他线程更改Vector,需要对代码进行同步处理。

如果不希望在迭代期间对容器加锁,那么另一种方法是“克隆”容器,并在副本上进行迭代。由于副本被封闭在线程中,因此其他线程不会在迭代期间对其进行更改。不过,在克隆容器的过程中存在显著的性能开销。

并发容器

上面的同步容器将所有对容器状态的访问都串行画,以实现它们的线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。

并发容器是针对多个线程并发访问设计的。Java5.0增加了许多并发容器类来改善同步容器的性能。

ConcurrentHashMap

与HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap使用更细粒度的分段锁机制而不是将每一个方法都在同一个锁上同步。这种机制中,任意数量的读线程可以并发访问Map,执行读取的线程可以和执行写入的线程并发访问Map,并且一定数量的写入线程可以并发地修改Map。ConcurrentHashMap带来的结果是,在并发环境下将带来更高的吞吐量,在单线程环境中只损失非常小的性能。

ConcurrentHashMap与其他并发容器一起增强了同步容器类:他们提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代的过程中加锁。返回的迭代器具有“弱一致性”,并非“即时失败”。

ConcurrentHashMap中没有实现对Map加锁已提供独占访问。在HashMap和synchronizedMap中获取Map的锁能防止其他线程访问这个Map。与HashMap和synchronizedMap相比,用ConcurrentHashMap来代替同步Map能进一步提高可伸缩性,只有在应用程序需要加锁Map以进行独占访问时,才应该放弃ConcurrentHashMap。

正如ConcurrentHashMap用于代替同步Map,Java6引入ConcurrentSkipListMap和ConcurrentSkipList-Set来分别作为SortedMap和SortedSet的并发替代品。

CopyOnWriteArrayList

CopyOnWriteArrayList用于替代同步List,在某些情况下提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制(类似地,CopyOnWriteArraySet用来代替同步Set)。

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

“写入时复制”容器的线程安全性在于:只要正确的发布一个事实不可变对象,那么在访问该对象时就不需要再进一步的同步。显然,每当修改时都会复制底层数组,这需要一定的开销,尤其是数组规模较大时。仅当迭代操作远远多于修改操作时,才应该使用“写入时复制”容器

Queue和BlockingQueue(阻塞队列)

Queue用来临时保存一组待处理的元素。它提供了几种实现,包括:ConcurrentLinkedQueue, 这是一个传统的先进先出队列;还有PriorityQueue,这是一个(非并发的)优先队列。Queue上的操作不会阻塞,如果队列为空,那么将会获取到空值。虽然可以用List模拟Queue的行为----事实上正是通过LinkedList来实现Queue的,但Queue能去掉List的随机访问请求,从而实现更高效的并发。

BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作。如果队列为空,那么获取元素的操作会阻塞知道队列中出现一个可用元素;如果队列已满,那么插入操作会阻塞知道队列出现可用空间。

生产者-消费者模式

阻塞队列支持生产者-消费者模式。该模式将“找出需要完成的工作”和“执行工作”这两个过程分离开来,并把工作放入一个“待完成”的列表中以便在随后处理,而不是找出后立即处理。生产者-消费者模式简化了开发过程,因为它消除了生产者类和消费者类之间的代码依赖性。

双端队列--Deque和BlockingDeque

Deque和BlockingDeque分别扩展了Queue和BlockingQueue。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和ArrayBlockingDeque。

工作密取模式

正如阻塞队列适用于“生产者-消费者”模式,双端队列适用于另一种模式--“工作密取”。在生产者-消费者模式中,所有消费者共享一个工作队列,而在工作密取中,每个消费者都各自有自己的一个双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者的双端队列末尾秘密地获取工作。工作密取模式比一般的生产者-消费者模式具有更高的可伸缩性,这是因为工作线程不会在单个共享的任务队列发生竞争。

同步工具类

闭锁

闭锁是一种工具同步类,可以延迟线程进度直到其达到终止状态。闭锁的作用相当于一道门,在闭锁到达结束状态之前,这扇门一直是关闭的,没有线程可以通过;当闭锁结束时,这扇门会打开所有线程可以通过。当闭锁达到结束状态打开门时,将不会再改变其状态,即门不会再次关闭。

闭锁的应用场景:
  • 确保某个计算在其需要的所有资源都被初始化之后才继续执行;
  • 确保某个服务在其依赖的所有其他服务都已经启动后才启动;
  • 等待直到某个操作的所有参与者都就绪再继续执行。

CountDownLatch是一种灵活的闭锁实现,可以在上述各种情况下使用,它可以使一个或多个线程等待一组事件的发生。闭锁状态包括一个计数器,计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示一个时间已经发生了。await方法等待计数器为0,这表示所有事件已经发生。如果计数器非0,那么await会一直阻塞,知道计数器为0,或者等待中的线程中断,或者等待超时。

代码语言:javascript
复制
public class TestHarness{
    public long timeTasks(int nThreads, final Runnable task) throws InterruptedException{
        final CountDownLanch startGate = new CountDownLanch(1);
        final CountDownLanch endGate = new CountDownLanch(nThreads);
        
        for(int i=0; i<nThreads; i++){
            Thread t = new Thread(){
                public void run(){
                    try{
                        startGate.await();    //n个线程都会阻塞在这里,知道startGate门打开
                        try{
                            task.run();
                        }finally{
                            engGate.countDown();    //每个线程都在执行结束时将endGate门的计数器减一
                        }
                    }catch (InterruptedException ignored){}
                }
            };
            t.start();    //启动线程,但会阻塞在startGate门
        }

        //使用nanoTime()提供准确的计时
        long start = System.nanoTime();

        startGate.countDown();    //将StartGate门打开,n个线程同时执行
        endGate.await();    //阻塞直到n个线程都执行完成,计数器变为0

        long end = System.nanoTime();
        return end-start;
    }
}

上面的TestHarness类中使用闭锁,确保n个线程同时开始执行。这在测试n个线程并发执行某个任务所需要的时间是很有用。如果不适用闭锁,先启动的线程必将领先后启动的线程。

信号量Semaphore

计数信号量用来控制同时访问某个特定资源的操作数量,或者同时指定某个特定操作的数量。信号量用来解决同步问题而不是用来解决死锁问题。

Semaphore中管理着一组虚拟的许可(premit),许可的初始数量可通过构造函数来制定。在执行操作时可以首先获得许可(只要还有剩余的许可),并在使用后释放许可。如果没有许可将被阻塞。

代码语言:javascript
复制
public class BoundedHashSet<T>{
    private final Set<T> set;
    private final Semaphore sem;
    
    public BoundedHashSet(int bound){
        this.set = Collection.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)    //如果add没有添加元素,立即释放许可
                sem.release();    
        }
    }

    public boolean remove(Object o){
        boolean wasRemoved = set.remove(o);
        if(wasRemoved)
            sem.release(); 
        return wasRemoved;
    }
}

栅栏

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

常见的栅栏有两种形式:CyclicBarrier和Exchanger。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 同步容器类
    • 同步容器类的问题
    • 并发容器
      • ConcurrentHashMap
        • CopyOnWriteArrayList
          • Queue和BlockingQueue(阻塞队列)
            • 生产者-消费者模式
          • 双端队列--Deque和BlockingDeque
            • 工作密取模式
        • 同步工具类
          • 闭锁
            • 闭锁的应用场景:
          • 信号量Semaphore
            • 栅栏
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档