关于并发编程与线程安全的思考与实践 | 京东云技术团队

news2024/9/23 13:10:28

作者:京东健康 张娜

一、并发编程的意义与挑战

并发编程的意义是充分的利用处理器的每一个核,以达到最高的处理性能,可以让程序运行的更快。而处理器也为了提高计算速率,作出了一系列优化,比如:

1、硬件升级:为平衡CPU 内高速存储器和内存之间数量级的速率差,提升整体性能,引入了多级高速缓存的传统硬件内存架构来解决,带来的问题是,数据同时存在于高速缓存和主内存中,需要解决缓存一致性问题。

2、处理器优化:主要包含,编译器重排序、指令级重排序、内存系统重排序。通过单线程语义、指令级并行重叠执行、缓存区加载存储3种级别的重排序,减少执行指令,从而提高整体运行速度。带来的问题是,多线程环境里,编译器和CPU指令无法识别多个线程之间存在的数据依赖性,影响程序执行结果。

并发编程的好处是巨大的,然而要编写一个线程安全并且执行高效的代码,需要管理可变共享状态的操作访问,考虑内存一致性、处理器优化、指令重排序问题。比如我们使用多线程对同一个对象的值进行操作时会出现值被更改、值不同步的情况,得到的结果和理论值可能会天差地别,此时该对象就不是线程安全的。而当多个线程访问某个数据时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,这个计算逻辑始终都表现出正确的行为,那么称这个对象是线程安全的。因此如何在并发编程中保证线程安全是一个容易忽略的问题,也是一个不小的挑战。

所以,为什么会有线程安全的问题,首先要明白两个关键问题:

1、线程之间是如何通信的,即线程之间以何种机制来交换信息。

2、线程之间是如何同步的,即程序如何控制不同线程间的发生顺序。

二、Java并发编程

Java并发采用了共享内存模型,Java线程之间的通信总是隐式进行的,整个通信过程对程序员完全透明。

2.1 Java内存模型

为了平衡程序员对内存可见性尽可能高(对编译器和处理的约束就多)和提高计算性能(尽可能少约束编译器处理器)之间的关系,JAVA定义了Java内存模型(Java Memory Model,JMM),约定只要不改变程序执行结果,编译器和处理器怎么优化都行。所以,JMM主要解决的问题是,通过制定线程间通信规范,提供内存可见性保证。

JMM结构如下图所示:

以此看来,线程内创建的局部变量、方法定义参数等只在线程内使用不会有并发问题,对于共享变量,JMM规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

为控制工作内存和主内存的交互,定义了以下规范:

•所有的变量都存储在主内存(Main Memory)中。

•每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本。

•线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存。

•不同的线程之间无法直接访问对方本地内存中的变量。

具体实现上定义了八种操作:

1.lock:作用于主内存,把变量标识为线程独占状态。

2.unlock:作用于主内存,解除独占状态。

3.read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。

4.load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。

5.use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。

6.assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。

7.store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。

8.write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

这些操作都满足以下原则:

•不允许read和load、store和write操作之一单独出现。

•对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

2.2 Java中的并发关键字

Java基于以上规则提供了volatile、synchronized等关键字来保证线程安全,基本原理是从限制处理器优化和使用内存屏障两方面解决并发问题。如果是变量级别,使用volatile声明任何类型变量,同基本数据类型变量、引用类型变量一样具备原子性;如果应用场景需要一个更大范围的原子性保证,需要使用同步块技术。Java内存模型提供了lock和unlock操作来满足这种需求。虚拟机提供了字节码指令monitorenter和monitorexist来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块-synchronized关键字。

这两个字的作用:volatile仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大,在可伸缩性和执行性能上,volatile更有优势。

2.3 Java中的并发容器与工具类

2.3.1 CopyOnWriteArrayList

CopyOnWriteArrayList在操作元素时会加可重入锁,一次来保证写操作是线程安全的,但是每次添加删除元素就需要复制一份新数组,对空间有较大的浪费。

    public E get(int index) {
        return get(getArray(), index);
    }

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

2.3.2 Collections.synchronizedList(new ArrayList<>());

