在当今高并发、高吞吐的分布式系统中,Java并发编程已成为开发者必备的核心能力。当线程如潮水般涌来,如何确保数据安全?如何避免死锁陷阱?如何实现无阻塞的高效运算?答案就隐藏在并发集合与原子类这两大基石之中。
我在最开始学习这个容器的时候当时会记住它的特点是:线程安全,允许多个线程进行读和写。null值和键:ConcurrentHashMap不允许null值作为键或值。但是如何实现线程安全的呢?我们来看一下源码的实现:
首先来看一下它的数据结构设计:
基础存储单元:
static class Node
implements Map.Entry { final int hash; final K key; volatile V val; // 保证可见性 volatile Node next; // 保证可见性 // ... }
核心数据结构:
transient volatile Node
[] table; // 主哈希表 private transient volatile Node [] nextTable; // 扩容时的新表 private transient volatile int sizeCtl; // 控制状态的核心变量
我们在探讨ConcurrentHashMap的数据结构的时候就不得不提它的扩容机制。它的扩容主要是在transfer方法
里,我截取一部分重要的来看一下。
private final void transfer(Node
[] tab, Node [] nextTab) { int n = tab.length, stride; // 计算每个线程处理的桶区间 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // 初始化新表(2倍扩容) if (nextTab == null) { try { Node [] nt = (Node [])new Node,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { sizeCtl = Integer.MAX_VALUE; return; } nextTable = nextTab; transferIndex = n; // 从后向前迁移 } // 多线程协同迁移 while (advance) { // 分配迁移任务区间 } // 迁移数据 synchronized (f) { if (tabAt(tab, i) == f) { Node ln, hn; // 链表迁移(高低位拆分) if (fh >= 0) { int runBit = fh & n; Node lastRun = f; for (Node p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } // ... 构建高低位链表 } // 树节点迁移 else if (f instanceof TreeBin) { // ... 树拆分逻辑 } } } // 设置ForwardingNode setTabAt(tab, i, fwd); }
整个扩容机制的触发条件是,当元素数量超过sizeCtl阈值,就会扩容,这个扩容是重新new一个,然后进行复制。整个数据结构在数据量大的时候会变成红黑树的数据结构。
另外ConcurrentHashMap
保证内存可见性也是通过CAS来保证:
// 原子获取表元素 static final
Node tabAt(Node [] tab, int i) { return (Node )U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE); } // CAS更新表元素 static final boolean casTabAt(Node [] tab, int i, Node c, Node v) { return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v); }
为什么ConcurrentHashMap
能够保证线程安全?那更细粒度的锁一定是关键所在,ConcurrentHashMap
使用的是桶锁,它利用更加细粒度的锁能够保证高效的读写操作。最后我们来看一下经常会拿来比较的HashMap
。
其实CopyOnWriteArrayList和CopyOnWriteArraySet可以放在一起说一下,他们有共同的特点:
ReentrantLock
保证串行因为核心思想就是写时复制,所以我们来看一下添加元素核心操作:
public boolean add(E e) { synchronized (lock) { // 加锁保证写操作原子性 Object[] es = getArray(); // 获取当前数组快照 int len = es.length; // 创建新数组(长度+1) Object[] newElements = Arrays.copyOf(es, len + 1); // 添加新元素 newElements[len] = e; // 原子切换数组引用 setArray(newElements); return true; } }
从这里可以看出,添加元素时首先就是创建新数组,然后整个复制过去,所以理所应当我们可以得知其实此容器是不适合频繁写操作的,只适合频繁读的场景来用。
BlockingQueue里有两个核心实现:ArrayBlockingQueue
和LinkedBlockingQueue
两者区别不大,就是数组和链表的区别。我们这里主要来看一下ArrayBlockingQueue
。它的数据结构没什么好看的,这里我们主要来看一下它的put()方法(阻塞插入
):
public void put(E e) throws InterruptedException { Objects.requireNonNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); // 可中断获取锁 try { // 当队列满时等待 while (count == items.length) notFull.await(); enqueue(e); // 入队 } finally { lock.unlock(); } } // 入队核心方法 private void enqueue(E e) { final Object[] items = this.items; items[putIndex] = e; // 循环数组处理 if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); // 唤醒等待的消费者 }
关键设计是:
我们再来看一个比较常用的实现类:PriorityBlockingQueue (优先级队列)
public class PriorityBlockingQueue
extends AbstractQueue implements BlockingQueue , java.io.Serializable { // 基于堆的优先级队列 private transient Object[] queue; // 单锁设计 private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); // 无界队列,put 永不阻塞 public void put(E e) { offer(e); // 永不阻塞 } }
原子类就是位于java.util.concurrent.atomic
包下,整个类是基于CAS操作实现的无锁线程安全类。我们知道CAS的全称是compare and swap
,来看一下具体的代码:
// 原子类的基石:Unsafe 类提供硬件级原子操作 public final class Unsafe { // CAS 方法(核心) public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x); public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); // 获取字段偏移量 public native long objectFieldOffset(Field f); }
那我们来看一个实现类
public class AtomicInteger extends Number implements java.io.Serializable { private static final Unsafe U = Unsafe.getUnsafe(); private static final long VALUE; static { try { // 获取 value 字段的偏移量 VALUE = U.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); } catch (ReflectiveOperationException e) { throw new Error(e); } } private volatile int value; // volatile 保证可见性 // 核心 CAS 操作 public final boolean compareAndSet(int expectedValue, int newValue) { return U.compareAndSetInt(this, VALUE, expectedValue, newValue); } // 原子递增并返回新值 public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; } // JDK 8 优化后的原子加法 public final int getAndAdd(int delta) { return U.getAndAddInt(this, VALUE, delta); } }
可以看到原子类的原理就是基于CAS构建的,但其实原子类有一个重要的ABA问题
,所谓的ABA就是变量在中间变化了但是在最后又变为预期值。那一般解决ABA问题就是采用时间戳、版本号等方法。来看一下版本号等解决方式:
public class AtomicStampedReference
{ private static class Pair { final T reference; final int stamp; // 版本戳 private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } } private volatile Pair pair; // 带版本戳的 CAS public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } }
这一块其实我也没有实战经验,只能从具体的集合和原子类的特点来考虑!
这里我只说三个比较常见的Map类型集合:
ConcurrentHashMap
:它的场景就是高频读写,优势在于是分段锁/桶设计,这样能够更细粒度的控制。缺点就是因为是非公平锁,然后扩容的时候会消耗大量资源则就会导致短暂堵塞。ConcurrentSkipListMap
:使用场景就是需要排序的键值对,优势在于基于跳表实现,能够支持有序遍历。缺点就是内存占用高,写入性能不及ConcurrentHashMap。CopyOnWriteMap
:无锁读就是最大优势。但缺点很明显,极其不适合写操作,开销很大。CopyOnWriteArrayList/Set
:这个也和上面一样,写操作复制整个数组,则开销很大LinkedBlockingDeque
:这个场景就是需要队列性质的列表,优势在于具有阻塞特性,缺点也是内存占比大Collections.synchronizedList()
:这个用起来简单,但问题是因为基于synchronized来实现,所以全局锁的性能差因为Queue类型一般多用于生产者/消费者,以及优先级的情况,那我们就说一下这几个。
ArrayBlockingQueue
:规定大小,有界,一般就用于有界的生产者-消费者模式PriorityBlockingQueue
:优势就是按优先级排序,但需要注意因为是无界队列,所以需要容量控制至于原子类的选用,我们也说几个常用的:
AtomicInteger/AtomicLong
:一般使用场景就是一些简单的计数器,优势在于它是轻量级CAS实现的AtomicReference
:这个就是对象引用更新的时候来用,但是要注意ABA问题AtomicStampedReference
:这个就是通过版本戳来解决ABA问题AtomicBoolean
:比volatile+sychronized更轻量,很适合用来管理状态到这里我们就可以总结一下:
实际上高并发系统中没有什么“银弹”
选择,具体场景具体分析,而且要实际压测来进行选择。根据具体场景选择核心数据结构,通过性能测试验证选择,监控生产环境表现,持续优化调整。