在多线程的情况下,对一个值进行 a++ 操作,会出现什么问题?
a++ 的问题先写个 demo 的例子。把 a++ 放入多线程中运行一下。定义 10 个线程,每个线程里面都调用 5 次 a++,把 a 用 volatile 修饰,可以让 a 的值在修改之后,所有的线程立刻就可以知道。最后结果是不是 50,还是其他的数字?
public class test { private static volatile int a = 0; public static void main(string[] args) { thread[] threads = new thread[10]; for (int i = 0; i < 10; i++) { threads[i] = new thread(new runnable(){ @override public void run() { try { for(int j = 0; j < 10; j++) { system.out.print(a++ + , ); thread.sleep(100); } } catch (exception e) { } } }); threads[i].start(); } }}
从结果上看 a++ 的操作并没有达到预期值的 50,而是少了很多,其中还有一定是有问题的。那就是因为 a++ 的操作并不是原子性的。
原子性并发编程,有三大原则:有序性、可见性、原子性
有序性:正常编译器执行代码,是按顺序执行的。有时候,在代码顺序对程序的结果没有影响时,编译器可能会为了性能从而改变代码的顺序。可见性:一个线程修改了一个变量的值,另外一个线程立刻可以知道修改后的值。原子性:一个操作或者多个操作在执行的时候,要么全部被执行,要么全部都不执行。上面的 a++ 就没有原子性,它有三个步骤:
在内存中读取了 a 的值。对 a 进行了 + 1 操作。将新的 a 值刷回到内存。这三个步骤可以被示例中的 10 个线程上下文切换打断:当 a = 10
线程 1 将 a 的值读取到内存, a = 10线程 2 将 a 的值读取到内存, a = 10线程 1 将 a + 1,a = 11此时线程发生切换,线程 2 对 a 进行 + 1 操作, a = 11线程 2 将 a 的值写回到内存, a = 11线程 1 将 a 的值写回到内存, a = 11从上面的步骤中可以看出 a 的值在两次相加后没有得到 12 的值,而是 11。这就是 a++ 引发的问题。
小 b 把上面的步骤对面试官讲了一遍,面试官又问了,有什么方式可以避免这个问题,小 b 不加思索的回答用 synchronized 加锁。面试官说 synchronized 太重了,还有其他的解决方式吗?小 b 晕了。其实可以使用 atomicinteger 的 incrementandget() 方法。
atomicinteger 源码分析主要属性首先看看 atomicinteger 的主要属性。
//sun.misc 下的类,提供了一些底层的方法,用于和操作系统交互private static final unsafe unsafe = unsafe.getunsafe();// value 字段的内存地址相对于对象内存地址的偏移量private static final long valueoffset;//通过 unsafe 初始化 valueoffset,获取偏移量static { try { valueoffset = unsafe.objectfieldoffset (atomicinteger.class.getdeclaredfield(value)); } catch (exception ex) { throw new error(ex); }}// 用 valatile 修饰的值,保证了内存的可见性private volatile int value;从属性中可以看出 atomicinteger 调用的是 unsafe 类,unsafe 类中大多数的方法是用 native 修饰的,可以直接进行一些系统级别的操作。
用 volatile 修饰 value 值,保证了一个线程的值对另外一个线程立即可见。
incrementandget()//atomicinteger.incrementandget()public final int incrementandget() { //调用 unsafe.getandaddint() return unsafe.getandaddint(this, valueoffset, 1) + 1;}//unsafe.getandaddint()//参数:需要操作的对象,偏移量,要增加的值public final int getandaddint(object var1, long var2, int var4) { int var5; do { var5 = this.getintvolatile(var1, var2); } while(!this.compareandswapint(var1, var2, var5, var5 + var4)); return var5;}//unsafe.compareandswapint()public final native boolean compareandswapint(object var1, long var2, int var4, int var5);incrementandget() 首先获取了当前值,然后调用 compareandswapint() 方法更新数据。
compareandswapint() 是 cas 的缩写来源,比较并替换。被 native 修饰,调用了操作系统底层的方法,保证了硬件级别的原子性。
var2,var4,var5 是它的三个操作数,表示内存地址偏移量 valueoffset,预期原值 expect,新的值 update。把 this.compareandswapint(var1, var2, var5, var5 + var4) 变成 this.compareandswapint(obj, valueoffset, expect, update),释义就是如果内存位置中的 valueoffset 值 与 expect 的值相同,就把内存中的 valueoffset 改成 update,否则不操作。
getandaddint() 方法中用了 do-while,就相当于如果 cas 一直更新不成功,就不退出循环。直到更新成功为止。
aba 问题cas 操作也并不是没有问题的。
循环操作时间长了,开销大。用了 do-while,如果更新一直不成功,就一直在循环。会给 cpu 带来很大的开销。只能保证一个共享变量的原子性。循环 cas 的方式只能保证一个变量进行原子操作,在对多个变量进行 cas 的时候就没办法保证原子性了。aba 问题。cas 的操作一般是 1. 读取内存偏移量 valueoffset。2. 比较 valueoffset 和 expect 的值。3. 更新 valueoffset 的值。如果线程 a 读取 valueoffset 后,线程 b 修改了 valueoffset 的值,并且将 valueoffset 的值又改了回来。线程 a 会认为 valueoffset 的值并没有改变。这就是 aba 问题。要解决这个问题,就是在每次修改 valueoffset 值的时候带上一个版本号。总结这篇文章介绍了 cas,它是 java 中的乐观锁,每次认为操作并不会有其他线程去修改数据,如果有其他线程操作了数据,就重试,一直到成功为止。
关于进一步促进工业设计发展的若干措施
基于Zynq压电陶瓷传感器的高精度采集系统设计
旭宇光电:实现LED应用的无限可能
半导体和电化学的一氧化碳报警器在地下车库中的应用
使用DS2790生成随机数
在多线程的情况下如何对一个值进行 a++ 操作
C语言中指针的定义与使用
RA6M3 HMI Board 之ADC获取电压值
中检南方受邀参加2020年中国国际信息通信展
思睿达AC/DC芯片工程测试数据曝光
小芯片设计让AMD处理器更便宜
买冰箱时 选单开门还是双开门有窍门
推荐一些适合学生党而且音质也可媲美有线的蓝牙耳机
薄膜瑕疵检测设备的原理、参数及优势
数据对企业具有重要价值
有机硅增强汽车电子产品的可靠性
科技小达人多款4G无线工业物联网网关终端设备选型表
理解人工智能的的12大特点
无人值守变电站综合监控系统
线性恒流LED闪光警示灯专用驱动芯片H7310特性介绍