CopyOnWriteArrayList:写时复制
读操作完全无锁——直接读 volatile 数组;写操作加锁后复制整个数组、在副本上改、再整块换引用。读写不冲突,读读更不冲突。代价也明摆着:写一次复制一次,写多就是灾难;内存瞬间翻倍;迭代器是创建那一刻的快照,弱一致——迭代中别人写的,你看不见。所以它的适用面极窄但极准:读多写极少——监听器列表、白名单、配置快照。典型用法:事件监听器注册一次跑一年,每次事件触发遍历全表,COW 让遍历零开销。
BlockingQueue 家族:锁粒度的又一次演进
阻塞队列是生产者消费者的标准答案——满了 put 自动等、空了 take 自动等,手写 wait/notify 的活它全包了。几个成员的锁设计各有心思:ArrayBlockingQueue——有界数组,一把锁管读写,简单但读写互斥;LinkedBlockingQueue——链表结构,两把锁分离读写(putLock/takeLock),读写可以并行,吞吐更高(线程池的无界队列用的就是它);SynchronousQueue——容量为零,不存,put 必须等到 take 来接手才算完,直接交接(cachedThreadPool 用的它);PriorityBlockingQueue——按优先级出队;DelayQueue——到时间才能取,延迟任务的底座(定时线程池用的类似结构)。
三组 API,别混着用
每个队列都提供三组行为,语义差之毫厘谬以千里:阻塞组——put/take,满了或空了就等,最常用;让路组——offer/poll,不行就返回 false/null,可带超时,配合「试一下不行就撤」的业务逻辑;炸雷组——add/remove,满了或空了直接抛异常。新手常把 add 当 put 用,队列一满就爆炸。写生产消费代码,默认用阻塞组,边界场景用让路组,炸雷组基本别碰。
现代版生产者消费者
对比第 4 篇手写的 wait/notify 缓冲区,BlockingQueue 版本三行完事:
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者线程
queue.put(produce());
// 消费者线程
Integer item = queue.take();没有 wait、没有 while、没有 notifyAll——第 4 篇讲的那些铁律,全被封装进了队列内部。自己手写 wait/notify 的意义在于理解原理,生产代码请直接用队列。顺带一提:不需要阻塞语义的并发队列还有 ConcurrentLinkedQueue(无锁 CAS 实现),性能更高但 API 不阻塞,按需选用。
容器篇完。下一篇协作三件套:CountDownLatch、CyclicBarrier、Semaphore——不是锁,是让线程「等齐」与「限流」的工具。
评论 (0)