预见猿份
主题
首页面试题模拟面试SQL练习在线工具关于我们老苗一对一私教学员评价
实战项目
零基础学习路线项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通达梦数据库从入门到实战MongoDB从入门到实战PostgreSQL从入门到实战TiDB从入门到实战Java数据结构与算法Java 并发编程(JUC)实战课程老苗一对一私教学员评价
blog我的会员
  • Java 并发编程(JUC)实战课程

    • 课程介绍
    • 01 多线程入门
    • 02 线程的常用方法
    • 03 线程池
    • 04 synchronized教程
    • 05 ReentrantLock教程
    • 06 读写锁
    • 07 JMM教程
    • 08 CAS教程
    • 09 wait&notify
    • 10 常用的并发集合
    • 11 异步任务控制
    • 12 常用并发工具







----- 到底线了 -----

第10章 常用的并发集合 ​

2.1 ConcurrentHashMap ​

2.1.1 ConcurrentHashMap介绍 ​

为什么要学ConcurrentHashMap?

HashMap是线程不安全的。

也有一些线程安全的映射表,如下:

早期java版本中有HashTable。

由Collections包装的集合类,比如:Collections.SynchronizedMap,在Collections.SynchronizedMap类中对Map的方法进行包装,方法内部使用synchronized修饰,经过包装实现了map的线程安全。

ConcurrentHashMap是JUC包中提供的线程安全的映射表,它的性能相比其它映射表更高。

2.1.2 ConcurrentHashMap源码解析 ​

ConcurrentHashMap比HashTable在性能上有很大的提升,HashTable是对全表进行锁定,ConcurrentHashMap则是局部锁定。ConcurrentHashMap在jdk1.7、1.8上设计不一样,1.7在数组+链表的基础上采用分段锁技术保证线程安全,1.8在数组+链表+红黑树的基础上采用CAS+synchronized保证线程安全。

ConcurrentHashMap实现Map接口,针对哈希表的一些常用方法这里不再测试,下边以jdk1.8为准通过阅读源码理解下它的运行原理:

1、put方法