这种方式是在 List的操作外包加了一层synchronize同步控制。需要注意的是在遍历List是还得再手动做整体的同步控制。

    public void add(int index, E element) {
        // SynchronizedList 就是在 List的操作外包加了一层synchronize同步控制
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }

2.3.3 ConcurrentLinkedQueue

通过循环CAS操作非阻塞的给队列添加节点,

    public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p是尾节点,CAS 将p的next指向newNode.
                if (p.casNext(null, newNode)) {
                    if (p != t) 
                        //tail指向真正尾节点
                        casTail(t, newNode);
                    return true;
                }
            }
            else if (p == q)
                // 说明p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加节点,所以返回head节点
                p = (t != (t = tail)) ? t : head;
            else
                // 向后查找尾节点
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

三、线上案例

3.1 问题发现

在互联网医院医生端,医生打开问诊IM聊天页,需要加载几十个功能按钮。在2022年12月抗疫期间,QPS全天都很高,高峰时是平日的12倍,偶现报警提示按钮显示不全,问题出现概率大概在百万分之一。

3.2 排查问题的详细过程

医生问诊IM页面的加载属于业务黄金流程,上面的每一个按钮就是一个业务线的入口,所以处在核心逻辑的上的报警均使用自定义报警,该类报警不设置收敛,无论何种异常包括按钮个数异常就会立即报警。

1. 根据报警信息,开始排查,却发现以下问题:

(1)没有异常日志:顺着异常日志的logId排查,过程中竟然没有异常日志,按钮莫名其妙的变少了。

(2)不能复现:在预发环境,使用相同入参,接口正常返回,无法复现。

2. 代码分析,缩小异常范围:

医生问诊IM按钮处理分组进行:

    // 多个线程结果集合
    List<DoctorDiagImButtonInfoDTO> multiButtonList = new ArrayList<>();
    // 多线程并行处理
    Future<List<DoctorDiagImButtonInfoDTO>> multiButtonFuture = joyThreadPoolTaskExecutor.submit(() -> {
        List<DoctorDiagImButtonInfoDTO> multiButtonListTemp = new ArrayList<>();
        buttonTypes.forEach(buttonType -> {
            multiButtonListTemp.add(appButtonInfoMap.get(buttonType));
        });
        multiButtonList.addAll(multiButtonListTemp);
        return multiButtonListTemp;
    });

3. 增加日志线上观察

由于并发场景容易引发子线程失败的情况,对各子线程分支增加必要节点日志上线后观察:

(1)发生异常的请求处理过程中,所有子线程正常处理完成

(2)按钮缺少个数随机等于子线程中处理的按钮个数

(3)初步判断是ArrayList并发addAll操作异常

4. 模拟复现

使用ArrayList源码模拟复现问题:

(1)ArrayList源码分析:


     public boolean addAll(Collection<? extends E> c) {
         Object[] a = c.toArray();
         int numNew = a.length;
         ensureCapacityInternal(size + numNew); // Increments modCount
 
         //以当前size为起点,向数组中追加本次新增对象
         System.arraycopy(a, 0, elementData, size, numNew);
 
         //更新全局变量size的值,和上一步是非原子操作,引发并发问题的根源
         size += numNew;
         return numNew != 0;
     }
 
     private void ensureCapacityInternal(int minCapacity) {
         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
             minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
         }
 
         ensureExplicitCapacity(minCapacity);
     }
 
     private void ensureExplicitCapacity(int minCapacity) {
         modCount++;
 
         // overflow-conscious code
         if (minCapacity - elementData.length > 0)
             grow(minCapacity);
     }
 
     private void grow(int minCapacity) {
         // overflow-conscious code
         int oldCapacity = elementData.length;
         int newCapacity = oldCapacity + (oldCapacity >> 1);
         if (newCapacity - minCapacity < 0)
             newCapacity = minCapacity;
         if (newCapacity - MAX_ARRAY_SIZE > 0)
             newCapacity = hugeCapacity(minCapacity);
         // minCapacity is usually close to size, so this is a win:
         elementData = Arrays.copyOf(elementData, newCapacity);
     }
 

(2) 理论分析

在ArrayList的add操作中,变更size和增加数据操作,不是原子操作。

