乐趣区

关于java:Java并发容器篇

作者:汤圆

集体博客:javalover.cc

前言

断断续续一个多月,也写了十几篇原创文章,感觉真的很不一样;

不能说技术有很大的提高,然而想法的确跟以前有所不同;

还没开始的时候,想着要学的货色太多,总感觉无从下手;

然而当你真正下定决心去做了几天后,就会发现 原来路真的是一步步走进去的;

如果总是原地踏步东张西望,对本人不会有帮忙;

好了,上面开始明天的话题,并发容器篇

简介

后面咱们介绍了同步容器,它的很大一个毛病就是在高并发下的环境下,性能差;

针对这个,于是就有了专门为高并发设计的并发容器类;

因为并发容器类都位于 java.util.concurrent 下,所以咱们也习惯把并发容器简称为 JUC 容器;

绝对应的还有 JUC 原子类、JUC 锁、JUC 工具类等等(这些前面再介绍)

明天就让咱们简略来理解下 JUC 中并发容器的相干知识点

文章如果有问题,欢送大家批评指正,在此谢过啦

目录

  1. 什么是并发容器
  2. 为什么会有并发容器
  3. 并发容器、同步容器、一般容器的区别

注释

1. 什么是并发容器

并发容器是针对高并发专门设计的一些类,用来代替性能较低的同步容器

常见的并发容器类如下所示:

这节咱们次要以第一个 ConcurrentHashMap 为例子来介绍并发容器

其余的当前有空会独自开篇剖析

2. 为什么会有并发容器

其实跟同步容器的呈现的情理是一样的:

同步容器是为了让咱们在编写多线程代码时,不必本人手动去同步加锁,为咱们解放了双手,去做更多有意义的事件(有意义?双手?);

而并发容器则又是为了进步同步容器的性能,相当于同步容器的升级版;

这也是为什么 Java 始终在被人唱衰,却又始终没有消退的起因(大佬们也很焦虑啊!!!);

不过话说回来,大佬们焦虑地有点过头了;不敢想 Java 当初都升到 16 级了,而咱们始终还在 8 级彷徨。

3. 并发容器、同步容器、一般容器的区别

这里的一般容器,指的是没有同步和并发的容器类,比方 HashMap

三个比照着来介绍,这样会更加清晰一点

上面咱们别离以 HashMap, HashTable, ConcurrentHashMap 为例来介绍

性能剖析

上面咱们来剖析下他们三个之间的性能区别:

注:这里一般容器用的是单线程来测试的,因为多线程不平安,所以咱们就不思考了

有的敌人可能会说,你这不偏心啊,可是没方法呀,谁让她多线程不平安呢。

如果非要让我在平安和性能之间选一个的话,那我选 ConcurrentHashMap(我都要)

他们三个之间的关系, 如下图

(红色示意堵的厉害,橙色示意堵的个别,绿色示意畅通)

能够看到:

  • 单线程 中操作 一般容器 时,代码都是串行执行的,同一时刻只能 put 或 get 一个数据 到容器中
  • 多线程 中操作 同步容器 时,能够多个线程排队去执行,同一时刻也是只能 put 或 get 一个数据 到同步容器中
  • 多线程 中操作 并发容器 时,能够多个线程同时去执行,也就是说同一时刻能够有多个线程去 put 或 get 多个数据 到并发容器中(可同时读读,可同时读写,可同时写写 - 有可能会阻塞,这里是以 ConcurrentHashMap 为参考)

上面咱们用代码来复现下下面图中所示的成果(慢 - 中 - 快)

  1. HashMap 测试方法
public static void hashMapTest(){Map<String, String> map = new HashMap<>();
  long start = System.nanoTime();
    // 创立 10 万条数据 单线程
  for (int i = 0; i < 100_000; i++) {
        // 用 UUID 作为 key, 保障 key 的惟一
    map.put(UUID.randomUUID().toString(), String.valueOf(i));
    map.get(UUID.randomUUID().toString());
  }
  long end = System.nanoTime();
  System.out.println("hashMap 耗时:");
  System.out.println(end - start);
}
  1. HashTable 测试方法
public static void hashTableTest(){Map<String, String> map = new Hashtable<>();
  long start = System.nanoTime();
    // 创立 10 个线程 - 多线程
  for (int i = 0; i < 10; i++) {new Thread(()->{
      // 每个线程创立 1 万条数据
      for (int j = 0; j < 10000; j++) {
        // UUID 保障 key 的唯一性
        map.put(UUID.randomUUID().toString(), String.valueOf(j));
        map.get(UUID.randomUUID().toString());
      }
    }).start();}
    // 这里是为了期待下面的线程执行完结,之所以判断 >2,是因为在 IDEA 中除了 main thread,还有一个 monitor thread
  while (Thread.activeCount()>2){Thread.yield();
  }
  long end = System.nanoTime();
  System.out.println("hashTable 耗时:");
  System.out.println(end - start);
}
  1. concurrentHashMap 测试方法
