当前位置: 首页 > news >正文

工作5年,没接触过高并发编程,这正常吗?

目录

    • 一、原子性
    • 二、可见性
      • 1、串行
      • 2、单核CPU
      • 3、多线程多CPU时的可见性问题
      • 4、看下面一段代码,猜猜看删除结果
    • 三、有序性
    • 四、解决方案
      • Java高并发编程实战系列文章
      • 哪吒那些年写过的优秀文章
    • 五、Java高并发与集合框架:JCF和JUC源码分析与实现

这当然不正常,赶紧补起来,先从最基础的高并发特性学起来。

一、原子性

原子性指操作在CPU执行的过程中,不可中断,也不可在中途切换,要么执行完成、要么不执行。

简单的分析一下原子性问题,写一段大众代码,如下:

package com.nezha.thread;

/**
 * @Autor 哪吒
 * @Date 2022-09-10
 */
public class ThreadAtomicityTest {

    private int step;

    public int getStep(){
        return step;
    }

    public void increaseStep(){
        step++;
    };
}

看不出什么问题,都这么写啊。

使用JDK自带的javap查看一下程序的指令码:

D:\MyProject\target\classes\com\nezha\thread>javap -c ThreadAtomicityTest.class
Compiled from "ThreadAtomicityTest.java"
public class com.nezha.thread.ThreadAtomicityTest {
  public com.nezha.thread.ThreadAtomicityTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int getStep();
    Code:
       0: aload_0
       1: getfield      #2                  // Field step:I
       4: ireturn

  public void increaseStep();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field step:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field step:I
      10: return
}

重点看一下increaseStep的指令码,大概包含三大步骤:

  1. 将变量step从内存中加载到CPU的寄存器中;
  2. 在CPU的寄存器中执行step++操作;
  3. 将step++后的结果写入缓存(CPU缓存或计算机内存);

线程切换可能发生在任何一条指令完成之后,而不是Java某条语句完成后。

假设线程1和线程2同时执行increaseStep()方法,在线程1执行过程中,CPU完成指令码的步骤①后发生了线程切换,此时线程2开始执行指令码的步骤①。
当两个线程都执行完整个increaseStep()方法后,得到的step的值是1而不是2。Why is this?

在这里插入图片描述

如图所示,线程1将step=0加载到CPU的寄存器后,发生了线程切换。此时还没有执行step++操作,也没有将操作的结果写入内存,所以,内存中的step值仍为0。

线程2将step=0加载到CPU的寄存器中,执行step++操作,并将执行后的结果写入内存。此时,CPU切换到线程1继续执行,在执行线程1中的step++后,线程1中的step仍为1,线程1将step=1写入内存,最终内存中的step为1。

如果在CPU中存在正在执行的线程,此时,发生了线程切换,就可能导致并发编程的原子性问题。

所以,造成原子性问题的根本原因是在线程执行过程中发生了线程切换。

二、可见性

可见性指一个线程修改了共享变量,其它线程能够立刻读到共享变量的最新值。

在并发编程中,有两种情况能实现当一个线程修改了共享变量后,其它线程立刻就能读到最新值。

1、串行

在这里插入图片描述
线程1和线程2是串行执行的,线程1写完数据后,线程2会从主内存中读取数据。线程1向主内存中写入数据对线程2是可见的,所以线程1和线程2之间不存在可见性问题。

2、单核CPU

在单核CPU中,多个线程之间也不会出现可见性问题。

在这里插入图片描述
在单核CPU中,只能有一个线程占用CPU资源来执行任务。当其它线程抢占CPU执行任务时,共享变量中的值一定是最新的。

3、多线程多CPU时的可见性问题

Java中,多个线程在读写内存中的共享变量时,会先把主内存中的共享变量数据复制到线程的工作内存中。每个线程在对数据进行读写操作时,都是直接操作自身的工作内存中的数据。由于每个线程都有自己的工作内存,所以线程1的数据对线程2是不可见的。线程1修改了数据,线程2不一定能够立刻读到修改后的值,这就造成了可见性问题。