(3)问题复现

复制源码创建自定义类,为方便复现并发问题,增加停顿

     public boolean addAll(Collection<? extends E> c) {
         Object[] a = c.toArray();
         int numNew = a.length;
         //第1次停顿,获取当前size
         try {
             Thread.sleep(1000*timeout1);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         ensureCapacityInternal(size + numNew); // Increments modCount
 
         //第2次停顿,等待copy
         try {
             Thread.sleep(1000*timeout2);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.arraycopy(a, 0, elementData, size, numNew);
 
         //第3次停顿,等待size+=
         try {
             Thread.sleep(1000*timeout3);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         size += numNew;
         return numNew != 0;
     }

3.3 解决问题

使用线程安全工具 Collections.synchronizedList 创建 ArrayList :

    List<DoctorDiagImButtonInfoDTO> multiButtonList = Collections.synchronizedList(new ArrayList<>()); 

上线观察后正常。

3.4 总结反思

使用多线程处理问题已经变得很普遍,但是对于多线程共同操作的对象必须使用线程安全的类。

另外,还要搞清楚几个灵魂问题:

(1)JMM的灵魂:Happens-before 原则

(2)并发工具类的灵魂:volatile变量的读/写 和 CAS

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/505454.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MS8257N跨阻放大器pin对pin兼容OPA857

MS8257N 是一颗宽带、快速过载恢复时间、快速建立时间、跨阻增益可调、超低噪声的跨阻放大器&#xff0c;主要用于光电监测和各种高性能的光电系统。可pin对pin兼容OPA857。快速过载恢复特性和内部输入保护电路可以让信号从过载传输中快速恢复正常。两档可选跨阻增益保证了极高…

MySQL基础(十八)MySQL8其它新特性

1. MySQL8新特性概述 MySQL从5.7版本直接跳跃发布了8.0版本&#xff0c;可见这是一个令人兴奋的里程碑版本。MySQL 8版本在功能上做了显著的改进与增强&#xff0c;开发者对MySQL的源代码进行了重构&#xff0c;最突出的一点是MySQL Optimizer优化器进行了改进。不仅在速度上得…

【Redis】概述与安装

概述 Redis 是什么&#xff1f; Redis&#xff08;Remote Dictionary Server )&#xff0c;即远程字典服务&#xff1b; 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库&#xff0c;并提供多种语言的 API。 Redis 会周期性的把…

深入理解什么是 dp(dip)

文章目录 dp 的计算公式dp 计算的例子dp 的来历究竟什么是 dp dp 的计算公式 dp 是安卓 UI 开发中最常见的单位之一&#xff0c;它有一个别名 dip。dp 与 dip 是同一个意思。dp 指的是什么呢&#xff1f;官方的解释是 device independent pixels&#xff08;设备独立像素&#…

Golang 函数调用机制

在真实的内存里面是没有test和main栈区的&#xff0c;这里只是为了区分。这两个数据空间是独立的&#xff0c;但是同时都在栈区里面。 这里可以清楚的看到&#xff0c;虽然两个名字都叫n1&#xff0c;但是其实上都是独立的。就好比不同的教师里面坐着名字相同的人。 注意test…

软考A计划-重点考点-专题四(多媒体相关)

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

Mongodb 在工业场景下的数字解决方案

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

数字经济下的企业数字化转型

上周&#xff0c;由国家网信办、国家发改委、科技部、工信部、国务院国资委、福建省人民政府共同主办的第六届数字中国建设峰会在福州隆重开幕。 本届峰会以“加快数字中国建设&#xff0c;推进中国式现代化”为主题&#xff0c;集中展示数字中国建设最新成果和优秀实践案例&a…

uniapp实现微信小程序横屏适配问题demo效果(整理)

使用VMIN进行布局 先了解css3的两个属性vmax和vmin vmax 相对于视口的宽度或高度中较大的那个。其中最大的那个被均分为100单位的vmax vmin 相对于视口的宽度或高度中较小的那个。其中最小的那个被均分为100单位的vmin竖屏布局的时候&#xff0c;750rpx就是竖屏布局屏幕的宽度…

[前端]深浅拷贝

一、回顾变量类型 基础类型 boolean&#xff08;bool&#xff09; number string null undefined 引用类型 object ​ function ​ array 基本类型与引用类型的存储 基本类型一般存储在 栈 (栈小) 栈一旦确认 大小就固定 可能会造成溢出栈一般是先进后出用于存储…

数据库sql语句练习(二)

例题&#xff1a; ●检索出生年份比杜甫早的诗人 select name from poets where birth <( select birth from poets where name 杜甫’) 重点&#xff1a;ALL&#xff0c;SOME,ANY(对于any&#xff0c;有些系统用some) 使用all或者any时。必须同时使用比较运算符 注&…

Linux配置sftp用户访问指定目录

一、环境 开发环境&#xff1a;Win 10 -- 64位 服务环境&#xff1a;centos7 SSH&#xff1a;OpenSSH_7.4p1 二、新建sftp用户&#xff1a; 需要root权限执行-------------------------- 1.新建一个用户组 为了方便后面添加用户 groupadd sftp2.添加用户 useradd -g sf…

【Python】【进阶篇】27、Django url标签详解

目录 27、Django url标签详解1. url标签基本使用2. 给定参数的动态url 27、Django url标签详解 在 Django 的模板语言中除了我们前面章节介绍过的 if 标签和 for 标签之外&#xff0c;还有许多我们时常用到标签&#xff0c;比如 url 标签。 1. url标签基本使用 Django 的模板…

Xcode 使用 Instruments 无法找到代码中耗时挂起操作的解决

问题现象 Instruments 是一套非常有用的代码分析和调试利器,我们经常用它来查找 App 中的性能瓶颈。 不过,有时 Instruments 却无法捕获到系统明显挂起或长耗时的方法,这是怎么回事呢? 如上图所示,App 运行中系统(Severe Hang)被挂起长达 3 秒钟,但在 Instruments 竟…

tmux一个终端复用器(terminal multiplexer)

tmux 安装 使用包管理工具 # Ubuntu 或 Debian $ sudo apt-get install tmux# CentOS 或 Fedora $ sudo yum install tmux# Mac $ brew install tmuxsession 会话管理 1. 新建会话 第一个启动的 Tmux 窗口&#xff0c;编号是0&#xff0c;第二个窗口的编号是1&#xff0c;以…

[无线统计设计与国际标准]【chapter 1】[参考信号设计]【DMRS】

前言&#xff1a; 参考信号相关的所有内容都在38.211 中有介绍。 DMRS是用于信道估计&#xff0c;服务于UE信 道解调的。基站将DMRS穿插到各种信道的时频资源里面去&#xff0c;伴随着数据一起发送给UE&#xff1b; 对UE来说&#xff0c;DMRS是一个确…

Java【多线程基础6】定时器 + 模拟实现Timer

文章目录 前言一、定时器1, 什么是定时器2, 如何使用定时器 二、模拟实现定时器1, 初步实现2, 问题改善 总结 前言 &#x1f4d5;各位读者好, 我是小陈, 这是我的个人主页 &#x1f4d7;小陈还在持续努力学习编程, 努力通过博客输出所学知识 &#x1f4d8;如果本篇对你有帮助, …

【设计模式】责任链模式的介绍及其应用

责任链的介绍 责任链模式是一种对象的行为模式。在责任链模式里&#xff0c;很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递&#xff0c;直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求&a…

一款非常经典的蓝牙多媒体芯片​RDA5850

RDA5850是一款高度集成的蓝牙无线电收发器和基带处理器&#xff0c;基于低成本的32位XCPU RISC核心&#xff08;其实虽然手册上说是RISC核心&#xff0c;但我怎么感觉这款芯片有点MIPS的影子 &#xff09;具有多媒体能力。此芯片有着丰富的外设&#xff0c;而大多数的功能都是…

找出3个数中的最大值与最小值

设计完整的程序实现以下功能&#xff1a;从键盘上输入3个整数&#xff0c;通过指针运算&#xff0c;找出3个数中的最大值与最小值和它们的地址&#xff0c;并实现最大值与最小值的交换&#xff08;要求用指针作为函数参数处理&#xff09;。 #include <stdio.h>void swap…