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用法
- 尽量测试
阅读每个类型的源码,并不是必要的,只是在有空或者必要的时候才做。