做者:汤圆java
我的博客:javalover.cc编程
断断续续一个多月,也写了十几篇原创文章,感受真的很不同;安全
不能说技术有很大的进步,可是想法确实跟之前有所不一样;多线程
还没开始的时候,想着要学的东西太多,总以为无从下手;并发
可是当你真正下定决心去作了几天后,就会发现 原来路真的是一步步走出来的;dom
若是老是原地踏步东张西望,对本身不会有帮助;高并发
好了,下面开始今天的话题,并发容器篇工具
前面咱们介绍了同步容器,它的很大一个缺点就是在高并发下的环境下,性能差;性能
针对这个,因而就有了专门为高并发设计的并发容器类;测试
由于并发容器类都位于java.util.concurrent
下,因此咱们也习惯把并发容器简称为JUC容器;
相对应的还有JUC原子类、JUC锁、JUC工具类等等(这些后面再介绍)
今天就让咱们简单来了解下JUC中并发容器的相关知识点
文章若是有问题,欢迎你们批评指正,在此谢过啦
并发容器是针对高并发专门设计的一些类,用来替代性能较低的同步容器
常见的并发容器类以下所示:
这节咱们主要以第一个ConcurrentHashMap
为例子来介绍并发容器
其余的之后有空会单独开篇分析
其实跟同步容器的出现的道理是同样的:
同步容器是为了让咱们在编写多线程代码时,不用本身手动去同步加锁,为咱们解放了双手,去作更多有意义的事情(有意义?双手?);
而并发容器则又是为了提升同步容器的性能,至关于同步容器的升级版;
这也是为何Java一直在被人唱衰,却又一直没有衰退的缘由(大佬们也很焦虑啊!!!);
不过话说回来,大佬们焦虑地有点过头了;不敢想Java如今都升到16级了,而咱们始终还在8级徘徊。
这里的普通容器,指的是没有同步和并发的容器类,好比HashMap
三个对比着来介绍,这样会更加清晰一点
下面咱们分别以HashMap
, HashTable
, ConcurrentHashMap
为例来介绍
下面咱们来分析下他们三个之间的性能区别:
注:这里普通容器用的是单线程来测试的,由于多线程不安全,因此咱们就不考虑了
有的朋友可能会说,你这不公平啊,但是没办法呀,谁让她多线程不安全呢。
若是非要让我在安全和性能之间选一个的话,那我选 ConcurrentHashMap(我都要)
他们三个之间的关系,以下图
(红色表示堵的厉害,橙色表示堵的通常,绿色表示畅通)
能够看到:
下面咱们用代码来复现下上面图中所示的效果(慢-中-快)
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); }
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); }
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); }
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中,多个线程可同时读,多个线程可同时写,多个线程同时读和写
并发容器、同步容器、普通容器的区别:
参考内容:
我这里介绍的都是比较浅的东西,其实并发容器的知识深刻起来有不少;
可是由于这节是并发系列的比较靠前的,还有不少东西没涉及到,因此就分析地比较浅;
等到并发系列的内容都涉及地差很少了,再回过头来深刻分析。
写在最后:
愿你的意中人亦是中意你之人。