JUC常用类 - ConcurrentHashMap并发HashMap

在jdk5之后Java 5.0 在 java.utilconcurrent 包中提供了多种并发容器类来改进同步容器的性能 , 下面我们来介绍这些类

目录

ConcurrentHashMap

CopyOnWriteArrayList 

CopyOnWriteArraySet

CountDownLatch

CyclicBarrier


ConcurrentHashMap

         我们说, hashMap是线程不安全的, 如果同时有多个线程对它进行操作就有可能会出现问题 , 所以在此之上出现了ConcurrentHashMap , 它是线程安全的 , 它保证的线程安全介于hashMap和Hashtable之间 . 其内部采用的是锁分段机制 ( jdk8前用了分段锁的方式, 使用CAS(比较并交换) + synchronized替代) , 大大提高了性能

上面提到了锁分段和分段锁 , 来解释一下这两个名词

分段锁 : 将锁进行细化 , 在细化的每个分段单独加锁, 提高并发效率

锁分段 : 将锁进行分解 , 只在某些必须加锁的分段上加锁

 这里我们先来说一下 : Hashtable

我们知道, Hashtable是线程安全的, 那么为什么还要用ConcurrentHashMap呢?

       Hashtable虽然是线程安全的, 但是它在绝大部分方法上都加了Synchronized锁, 它的线程安全完全由synchronized保证, synchronized是一种独占锁, 就是说, 我这个线程在操作的时候, 其他线程就只能在外面等着, 等我把锁释放给你才行, 所以Hashtable的这种做法就导致了它的效率不高, 所以并不适合于高并发的情况下

接着上一个问题 , 为什么放弃分段锁的做法 ? 锁分段是怎样来做的? 有怎样的好处呢?

放弃分段锁的原因

       首先就是在每个分段上加锁浪费内存空间 , 再一个就是在map 的底层结构中, 竞争同一把锁的概率也是不大的 , 分段锁可能反而导致操作时间变长, 效率变低

锁分段的做法与好处

        java 8 放弃了分段锁, 而使用了Node锁 , 我们知道hashMap存储结构是 : 每个位置存储一个节点node,  在往map中 put 值的时候, 如果要插入的位置上还没有结点, 此时采用CAS机制插入第一个结点, 使用CAS保证了node的原子性, 无需加锁 , 但是如果要插入的位置上已经有结点node, 那么此时就使用这个node作为锁(使用的是synchronized)

        如上图 , 添加过结点的位置就采用第一个node作为锁, 没有添加过结点的位置就暂时不加锁,这样的做法极大提高了效率

        我们可以去看concurrentHashMap底层的 put() 方法实现, 部分如下 : 

  在之后添加结点就会有加锁操作 , 如下 : 

可以看到 , 加锁的力度明显降低 , 同时保证了安全和效率

CopyOnWriteArrayList 

       同样的 , ArrayList也是线程不安全的 , Vector虽然线程安全, 但由于是在方法上直接加锁, 锁的力度太大, 导致效率也不高, 所以就出现了CopyOnWriteArrayList

       我们了解 , 在正常的程序中, 读操作远远大于写操作, CopyOnWriteArrayList 就采用了读写分离的做法, 因为读操作并不会影响数据的值, 所以读操作是没有必要去加锁的 , 多个线程同时去访问读取 list 也是没有事情的 , 写操作单独加锁就行 , 相对于Vector而言 , Vector甚至在读操作上都加了synchronized锁 , 那么可想而知它的效率肯定就跟不上了

另外 , CopyOnWriteArrayList 的一个比较厉害的点就是 : 写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。我们下面来说这点是如何做到的

我们来看CopyOnWriteArrayList底层的 add() 与 get()方法是怎样去做的

add() 方法如下 :

我们可以注意到 , add() 方法中复制了一个新数组 , 为什么有这一步操作呢 ?

       原来, CopyOnWriteArrayList中涉及到对数据的操作 , 如add(), set()等,  每次都会去创建原数组的副本, 然后才会去进行写操作 , 在副本中进行写操作之后, 再将副本中的数据替换原数组的数据, 这样做的目的是为了保证读与写操作之间互不影响(也就是说, 即使在写的过程中, 其他线程也是可以去进行读操作的)

get()方法如下 : 

可以发现, get() 方法并没有加任何的锁, 这就使得CopyOnWriteArrayList完美的实现了读写分离 

CopyOnWriteArraySet

        在集合的容器类体系中, 我们知道, List接口底下的实现类存储的数据是可重复的, 而Set接口底下的实现类都是不可重复的, 当然CopyOnWriteArraySet也一样 , 不过它和 HashSet 与 TreeSet不同 , HashSet不重复和无序性是因为 它使用了HashMap的key(键)作为了自己的底层数据结构, 我们知道hashMap的键是不可重复的 , 而 TreeSet也是使用了TreeMap作为自己的底层,  但CopeOnWriteArraySet与他两却不同 , 我们来看它的源码实现 ,如下:

      可以发现, 它的底层竟然使用的是CopyOnWriteArrayList , 所以我们可以断定它是有序的, 而不像HashSet等是无序的, 当然它也具有CopyOnWriteArrayList的大部分特性

 通过它底层的add() 方法可以看出, 在添加数据时做了判断, 不加入重复的数据

CountDownLatch

      CountDownLatch是一个辅助类,  这个类可以使得一个线程等待其他线程各自执行完毕后再执行, 它通过一个计数器实现, 计数器的初始值是等待线程执行的数量, 当一个线程执行完毕后, 这个计数器就 - 1, 当计数器为 0 时, 等待中的线程才可以执行

public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(6);//计数
        for (int i = 0; i < 6 ; i++) {
            new Thread(
                ()->{
                    System.out.println(Thread.currentThread().getName());
                    downLatch.countDown();//计数器减一操作
                }
            ).start();
        }
        downLatch.await();//关闭计数
        System.out.println("main线程执行");
    }

       上面代码的意思是 : 计数器为6,  我们创建6个线程, 每个线程执行完后计数器 -1 , 当计数器为0 时, 执行await() 方法关闭计数, 此时main()线程才能继续执行

        相当于一个减法计数器

CyclicBarrier

         CyclicBarrier也是一个辅助类, 相对于CountDownLatch来说, 它可以当做一个加法计数器, 它是对一组线程到达一个屏障时被阻塞, 直到最后一个线程到达时, 才会放行它们, 每当一个线程到达,计数器就加 1 ,达到期待值时放行

它有两个构造方法: 

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)

对于第二个构造方法, 第一个参数是计数的值, 第二个参数是最后一个线程到达时要做的事情

public static void main(String[] args) {
        CyclicBarrier c = new CyclicBarrier(5, ()->{
            System.out.println("大家都到齐了 ");
        });

        for (int i = 0; i < 5; i++) {
            new Thread(
                    ()->{
                        System.out.println(Thread.currentThread().getName());
                        try {
                            c.await();//加一计数器
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                    }
            ).start();
        }
    }

         以上代码表示当阻塞线程到达5 时放行它们

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值