java
public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    //onlyIfAbsent: false每次用新值覆盖旧值,true:已存在相同key不作任何操作
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //计算机哈希值并保证正数
        //(h ^ (h >>> 16)) & HASH_BITS (h ^ (h >>> 16))是hashMap中计算哈希的方法,& HASH_BITS的作用是将负数转正数 HASH_BITS是31位1
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                //使用cas初始化table
                tab = initTable();
            //桶的位置为空则用cas方式创建头结点,i是桶的位置
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//如果发现有线程在扩容则先协助扩容
                tab = helpTransfer(tab, f);
            else {//哈希冲突
                V oldVal = null;
                //使用同步锁对头结点进行加锁
                synchronized (f) {
                    if (tabAt(tab, i) == f) {//如果头结点没有被移动过
                        if (fh >= 0) {//普通结点
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //根据key查找,如果已经存在
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    //更新旧值 
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //如果结点不存在则新增一个结点到链表尾部
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {//如果是红黑树结点
                            Node<K,V> p;
                            binCount = 2;
                            //查找key是否在红黑树中,如果没有则添加
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                //更新旧值
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //如是链表长度大于阀值则将链表转成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//统计结点个数,检查是否需要扩容
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

initTable()

java
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
        	//sizeCtl用于控制初始化和扩容
        	//如果sizeCtl小于0说明有线程执行CAS成功正在初始化,此时当前线程让出CPU
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                    	//默认大小16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

tabAt:

java
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    	//直接获取指定内存数据,保证每次拿到的就是最新数据
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
1
2
3
4

2、get方法

get方法全程没有加锁,性能很高,源代码如下:

java
public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //计算机哈希值并保证正数
        int h = spread(key.hashCode());
        //如果表的长度大于0开始定位桶
        if ((tab = table) != null && (n = tab.length) > 0 &&
            //(n - 1) & h)桶的下标
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {//如果桶位置上的结点的哈希与查找的key的哈希相同则开始比较key是否相等,如果相等则返回key对应 的value
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)//如果eh为负数则表示链表在扩容中或为红黑树,调用find查找key
                return (p = e.find(h, key)) != null ? p.val : null;
            //以上两种情况都不符合则开始遍历每个结点
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

2.1.3 ConcurrentHashMap案例 ​

统计字符串每个字符出现的次数。

1、首先准备等待统计的数据,将0到9的10个数字循环100遍放入集合。

2、首先用HashMap实现,创建一个HashMap对象,遍历集合中等待统计的字符,如果字符在map中存在则计数加1,否则初始为1。

代码如下:

java
package com.yjoffer.javase.thread.collection;


import java.util.ArrayList;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.TimeUnit;  
import java.util.concurrent.atomic.AtomicLong;

/**

- 线程安全集合类测试
- @author 预见猿份(www.yjoffer.com)
- @version 1.0  
*/  
public class ConcurrentHashMapTest {//准备等待统计的数据,统一放入集合  
private static List data_init(){  
String data = "0123456789";  
List<String> characters = new ArrayList<>();  
for (int j = 0; j < 100; j++) {  
  for (int i = 0; i < data.length(); i++) {  
    char c = data.charAt(i);  
    characters.add(String.valueOf(c));  
  }  
}

return characters;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

按预期map中共有10个key,每个key对应的value为100。

执行程序,存在错误。

下边用线程安全的集合类ConcurrentHashMap进行修改。

首先将new HashMap()更改为new ConcurrentHashMap(),再次运行程序,结果 仍然 存在错误。

下边分析错误,导致问题的代码如下:

java
 Integer integer = map.get(c);
 if(integer == null){
 	map.put(c,1);
 }else{
 	map.put(c,integer+1);
 }
1
2
3
4
5
6

虽然get方法是线程安全,put方法是线程安全,但是由于是多线程执行存在线程切换,上边整体代码线程不安全。

修改方案1:使用synchronized将上边的代码保持原子性。

java
synchronized (map){
Integer integer = map.get(c);
 if(integer == null){
 	map.put(c,1);
 }else{
 	map.put(c,integer+1);
 }
}
1
2
3
4
5
6
7
8

运行程序,结果正确。

此方法是将整个表对象作为锁对象,性能较低。

修改方案2:使用CAS方法

下边使用putIfAbsent方法实现key判断的逻辑,如果key不存在则用一个初始值填充,这里用原子类填充,并利用原子类的CAS方法自增1。

代码修改如下:

java
AtomicLong atomicLong = map.putIfAbsent(c, new AtomicLong(1));
if(atomicLong!=null){
	atomicLong.incrementAndGet();
}
1
2
3
4

map.putIfAbsent(c, new AtomicLong(1));代码相当于下边的代码:

java
if (!map.containsKey(key)) return map.put(key, new AtomicLong(1)); else return map.get(key);
1

完整代码如下:

java
public static void test_concurrentMap(){
        List<String> characters = data_init();
        Map<String, AtomicLong> map = new ConcurrentHashMap<>();
        ExecutorService executorService = Executors.newCachedThreadPool();
        characters.stream().forEach(c->{
            executorService.execute(()->{
                AtomicLong atomicLong = map.putIfAbsent(c, new AtomicLong(1));
                if(atomicLong!=null){
                    atomicLong.incrementAndGet();
                }
            });
        });
        shutdown(executorService);
        System.out.println(map.size());
        System.out.println(map);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
🔒关注公众号,免费阅读全文

请免费关注 预见猿份 公众号,发送:yjoffer.com,获取解锁密码

公众号二维码微信扫码关注

关注公众号回复获取密码后,在下方输入并点击解锁,本机后续再读无需重复输入。

← 09 wait&notify11 异步任务控制 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系我们
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节