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

途中也可看到每一个进程都有着显示操作系统分配使用的对应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 读者可以试试。

显然多线程并发的带来的这种不确定结果,不是编程设计所想要的。
为什么产生并发问题
IntStream.range(0, 1000).forEach(i -> { val +=1; });
要详细阐述并发问题的产生,仔细分析下上述代码。计算机运行程序底层其实也是一条条指令在执行。对于val +=1 这行语句,编译完后其实有4条语句。
- GETSTATIC 将静态变量 val压入栈中;
- ICONST_1 将常量1压入栈中;
- IADD 执行加(+)运算操作;
- PUTSTATIC 将结果放回 val变量。

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

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

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

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

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

这个例子里面 多线程性能优势,与单线程的1914ms 相比多线程只需要 262ms。当然具体提升的数值和运行的机器、CPU等等有关系,笔者电脑是 4核8线程的情况。
本篇总结下,介绍了进程、线程以及相关发展史;展示了一个具体的并发问题;详细分析了并发问题的发生原因以及解决办法。最后对多线程并发程序进行了验证,以及相关性能上的探究。
写在最后,文章中使用的Unsafe 类的功能, 在实际编程中绝大部分情况下都不会使用 ;更多地使用 java.util.concurrent 下提供的功能。比如例子中的多线程操作整数加1,应该使用的是 AtomicInteger 。关于Java并发编程其他技巧后续文章中,接着进行讲解。
欢迎长期关注公众号/头条号(Java研究者)
Java并发编程 | 从进程、线程到并发问题实例解决的更多相关文章
- Java并发编程系列-(2) 线程的并发工具类
2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...
- 并发编程-concurrent指南-线程池ExecutorService的实例
1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { ...
- Java并发编程:进程的创建
Java并发编程:进程的创建 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: #839496;} J ...
- python并发编程之进程、线程、协程的调度原理(六)
进程.线程和协程的调度和运行原理总结. 系列文章 python并发编程之threading线程(一) python并发编程之multiprocessing进程(二) python并发编程之asynci ...
- Java并发编程的艺术,解读并发编程的优缺点
并发编程的优缺点 使用并发的原因 多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升. 在特殊的业务场景下先天的就适合于并发编程. 比如在 ...
- java并发编程笔记(二)——并发工具
java并发编程笔记(二)--并发工具 工具: Postman:http请求模拟工具 Apache Bench(AB):Apache附带的工具,测试网站性能 JMeter:Apache组织开发的压力测 ...
- Java并发编程:进程和线程之由来
Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够融会贯通 ...
- java并发编程:进程和线程
java并发编程涉及到很多内容,当然也包括多线程,再次补充点相关概念 原文地址:http://www.cnblogs.com/dolphin0520/p/3910667.html 一.操作系统中为什么 ...
- Java并发编程:进程和线程之由来__进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能
转载自海子:http://www.cnblogs.com/dolphin0520/p/3910667.html Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨 ...
随机推荐
- jdbc 02: 连接mysql,并实现删除与更新
jdbc连接mysql,并实现删除与更新 package com.examples.jdbc.o2_删除与更新; import java.sql.*; //连接与插入 /* jdbc删除操作 */ p ...
- mybatis-plus详解
旧的代码生成 记得导包,依赖如下 <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</grou ...
- 没错,请求DNS服务器还可以使用UDP协议
目录 简介 搭建netty客户端 在netty中发送DNS查询请求 DNS消息的处理 总结 简介 之前我们讲到了如何在netty中构建client向DNS服务器进行域名解析请求.使用的是最常见的TCP ...
- 使用OnPush和immutable.js来提升angular的性能
angular里面变化检测是非常频繁的发生的,如果你像下面这样写代码 <div> {{hello()}} </div> 则每次变化检测都会执行hello函数,如果hello函数 ...
- md 中超链接的解析问题:解析`this.$set()`,`$`前要加空格或转义符 `\`
在用 Editor.md 写文档时,插入超链接,发现一个奇怪的现象: 要想正确显示超链接,必须在 $ 前加空格或转义符 \
- linux学习(小白篇)
当前服务器:centos 7 shell命令框:xshell 文件预览及上传:xftp (界面化软件,非常好用) 数据库连接:navicat 此文是在学习linux时做一个指令合集,方便自己查阅 进文 ...
- gitpod.io,云端开发调试工具。
gitpod,一款在线开发调试工具,使用它你可以在网页上直接开发软件项目了. 比如你的项目仓库在github上,你可以直接在网址的前面添加gitpod.io/#,然后回车就能在网页上使用vscode打 ...
- Java多线程开发系列之五:Springboot 中异步请求方法的使用
Springboot 中异步线程的使用在过往的后台开发中,我们往往使用java自带的线程或线程池,来进行异步的调用.这对于效果来说没什么,甚至可以让开发人员对底层的状况更清晰,但是对于代码的易读性和可 ...
- 如何快速上手AIRIOT?
AIRIOT物联网低代码平台,快速构建稳定可靠的物联网系统,丰富的功能库及组件库,具备低成本.高效率.易操作,可扩展等特点,节省物联网项目实施时间及人力成本,支持二次开发. [六步快速上手,玩儿转 ...
- True 和 False 分别代表数字中的几?形象地记忆
True 和 False 作为布尔值分别代表的意思是真和假. 灯泡亮起就是 1,灯泡熄灭就是 0.0 就是无状态,所以可以代表灯泡熄灭的状态,而 1 就是有状态的,所以可以代表灯泡亮起的状态. 那么, ...