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运行由于执行次数较大大概率结果会是 小于(

为什么产生并发问题

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)。

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线程的情况。

本篇总结下,介绍了进程、线程以及相关发展史;展示了一个具体的并发问题;详细分析了并发问题的发生原因以及解决办法。最后对多线程并发程序进行了验证,以及相关性能上的探究。

写在最后,文章中使用的Unsafe 类的功能, 在实际编程中绝大部分情况下都不会使用 ;更多地使用 java.util.concurrent 下提供的功能。比如例子中的多线程操作整数加1,应该使用的是 AtomicInteger 。关于Java并发编程其他技巧后续文章中,接着进行讲解。

欢迎长期关注公众号/头条号(Java研究者)

Original: https://www.cnblogs.com/jzhlin/p/16753439.html
Author: Java研究者
Title: Java并发编程 | 从进程、线程到并发问题实例解决

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/615859/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 异常:Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.3.2.RELEASE:repackage (repackage) 解决办法

    异常:Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.3.2.RELEASE:…

    Java 2023年6月9日
    086
  • 详解JS中 call 方法的实现

    摘要:本文将全面的,详细解析call方法的实现原理 本文将全面的,详细解析call方法的实现原理,并手写出自己的call方法,相信看完本文的小伙伴都能从中有所收获。 call 方法…

    Java 2023年6月15日
    081
  • 【Redis】Redis Cluster-集群故障转移

    集群故障转移 节点下线 在集群定时任务 clusterCron中,会遍历集群中的节点,对每个节点进行检查,判断节点是否下线。与节点下线相关的状态有两个,分别为 CLUSTER_NO…

    Java 2023年6月8日
    095
  • Spring基于注解+扫描管理bean

    @Component:将类标识为普通组件 @Controller:将类标识为控制层组件 @Service:将类标识为业务层组件 @Repository:将类标识为持久层组件(dao…

    Java 2023年6月16日
    087
  • 几大,几届,几中 常识

    “几大”是指? 党的几大,是指党的全国代表大会。 按照《中国共xx章程》规定,党的全国代表大会每五年举行一次,我们通常简称为”**大&#8221…

    Java 2023年6月5日
    085
  • Spring Boot + Spring Cloud 构建微服务系统(十):配置中心(Spring Cloud Bus)

    技术背景 我们在上一篇讲到,Spring Boot程序只在启动的时候加载配置文件信息,这样在GIT仓库配置修改之后,虽然配置中心服务器能够读取最新的提交信息,但是配置中心客户端却不…

    Java 2023年5月30日
    070
  • SpringMVC学习笔记

    本文转载自尚硅谷杨博超老师的笔记,视频链接–>哔哩哔哩 一、SpringMVC简介 1、什么是MVC MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划…

    Java 2023年6月8日
    086
  • MySQL索引凭什么能让查询效率提高这么多?

    我相信大家在数据库优化的时候都会说到索引,我也不例外,大家也基本上能对数据结构的优化回答个一二三,以及页缓存之类的都能扯上几句,但是有一次阿里P9的一个面试问我:你能从计算机层面开…

    Java 2023年6月9日
    0149
  • java网络编程(TCP详解)

    网络编程详解-TCP 一,TCP协议的特点面向连接的协议(有发送端就一定要有接收端) 通过三次连接握手建立连接 通过四次握手断开连接 基于IO流传输数据 传输数据大小没有限制 速度…

    Java 2023年6月8日
    0171
  • nginx+tomcat+java部署总结

    昨天部署了一下nginx+tomcat+java出现了很多问题,以下为整理总结。 使用了两种部署方式,一种是源码部署,一种是war部署。 java源码部署总结: 环境:nginx+…

    Java 2023年5月29日
    076
  • 回文字符串_125_680

    题目描述: 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写,和空格影响。 说明:本题中,我们将空字符串定义为有效的回文串。 题目描述: 给定一个非空…

    Java 2023年6月5日
    0102
  • (转发)使用 YUMI UEFI 0.0.4.3 在单个 USB 磁盘上制作多系统启动盘

    YUMI 全称是 Your Universal Multiboot Integrator, 可用于制作 usb 启动盘,在同一个 usb 盘上,存放多个操作系统,比如,多个不同版本…

    Java 2023年6月9日
    090
  • Redis变慢?深入浅出Redis性能诊断系列文章(四)

    (本文首发于”数据库架构师”公号,订阅”数据库架构师”公号,一起学习数据库技术,助力职业发展) 本篇为Redis性能问题诊断系列的第…

    Java 2023年6月16日
    084
  • rabbitmq 一些参数设置

    rabbitmq 方法的一些参数设置以及解释 消费者采用手动应答 // 采用手动应答 false 采用手动应答,true采用自动应答 boolean autoAck = false…

    Java 2023年6月9日
    080
  • lua中获取nginx变量

    –属性 method = ngx.var.request_method — http://wiki.nginx.org/HttpCoreModule#.24request_me…

    Java 2023年5月30日
    082
  • Spring中获取bean的方式

    1. 获取bean 在上图的测试类中我们是通过id来获取bean的。实际上获取bean的方式有很多种,下面我们就一一说明。 由于 id 属性指定了 bean 的唯一标识,所以根据 …

    Java 2023年6月14日
    079
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球