19-10-17 总结:对唯一的临界值 i 的操作只有一行代码:compareAndSet,我们只需要关心这行代码就能解决多线程计算问题, 添加常量 current 是为了监听其它线程的干扰,一旦被干扰,current 就与 i 不同,通过 compareAndSet 就能知道已经被干扰了。 compareAndSet 返回 true 意味着 i 还没有被其它线程干扰,并且已经成功操作 i,所以我们直接让线程结束就行。 compareAndSet 返回 false 意味着 i 已经被其他线程干扰了,所以我们需要重新添加干扰监听,以及操作 i。
放图 双线程执行 I++,正确结果为 I=2: —— 普通多线程会因计算顺序同步而出错 —— 利用 compareAndSet(蓝色部分) 让多线程计算正确/* 为什么多线程计算临界资源会错误? 参考帖:https://www.cnblogs.com/wxd0108/p/5479442.html 即: 多线程的内存模型分【主存】和【线程栈】,在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去, 这导致代码的执行增加了保存和读取操作,所以多线程操作在以一行一行执行为前提而写的代码中很可能出现错误。 例如:2个线程执行 i++ 1、线程1 load i的值(0) 到本地栈 2、线程2 load i的值(0) 到本地栈 3、线程1 save i的值(1) 到主存 4、线程2 save i的值(1) 到主存 5、最终,主存 i = 1,而非 i=2 为何原子操作可以解决多线程计算错误呢? 网上对于原子操作原理的解释是:https://www.jianshu.com/p/9ff426a784ad 即: 用CPU的一条指令完成这么一个操作: A.compareAndSet(B,C) 比较 A“要更新的变量” 与 B“变量的预期值” 若两者相同,将 A 设为 C“变量的新值” 并return是否相同 使用CPU的一条指令,是原子行为,无法再多线程情况下拆分。 i.compareAndSet(0,1) //这一句就是具体的使用, 比较 i == 0 则返回 true 并让 i = 1; 反之若 i != 0 则返回 false 并不改变任何变量 由于底层是CPU的一条指令,这行代码是原子性的,是不会再多线程情况下因执行顺序而出岔子的,但显然单单一行代码是没什么卵用 看看官方的使用例子: fun incrementAndGet(){//官方的原子自加操作 for(;;){ int current = get(); //保存当前的值到current。get()方法仅用于得到当前数字值 int next = current + 1; //计算出自加后的值到next if(compareAndSet(current,next)){ //若此时当前的值与自身值相同,则原子的自加一 return next; //死循环直到计算成功 } } } 看到这例子时,我有个疑问: 这一堆是什么鬼,为什么可以解决多线程计算出错问题?直接使用 i.compareAndSet(i,i+1)来实现原子自加不行吗? tip:在多线程操作时要把代码拆分成不能再拆分的样子才好观察: ———————————————————————————————————————————————————— 实验1: val next = i + 1 //设 i 初始为0 i.compareAndSet(i,next) //原子操作,无法分解 猜想上述代码依次执行两遍,第一遍执行后i=1,第二遍执行后i=2 假如是这么执行的: 1、线程1 计算出 next = 1 2、线程2 计算出 next = 1 3、线程1 执行 compareAndSet 判断 i(0) == i(0),执行赋值 i = next(1) = 1 4、线程2 执行 compareAndSet 判断 i(1) == i(1),执行赋值 i = next(1) = 1 5、最终运算得 i = 1 而不是 i = 2 在上述操作中,参B“变量的预期值”是不可靠的,它导致compareAndSet永远成功执行,进而让第4步的compareAndSet执行成功了, 既然错误的原因是compareAndSet由于参数的错误失去了它自身的意义,那么让 参B 变得可靠就行了吧。 ———————————————————————————————————————————————————— 实验2: val current = i //设 i 初始为0 val next = current + 1 //不写 next = i + 1 是因为 i 是不可靠的变量,而 current 是可靠的常量 i.compareAndSet(current,next) 猜想上述代码依次执行两遍,第一遍执行后i=1,第二遍执行后i=2
执行假设: 1、线程1 计算出 current = 0 2、线程2 计算出 current = 0 3、线程1 计算出 next = 1 4、线程2 计算出 next = 1 5、线程1 执行 compareAndSet:i(0) == current(0),i = 1 6、线程2 执行 compareAndSet:i(1) != current(0) 7、最终运算得 i = 1 仍然失败了,因为第6步 compareAndSet 判断失败导致线程2没有让 i+1 那么让它重新进行判断咯 ———————————————————————————————————————————————————— 实验3: while(true){ val current = i //设 i 初始为0 val next = current + 1 if(i.compareAndSet(current,next)){ //+1成功会返回 true,当所有线程中+1都成功才是计算结束 break } } 执行假设: 1、线程1 计算出 current = 0 2、线程2 计算出 current = 0 3、线程1 计算出 next = 1 4、线程2 计算出 next = 1 5、线程1 执行 compareAndSet:i(0) == current(0),i = 1; if公式成立,线程1计算结束 6、线程2 执行 compareAndSet:i(1) != current(0); if公式失败 7、线程1 执行 break 退出循环 8、线程2 进入第二次循环 此时 i = 1 9、线程2 计算出 current = 1 10、线程2 计算出 next = 2 11、线程2 执行 compareAndSet:i(1) == current(1),i = 2; if公式成立,线程2计算结束 12、最终运算得 i = 2 执行假设2: 1、线程1 计算出 current = 0 2、线程1 计算出 next = 1 3、线程1 执行 compareAndSet:i(0) == current(0),i = 1; if公式成立,线程1计算结束 4、线程2 计算出 current = 1 5、线程2 计算出 next = 2 6、线程2 执行 compareAndSet:i(1) == current(1),i = 2; if公式成立,线程2计算结束 7、最终运算得 i = 2 ———————————————————————————————————————————————————— 区区 while 和 val current = i 竟能解决线程的不靠谱!为什么? 1、有点像数据库的同步块,使用 while(true){} 包裹的同步快,一进入这个快,就意味着必须运算正确,否则要重新运算。 2、每个线程的运算都从 val current = i 开始,直到 compareAndSet == true 来结束运算,并保留了运算结果。 3、整个运算从头到尾,除了 i 没有其它的变量干扰,所以运算结果一定是固定的(结果仅由i来决定)。 4、无论线程如何穿插执行,最后的 compareAndSet 限制死了“只有第一个跑到我这的(线程),才是运算成功的,其它的都回去重跑” */ //Kotlin 1.3 新特性,直接运行 main() //执行测试 fun main() { val i = AtomicInteger(0) //主角登场 //启动4个线程 (0..3).forEach { j -> thread(name = "线程$j") { //每个线程计算1000次 (0 until 1000).forEach { while (true) { val current = i.get() // 通过 get() 来得到值 val next = current + 1 if (i.compareAndSet(current, next)) { //展示计算,当然可能顺序不对,但最终结果一定正确 println("${Thread.currentThread().name} -> $i") break } } } } } }
傻瓜式的解答:为什么原子运算 AtomicInteger 可以解决多线程计算临界资源错误
最新推荐文章于 2023-12-02 18:04:34 发布