public static void concurrentHashMapTest(){Map<String, String> map = new ConcurrentHashMap<>();
  long start = System.nanoTime();
  // 创立 10 个线程 - 多线程
  for (int i = 0; i < 10; i++) {new Thread(()->{
      // 每个线程创立 1 万条数据
      for (int j = 0; j < 10000; j++) {
        // UUID 作为 key,保障唯一性
        map.put(UUID.randomUUID().toString(), String.valueOf(j));
        map.get(UUID.randomUUID().toString());
      }
    }).start();}
    // 这里是为了期待下面的线程执行完结,之所以判断 >2,是因为在 IDEA 中除了 main thread,还有一个 monitor thread
  while (Thread.activeCount()>2){Thread.yield();
  }
  long end = System.nanoTime();
  System.out.println("concurrentHashMap 耗时:");
  System.out.println(end - start);
}
  1. main 办法别离执行下面的三个测试
public static void main(String[] args) {hashMapTest();
  hashTableTest();
  while (Thread.activeCount()>2){Thread.yield();
  }
  concurrentHashMapTest();}

运行能够看到,如下后果(运行屡次,数值可能会变好,然而法则基本一致)

hashMap 耗时:754699874 (慢)
hashTable 耗时:609160132(中)concurrentHashMap 耗时:261617133(快)

论断就是,失常状况下的速度:一般容器 < 同步容器 < 并发容器

然而也不那么相对,因为这里插入的 key 都是惟一的,所以看起来失常一点

那如果咱们不失常一点呢?比方极其到 BT 的那种

上面咱们就不停地插入同一条数据,下面的所有 put/get 都改为上面的代码:

map.put("a", "a");
map.get("a");

运行后,你会发现,又是另外一个论断(大家感兴趣的能够敲进去试试)

不过论断不论断的,意义不是很大;

锁剖析

一般容器没锁

同步容器中锁的都是办法级别,也就是说锁的是整个容器,咱们先来看下 HashTable 的锁

public synchronized V put(K key, V value) {}
public synchronized V remove(Object key) {}

能够看到:因为锁是内置锁, 住的是 整个容器

所以咱们在put 的时候,其余线程都不能 put/get

而咱们在get 的时候,其余线程也都不能 put/get

所以 同步容器 效率 会比拟

并发容器,咱们以 1.7 的 ConcurrentHashMap 为例来说下(之所以选 1.7,是因为它外面波及的内容都是后面章节介绍过的)

它的锁粒度很小,它不会给整个容器上锁,而是 分段上锁

分段的根据就是 key.hash,依据不同的 hash 值映射到不同的段(默认 16 个段),而后插入数据时,依据这个 hash 值去给对应的段上锁,此时其余段还是能够被其余线程读写的;

所以这就是文章结尾所说的,为啥 ConcurrentHashMap 会反对多个线程同时写(因为只有插入的 key 的 hashCode 不会映射到同一个段里,那就不会抵触,此时就能够同时写)

读因为没有上锁,所以当然也反对同时读

如果读操作没有锁,那么它怎么保证数据的一致性呢?

答案就是以前介绍过的 volatile(保障可见性、禁止重排序),它润饰在节点 Node 和值 val 上,保障了你 get 的值永远是最新的

上面是 ConcurrentHashMap 局部源码,能够看到 val 和 net 节点都是 volatile 类型

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash;
  final K key;
  volatile V val;
  volatile Node<K,V> next;
}

总结下来就是:并发容器 ConcurrentHashMap 中,多个线程可同时读,多个线程可同时写,多个线程同时读和写

总结

  1. 什么是并发容器:并发容器是针对高并发专门设计的一些类,用来代替性能较低的同步容器
  2. 为什么会有并发容器:为了进步同步容器的性能
  3. 并发容器、同步容器、一般容器的区别:

    • 性能:高 – 中 – 低
    • 锁:粒度小 – 粒度大 – 无
    • 场景:高并发 – 中并发 – 单线程

参考内容:

  • 《Java 并发编程实战》
  • 《实战 Java 高并发》
  • 《深刻了解 Java 虚拟机》

后记

我这里介绍的都是比拟浅的货色,其实并发容器的常识深刻起来有很多;

然而因为这节是并发系列的比拟靠前的,还有很多货色没波及到,所以就剖析地比拟浅;

等到并发系列的内容都波及地差不多了,再回过头来深入分析。

写在最初:

愿你的意中人亦是中意你之人。

退出移动版