在这里插入图片描述

4、看下面一段代码,猜猜看删除结果

package com.guor.demo.sync;

public class SynchronizedTest {

    private static int count = 0;

    public static void  incremetCount(){
        count++;
    }

    public static int increment() throws InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                incremetCount();
            }
        });

        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                incremetCount();
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println(SynchronizedTest.increment());
    }
}

控制台输出:
在这里插入图片描述
在这里插入图片描述
为什么呢?

因为多个线程同时调用incremetCount()方法,出现了线程安全问题。

所以,造成可见性问题的根本原因是CPU缓存机制。

三、有序性

有序性指程序能够按照编写的代码顺序执行,不会发生跳过代码行的情况,也不会出现跳过CPU指令的情况。

那么什么时候会出现有序性问题呢?

为了提高程序的执行性能和编译性能,计算机和编译器有时候会修改程序的执行顺序。

在Java中一个典型的案例就是使用双重检测机制来创建单例对象。

package com.nezha.thread;

/**
 * 线程不安全的单例模式
 */
public class SingleInstance {

    private static SingleInstance instance;

    public static SingleInstance getInstance(){
        if(instance == null){
            synchronized (SingleInstance.class){
                if(instance == null){
                    instance = new SingleInstance();
                }
            }
        }
        return instance;
    }
}

如果编译器和解释器不对上面的代码进行优化,也不改变程序的执行顺序,则代码的执行流程如下图所示:

在这里插入图片描述
如上图所示,假如线程1和线程2同时调用getInstance()方法获取对象实例,两个线程会同时发现instance为空,同时对SingleInstance.class加锁,而JVM会保证只有一个线程获取到锁,这里我们假设线程1获取到锁,线程2因为未获取到锁而进行等待。接下来,线程1再次判断instance对象为空,从而创建instance对象的实例,然后释放锁。此时,线程2被唤醒,再次尝试获取锁,获取锁成功后,线程2检查此时的instance对象已经不再是空,线程2不再创建instance对象。

上述流程看起来没有什么问题,但是,在高并发、大流量的场景下获取instance对象时,使用new关键字创建SingleInstance类的实例对象时,会因为编译器或解释器对程序的优化而出现问题。也就是说,问题的根源在于如下代码:

```instance = new SingleInstance();````

对于上面的代码包含三个步骤:

① 分配内存空间
② 初始化对象
③ 将instance引用指向内存空间

正常执行的CPU指令顺序为①②③,CPU对程序进行重排序后的执行顺序是①③②,此时就会出现问题。

在这里插入图片描述
如上图所示,当线程1判断instance为空时,为对象分配内存空间,并将instance指向内存空间。此时还没有进行对象的初始化,发生了线程切换,线程2获取到CPU资源执行任务。线程2判断此时的instance不为空,则不再执行创建对象的操作,直接返回未初始化的instance对象。

所以,造成有序性问题的根本原因是编译器对程序进行优化,从而可能造成有序性问题。

四、解决方案

在Java中解决原子性问题的方案包括synchronized、Lock、ReentranLock、ReadWriteLock、CAS操作、Java中提供的原子类等。

解决可见性和有序性问题,可以禁用CPU缓存和编译器优化。

JVM提供了禁用缓存和编译优化的方法,包括volatile关键字、synchronized、final关键字以及Java内存模型中的Happens-Before原则。

Java高并发编程实战系列文章

Java高并发编程实战1,那些年学过的锁

哪吒那些年写过的优秀文章

Java学习路线总结,搬砖工逆袭Java架构师

10万字208道Java经典面试题总结(附答案)

【Java基础知识 1】Java入门级概述

Java学习路线总结(思维导图篇)

SQL性能优化的21个小技巧

五、Java高并发与集合框架:JCF和JUC源码分析与实现

本书并不讲解JCF和JUC中各个组件的基本使用方法,因为作者相信JCF和JUC中各种组件的基本使用方法大家通过查看网络资料就可以详细了解。

本书的主要内容是从源代码的层面剖析JCF和JUC的实现原理,以及讲解源代码中蕴含的理论知识,并讲解如何将这些大师级的理论知识应用到实际工作中。

因为是进行源代码分析,所以这本书包含了大量的Java原生模块的源代码,并且进行了逐行注释。请注意是逐行注释,所以在大家阅读本书时,不用担心无法读懂源代码。这解决了很多读者的源代码恐惧症问题。

这些被逐行注释、逐行剖析讲解的源代码,也是本书区别于市面上同类书籍的一大亮点。本书还包含了大量的工作原理图例,这些图例与每一个进行了源码剖析的知识点一一对应,形成了本书独具特点的图文并茂的讲解方式。

例如本书当然会详细剖析ConcurrentHashMap集合的结构和工作过程,但本书更关注ConcurrentHashMap集合为了适应高并发场景所做的典型化设计。

在这里插入图片描述

相关文章:

  • 【微信小程序】带你进入小程序的世界
  • 机器学习-线性回归 二维问题
  • 分享从零开始学习网络设备配置--2.1 交换机基本配置
  • 大数据ClickHouse进阶(九):ClickHouse的From和Sample子句
  • vue3 | HighCharts实战自定义封装之径向条形图
  • Web前端系列技术之Web APIs基础(从基础开始)③
  • 线段树基本操作——建树+单点修改+区间查询
  • python/php/java/nodejs通讯录管理系统vue+elementui
  • 【老生谈算法】matlab实现蒙特卡罗定积分源码——蒙特卡罗定积分
  • 卷积神经网络 - 从全连接层到卷积
  • selenium爬虫如何绕过反爬,看这一篇文章就足够了
  • c语言进阶:冒泡排序函数初步实现到逐步优化
  • 5年测试经验要个20K不过分吧,谁料面试官三个问题把我打发走了···
  • 内网渗透之Msf-Socks代理实战(CFS三层靶场渗透过程及思路)
  • 命令执行漏洞——远程命令执行
  • IE9 : DOM Exception: INVALID_CHARACTER_ERR (5)
  • 【面试系列】之二:关于js原型
  • ES6 学习笔记(一)let,const和解构赋值
  • express.js的介绍及使用
  • java多线程
  • Linux中的硬链接与软链接
  • select2 取值 遍历 设置默认值
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • 第2章 网络文档
  • 将回调地狱按在地上摩擦的Promise
  • 普通函数和构造函数的区别
  • 前端每日实战 2018 年 7 月份项目汇总(共 29 个项目)
  • 如何使用 OAuth 2.0 将 LinkedIn 集成入 iOS 应用
  • 使用Envoy 作Sidecar Proxy的微服务模式-4.Prometheus的指标收集
  • 适配iPhoneX、iPhoneXs、iPhoneXs Max、iPhoneXr 屏幕尺寸及安全区域
  • 通信类
  • 写代码的正确姿势
  • 栈实现走出迷宫(C++)
  • 2017年360最后一道编程题
  • 国内开源镜像站点
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • #include<初见C语言之指针(5)>
  • #Lua:Lua调用C++生成的DLL库
  • (30)数组元素和与数字和的绝对差
  • (pojstep1.3.1)1017(构造法模拟)
  • (读书笔记)Javascript高级程序设计---ECMAScript基础
  • (二)fiber的基本认识
  • (附源码)springboot 基于HTML5的个人网页的网站设计与实现 毕业设计 031623
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (六)vue-router+UI组件库
  • (三)Pytorch快速搭建卷积神经网络模型实现手写数字识别(代码+详细注解)
  • (十一)JAVA springboot ssm b2b2c多用户商城系统源码:服务网关Zuul高级篇
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (图)IntelliTrace Tools 跟踪云端程序
  • (轉貼) 2008 Altera 亞洲創新大賽 台灣學生成果傲視全球 [照片花絮] (SOC) (News)
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .NET 4 并行(多核)“.NET研究”编程系列之二 从Task开始
  • .net 反编译_.net反编译的相关问题