Java并发编程 | 从进程、线程到并发问题实例解决( 二 )


  1. GETSTATIC 将静态变量 val压入栈中;
  2. ICONST_1 将常量1压入栈中;
  3. IADD 执行加(+)运算操作;
  4. PUTSTATIC 将结果放回 val变量 。
    Java并发编程 | 从进程、线程到并发问题实例解决

    文章插图
    可以看到执行 +1 这个操作其实是在独立栈内进行,不同线程其实有不同的操作栈 。
如果线程(1)还未执行完 PUTSTATIC 操作,另外一个线程(2)进行了 GETSTATIC ;这个时候线程(2)执行 +1 操作时,就不会使用线程(1)+1 执行完成后的结果 。
当同样执行到 PUTSTATIC 时,也不会考虑线程(1)情况 直接把自己运算结果写进 val 。这样也就出现了并发问题,并非我们想象的多线程执行都能改变val的值 。
Java并发编程 | 从进程、线程到并发问题实例解决

文章插图
怎么解决这种并发问题?设计初衷上说val+1操作的逻辑时希望在读取val值上进行+1的操作,而非在+1过程中初始val值由于其他线程操作而改变 。因此在计算机指令上就给到了一个指令 cmpxchg,在将栈里面值交换到堆里面val时,比较val初始值么没有变化执行成,否则执行失败 。如果指令执行失败了,我们再重新进行新val值的计算直到完成一次成功操作 。这也就是 解决Java并发一个基本算法 CAS(Compare-and-Swap) 。
CAS算法有三个操作数,通过内存中的值(V)、预期原始值(A)、修改后的新值 。
如果内存中的值和预期原始值相等,就将修改后的新值保存到内存中 。如果内存中的值和预期原始值不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作,直到重试成功 。Java中Unsafe 中的getAndAddInt就是使用的这个算法,不妨详细解读下其代码 。
Java并发编程 | 从进程、线程到并发问题实例解决

文章插图
到这里还涉及到一个线程变量修改同步问题,由于计算机结构复杂性,CPU、Mem等各级缓存特性、不同操作系统、不同厂商硬件等等,其中有着很多缓存/同步设计;为了屏蔽这些复杂性,java提供了volatile 关键字来进行保证 。截取一段The Java Language Specification (Java SE 10 Edition)原文:
Java并发编程 | 从进程、线程到并发问题实例解决

文章插图
抓重点的理解:字段被声明为volatile,在这种情况下,Java内存模型确保所有线程都看到变量的一致值 。
试一试,多线程性能更好?按照前面解决的思路,修改下之前的代码进行测试下 。另外将耗时也记录一下:
Java并发编程 | 从进程、线程到并发问题实例解决

文章插图
是不是发现,val 的数值已经和单线程的一致了都是 1000,没有并发问题了 。性能上从这个例子可以看到,单线程耗时6ms,多线程耗时29ms 。不用质疑结果是没错的,明显多线程耗时更高 。
可以看出多线程运行简单程序并不一定能够提升性能,因为其开启线程有相关的开销;同时看到其 复杂性高、维护成本高、可读性降低 等缺陷 。对于简单业务逻辑场景,不建议用多线程 。
在此基础上,加上模拟下相关业务逻辑,模拟逻辑执行doSomeThings(),模拟实现逻辑就是线程休眠 1ms 。相关代码,耗时记录如下:
Java并发编程 | 从进程、线程到并发问题实例解决

文章插图
这个例子里面 多线程性能优势,与单线程的1914ms 相比多线程只需要 262ms 。当然具体提升的数值和运行的机器、CPU等等有关系,笔者电脑是 4核8线程的情况 。
本篇总结下,介绍了进程、线程以及相关发展史;展示了一个具体的并发问题;详细分析了并发问题的发生原因以及解决办法 。最后对多线程并发程序进行了验证,以及相关性能上的探究 。

经验总结扩展阅读