java的ConCurrentHashMap

一般的应用的编程,用到ConCurrentHashMap的机会很少,就象大家调侃的一样:只有面试的时候才用得着。

但还是有。

网上关于这个的资料,多如牛毛,大部分是原理分析和简单例子。

原理的核心就一个:并发Map其实是多个HashTable拼凑的,可以在写的时候具有更小的锁粒度,它适用于读多写少的场景。其它细枝末节有空再关注了。知道这个就足够了。

关于的原理等,可以看看 ConcurrentHashMap原理分析(一)-综述 - 猿起缘灭 - 博客园 (cnblogs.com)

 

不过许多文章并没有讨论的使用的注意事项:如何在并发的情况下,正确修改某个key。

我们举一个最简单的例子,一个map有两个key,分别是a和b,有10来个线程分别修改a,b。线程的作用就是把a,b的值取出+1。

最后要求,运行一段时间后,a,b的值应该是它们分别被操作的次数。

 

在开始前,重申下:并发map只有部分操作是上锁的,并非说所有的操作都会上锁;特别map有上锁操作,并不意味着其它关联代码都上锁。

如果要了解哪些是上锁的,请查看源码,现在eclipse查看源码简直不要太方便。

 

来个例子:

运行环境:windows11,jdk17

/**
 * 
 */
package study.base.types.map;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author luzhifei
 *
 */
public class ConcurrentMapRunable implements Runnable {

    private AtomicInteger loss;

    private ConcurrentHashMap<String, Integer> flag;

    public ConcurrentMapRunable(ConcurrentHashMap<String, Integer> flag, AtomicInteger loss) {
        this.flag = flag;
        this.loss = loss;
    }

    @Override
    public void run() {
        String tname = Thread.currentThread().getName();
        synchronized (flag) {
            Integer oldVal = flag.get("score").intValue();
            Integer newVal = oldVal + 1;
            System.out.println(tname + " :" + newVal.toString());
            flag.replace("score", oldVal, newVal);
            loss.incrementAndGet();
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int tty = 33;

        ConcurrentHashMap<String, Integer> score = new ConcurrentHashMap<String, Integer>(1, 1);
        score.put("score", 0);

        AtomicInteger loss = new AtomicInteger(0);
        ConcurrentMapRunable job = new ConcurrentMapRunable(score, loss);

        // 初始化线程,并运行
        List<Thread> jobs = new ArrayList<>();
        for (int i = 0; i < tty; i++) {
            Thread t = new Thread(job, i + "");
            jobs.add(t);
        }

        for (int i = 0; i < tty; i++) {
            jobs.get(i).start();
        }

        // 等待返回
        while (loss.intValue() < tty) {

        }

        System.out.println("total score is:" + score.get("score"));
    }

}

上文中红色部分是自增的逻辑:取出并加一,然后放回去。结果是对的,因为synchronized了。如果没有加synchronized,那么结果就不是预期的。

但这样写,好像没有必要使用并发map,那么要怎么写了?

   原代码:
   synchronized (flag) {
            Integer oldVal = flag.get("score").intValue();
            Integer newVal = oldVal + 1;
            System.out.println(tname + " :" + newVal.toString());
            flag.replace("score", oldVal, newVal);
            loss.incrementAndGet();
        }
    修改后代码:

    flag.replace("score", flag.get("score") + 1);

注:以上代码,只适用于当前这种简单场景,所以可以不需要synchronized,因为replace代码自带。

为什么一行就可以了?我们看下并发map的replace代码:

public V replace(K key, V value) {
        if (key == null || value == null)
            throw new NullPointerException();
        return replaceNode(key, value, null);
    }

final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        if (value != null)
                                            e.val = value;
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

 

不关心的代码一堆,有用的就是"synchronized (f) {",也就是说并发map的有些操作自带了同步锁。

 

所以,学习并发的时候,如果仅仅只是为了满足低下并发要求,那么不需要了解那么多,关键了解几点即可:

  • 计算机分时原理,计算机多核并行原理
  • 并发概念
  • 并发的软实现
  • java如何创建线程
  • 如何选择适当的锁粒度
  • java有哪些线程安全的数据类型
  • java 的synchronize用法
  • 尽量测试

 阅读每个类型的源码,并不是必要的,只是在有空或者必要的时候才做。

 

posted @ 2022-05-31 22:56  正在战斗中  阅读(33)  评论(0编辑  收藏  举报