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

计划写几篇文章讲述下Java并发编程,帮助一些初学者成体系的理解并发编程并实际使用,而不只是碎片化的了解一些Synchronized、ReentrantLock等技术点 。在讲述的过程中,也想融入一些相关技术、概念的发展历史 , 这样便于看到其演化过程而更好地进行理解 。文字描述上希望是更通俗些,如果阅读者能在寥寥文字中稍有所得就很满足了 。
什么是进程?在日常使用计算机的过程中我们会用各类的软件来处理各种事物,比如听歌、看视频、写文档等等 。对于相对简单的软件对应于Windows操作系统就是一个任务,用计算机术语上说也是一个进程;当然对于复杂的软件在启动的时候也有启动多个进程 。切实感受的话,如果熟悉的 Ctrl+Alt+Del 控制台任务管理器上就能看到,如下图:

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

文章插图
途中也可看到每一个进程都有着显示操作系统分配使用的对应CPU、内存、磁盘等资源的信息,这也是常可以听说到的一句话:进程是资源分配的最小单位。
如果回到 Java 中 , 最开始编程时运行的 Main 函数其实就是执行一个控制台进程 。也是另外听到的描述 进程是正在运行的程序的实例。专业一点定义来说 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。
历史角度上说,进程最先是60年代初由 麻省理工学院的MULTICS系统和IBM公司的CTSS/360系统 引出的 。
从进程到线程如果回到60年代,计算机其实是没有线程的;随着各行业系统软件发展 , 进程很多缺陷开始凸显,比如进程是有分配资源,在进程进行切换/创建等时候其实时间也好、内存空间也好耗费都非常的大 。于是开始了有轻型进程等一些设计概念,大约到了80年代左右,线程(Threads)正式开始出现 。
从历史发展可以看到线程解决进程承担分配资源等过重的作用而产生的,所以有些操作系统里面一直也有称之为轻量级进程,在进程(Process)单词上加上Lightweight 轻量线程(Lightweight Process) , 也有说法叫内核线程(Kernel thread) 。
同一个进程往往包含多个线程,是计算机操作系统进行运算调度的最小单位 。多线程之间是可以共享同一进程的资源的 。存在共享,这其实就代表了 其存在竞争关系;比如:多个线程同时变更同一个变量的场景 。在Java编程体系下,如何解决这种并发使用资源的问题,指的就是Java并发编程 。
什么是并发问题?用简单代码来举例演示下并发的问题,定义一个变量 val 分别使用单线程/多线程的方式来对 int val 执行 1000000 次 加1 的操作 。系统在执行加1操作,底层其实包含了读取val值 和 修改val值的两个指令 。因此在多线程执行的条件下,没有使用到Java并发编程技巧 , 将会在操作执行 变更val变量上产生并发操作 。
单线程结果当然会是 1000000,多线程CPU运行由于执行次数较大大概率结果会是 小于(<) 1000000 。下图为笔者运行的结果,“more threads val is 240799 ”。当然运行多次不一定是 240799 , 但一般都会小于(<) 1000000 读者可以试试 。
Java并发编程 | 从进程、线程到并发问题实例解决

文章插图
显然多线程并发的带来的这种不确定结果,不是编程设计所想要的 。
为什么产生并发问题IntStream.range(0, 1000).forEach(i -> {val +=1; });要详细阐述并发问题的产生,仔细分析下上述代码 。计算机运行程序底层其实也是一条条指令在执行 。对于val +=1 这行语句,编译完后其实有4条语句 。
  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) 。

推荐阅读