ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java进阶篇之AtomicIntegerArray:单格更新与整组读取的边界

Java进阶篇之AtomicIntegerArray:单格更新与整组读取的边界 Java进阶篇之AtomicIntegerArray单格更新与整组读取的边界上一篇用AtomicMarkableReference将引用与布尔标记一起更新。今天把视线移到数组多个线程分别给不同位置计数每个位置都需要可靠地累加怎样避免“读出旧值再写回新值”造成的覆盖AtomicIntegerArray为每个整数元素提供原子操作。它适合固定槽位的并发计数也有一个容易被忽略的边界单个位置更新可靠连续读取两个位置时仍然可能拼出一组混合观察。本文适用于Java21完整示例在JDK21.0.8实际编译运行。示例用CountDownLatch固定关键顺序只展示语义边界不包含吞吐量或性能测试结论。一、先看数组里存了什么AtomicIntegerArray的长度在构造时确定。new AtomicIntegerArray(2)创建两个初始值为0的槽位传入int[]时构造器复制数组中的值随后修改原数组不会同步到它。类没有追加、删除元素的接口。应用可以将索引对应到固定的业务分组例如两个请求类型分别使用索引0与索引1。保存到哪个槽位由应用决定类本身不会替你完成业务分组。普通int[]上的values[i]包含读取、计算和写回。多个线程交错执行时可能基于同一个旧值计算造成更新覆盖。这里需要关注整个累加动作不能只看元素类型是int。构造器与各方法的约定可参考Java21官方API。二、返回旧值还是新值先分清操作更新方式返回值incrementAndGet(i)索引i加1更新后的值getAndIncrement(i)索引i加1更新前的值addAndGet(i, delta)索引i加delta更新后的值getAndAdd(i, delta)索引i加delta更新前的值compareAndSet(i, expected, next)当前值等于expected时改为next是否更新成功CAS检验的是这个索引当前的整数值。一次返回false表示这次条件更新没有成功调用方应读取新状态重新判断业务条件。若条件只包含数值它也无法知道这个数值中间是否变化过。调用get(i)之后再调用set(i, old 1)仍是两次独立操作。需要累加时直接使用类提供的原子累加方法。三、单格更新可靠整组观察需要另作设计假设数组初始为[0, 0]写入线程依次执行pair.set(0,1);pair.set(1,1);整体状态按顺序经过[0, 0]、[1, 0]、[1, 1]。读取者先在写入前拿到索引0的0等两次写入都结束再拿到索引1的1就会把本地结果拼成[0, 1]。混合观察中的每个元素都来自有效读取但这组结果没有在上述整体状态中出现。单个元素的原子性不提供跨索引的事务或一致快照。图中展示的是读取时序的隐喻。两格最终都为1学习猫记录的0与1来自不同时间不能据此认定它拿到了同一时刻的完整数组。如果业务要求两格共同满足约束例如总量守恒或成对迁移应让所有相关读取和写入遵循同一把锁或将整组状态设计为不可变对象通过一个AtomicReference整体替换。只在读取端加锁不能约束未参与这套协议的写入者。四、完整示例计数与混合观察四个线程各累加5000次两个线程共用一个索引。先等待全部线程结束再汇总静止后的计数。最后通过两个CountDownLatch安排“读第一格、完成两次写入、读第二格”的顺序。保存为AtomicArrayDemo.javaimportjava.util.Arrays;importjava.util.concurrent.CountDownLatch;importjava.util.concurrent.atomic.AtomicIntegerArray;publicclassAtomicArrayDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{basicOperations();countByIndex();observeMixedPair();}privatestaticvoidbasicOperations(){int[]original{1,2};AtomicIntegerArrayvaluesnewAtomicIntegerArray(original);original[0]99;System.out.println(copiedvalues.get(0));System.out.println(increment-returnvalues.incrementAndGet(0));intbeforevalues.getAndAdd(0,3);System.out.println(getAndAdd-returnbefore, currentvalues.get(0));booleanchangedvalues.compareAndSet(0,5,9);System.out.println(caschanged, currentvalues.get(0));}privatestaticvoidcountByIndex()throwsInterruptedException{AtomicIntegerArraycountersnewAtomicIntegerArray(2);Thread[]workersnewThread[4];for(inti0;iworkers.length;i){intindexi%counters.length();workers[i]newThread(()-{for(intround0;round5_000;round){counters.incrementAndGet(index);}});workers[i].start();}for(Threadworker:workers){worker.join();}System.out.println(counterscounters);System.out.println(total(counters.get(0)counters.get(1)));}privatestaticvoidobserveMixedPair()throwsInterruptedException{AtomicIntegerArraypairnewAtomicIntegerArray(2);CountDownLatchfirstReadnewCountDownLatch(1);CountDownLatchwritesFinishednewCountDownLatch(1);int[]observednewint[2];ThreadwriternewThread(()-{try{firstRead.await();pair.set(0,1);pair.set(1,1);writesFinished.countDown();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}});writer.start();observed[0]pair.get(0);firstRead.countDown();writesFinished.await();observed[1]pair.get(1);writer.join();System.out.println(mixed-readArrays.toString(observed));System.out.println(final-arraypair);}}执行javac-encodingUTF-8 AtomicArrayDemo.javajavaAtomicArrayDemo本次实际输出copied1 increment-return2 getAndAdd-return2, current5 castrue, current9 counters[10000, 10000] total20000 mixed-read[0, 1] final-array[1, 1]copied1说明原数组改成99后原子数组里的副本仍是1。两个累加方法返回值不同CAS检验当前5成功改成9。两个槽位最终各为10000总量为20000。这里先join()再汇总读取期间已经没有计数线程更新不要把这个结果推广为“并发写入时遍历数组也能得到一致总量”。最后两行明确区分本地混合结果[0, 1]与最终数组[1, 1]。两个闩锁用于教学中固定顺序不能直接当成生产环境的整组快照方案。五、选择方法时的几个注意点更新函数保持无副作用。updateAndGet与getAndUpdate等方法接收更新函数竞争期间函数可能重新应用。不要在函数里发送消息、扣款或累加另一个外部计数。纯粹地从旧值计算新值才能使重试语义清楚。内存语义要按方法核对。get与set分别具有对应的volatile读取与写入效果类还提供plain、opaque、acquire、release等访问方式不能将所有方法笼统视为相同的内存语义。学习时先用含义清晰的常规方法确需调整时再核对官方方法说明。原子性不会扩大整数范围。int计数仍可能溢出。索引越界会抛出异常槽位分配也应由应用保证正确。AtomicIntegerArray没有跨多个索引的一次性更新接口。六、 思维导图AtomicIntegerArray固定长度长度构造初始为零数组构造复制数值单个索引原子累加CAS条件更新区分旧值与新值整组边界分次读取可能混合共享锁或整体替换使用细节更新函数无副作用按方法核对内存语义注意索引与溢出七、总结总结要点每个槽位独立保证原子操作。累加与条件更新可以直接交给AtomicIntegerArray先get再set的业务组合仍需要自己处理并发边界。多格读取有时序。连续两次get可能拼出不同时间的结果。整组约束与一致快照需要所有参与者共同遵循额外协议。返回值与更新函数都影响用法。分清旧值和新值保持更新函数无副作用再根据需求核对内存语义和数值范围。下一篇继续看AtomicLongArray计数范围扩大之后哪些用法可以沿用哪些并发边界依旧存在。如果你觉得这篇文章对你有所帮助欢迎点赞、收藏、分享
返回列表