java并发编程之 并发问题及解决方法

article/2025/11/7 13:37:51

一、并发问题的根源

首先,我们要知道并发要解决的是什么问题?并发要解决的是单进程情况下硬件资源无法充分利用的问题。而造成这一问题的主要原因是CPU-内存-磁盘三者之间速度差异实在太大。如果将CPU的速度比作火箭的速度,那么内存的速度就像火车,而最惨的磁盘,基本上就相当于人双腿走路。

这样造成的一个问题,就是CPU快速执行完它的任务的时候,很长时间都会在等待磁盘或是内存的读写。

计算机的发展有一部分就是如何重复利用资源,解决硬件资源之间效率的不平衡,而后就有了多进程,多线程的发展。并且演化出了各种为多进程(线程)服务的东西:

  • CPU增加缓存机制,平衡与内存的速度差异
  • 增加了多个概念,CPU时间片,程序计数器,线程切换等,用以更好得服务并发场景
  • 编译器的指令优化,希望在内部充分利用硬件资源

但是这样一来,也会带来新的并发问题,归结起来主要有三个。

  • 由于缓存导致的可见性问题
  • 线程切换带来的原子性问题
  • 编译器优化带来的有序性问题

我们分别介绍这几个:

1、缓存导致的可见性

CPU为了平衡与内存之间的性能差异,引入了CPU缓存,这样CPU执行指令修改数据的时候就可以批量直接读写CPU缓存的内存,一个阶段后再将数据写回到内存。

但由于现在多核CPU技术的发展,各个线程可能运行在不同CPU核上面,每个CPU核各有各自的CPU缓存。前面说到对变量的修改通常都会先写入CPU缓存,再写回内存。这就会出现这样一种情况,线程1修改了变量A,但此时修改后的变量A只存储在CPU缓存中。这时候线程B去内存中读取变量A,依旧只读取到旧的值,这就是可见性问题。

2、线程切换带来的原子性

为了更充分得利用CPU,引入了CPU时间片时间片的概念。进程或线程通过争用CPU时间片,让CPU可以更加充分得利用。

比如在进行读写磁盘等耗时高的任务时,就可以将宝贵的CPU资源让出来让其他线程去获取CPU并执行任务。

但这样的切换也会导致问题,那就是会破坏线程某些任务的原子性。比如java中简单的一条语句count += 1。

映射到CPU指令有三条,读取count变量指令,变量加1指令,变量写回指令。虽然在高级语言(java)看来它就是一条指令,但实际上确是三条CPU指令,并且这三条指令的原子性无法保证。也就是说,可能在执行到任意一条指令的时候被打断,CPU被其他线程抢占了。而这个期间变量值可能会被修改,这里就会引发数据不一致的情况了。所以高并发场景下,很多时候都会通过锁实现原子性。而这个问题也是很多并发问题的源头。

3、编译器优化带来的有序性

因为现在程序员编写的都是高级语言,编译器需要将用户的代码转成CPU可以执行的指令。

同时,由于计算机领域的不断发展,编译器也越来越智能,它会自动对程序员编写的代码进行优化,而优化中就有可能出现实际执行代码顺序和编写的代码顺序不一样的情况。

而这种破坏程序有序性的行为,在有些时候会出现一些非常微妙且难以察觉的并发编程bug。

举个简单的例子,我们常见的单例模式是这样的:

public class Singleton {private Singleton() {}private static Singleton sInstance;public static Singleton getInstance() {if (sInstance == null) {	//第一次验证是否为nullsynchronized (Singleton.class) {   //加锁if (sInstance == null) {	  //第二次验证是否为nullsInstance = new Singleton();  //创建对象}}}return sInstance;}}

即通过两段判断加锁来保证单例的成功生成,但在极小的概率下,可能会出现异常情况。原因就出现在sInstance = new Singleton();这一行代码上。这行代码,我们理解的执行顺序应该是这样:

  1. 为Singleton象分配一个内存空间。
  2. 在分配的内存空间实例化对象。
  3. 把Instance 引用地址指向内存空间。

但在实际编译的过程中,编译器有可能会帮我们进行优化,优化完它的顺序可能变成如下:

  1. 为Singleton对象分配一个内存空间。
  2. 把instance 引用地址指向内存空间。
  3. 在分配的内存空间实例化对象。

按照优化完的顺序,当并发访问的时候,可能会出现这样的情况

  1. A线程进入方法进行第1次instance == null判断。
  2. 此时A线程发现instance 为null 所以对Singleton.class加锁。
  3. 然后A线程进入方法进行第2次instance == null判断。
  4. 然后A线程发现instance 为null,开始进行对象实例化。
  5. 为对象分配一个内存空间。
    6.把Instance 引用地址指向内存空间(而就在这个指令完成后,线程B进入了方法)。
  6. B线程首先进入方法进行第1次instance == null判断。
  7. B线程此时发现instance 不为null ,所以它会直接返回instance (而此时返回的instance 是A线程还没有初始化完成的对象)

最终线程B拿到的instance 是一个没有实例化对象的空内存地址,所以导致instance使用的过程中造成程序错误。解决办法很简单,可以给sInstance对象加上一个关键字,volatile,这样编译器就不会乱优化,有关volatile的具体内容后续再细说。

二、主要解决办法

1、同步与互斥的概念

通过上面的介绍,其实可以归纳无论是CPU缓存,线程切换还是编译器优化乱序,出现问题的核心都是因为多个线程要并发读写某个变量或并发执行某段代码。那么我们可以控制,一次只让一个线程执行变量读写就可以了,这就是互斥

而在某些时候,互斥还不够,还需要一定的条件。比如一个生产者一个消费者并发,生产者向队列存东西,消费者向队列拿东西。那么生产者写的时候要保证存的时候队列不是满的,消费者要保证拿的时候队列非空。这种线程与线程间需要通信协作的情况,称为同步同步可以说是更复杂的互斥

2、解决方法

既然知道了并发编程的根源以及同步和互斥,那我们来看看有哪些解决的思路。其实一共也就三种:

  • 避免共享
  • Immutability(不变性)
  • 管程及其他工具

下面我们分别说说这三种方案的优缺点

(1)避免共享

我们先来说说避免共享,其实避免共享说是线程本地存储技术,在java中指的一般就是Threadlocal。ThreadLocal会为每个线程提供一个本地副本,每个线程都只会修改自己的ThreadLocal变量。这样一来就不会出现共享变量,也就不会出现冲突了。

其实现原理是在ThreadLocal内部维护一个ThreadLocalMap,每次有线程要获取对应变量的时候,先获取当前线程,然后根据不同线程取不同的值,典型的以空间换时间。

所以ThreadLocal还是比较适用于需要共享资源,且资源占用空间不大的情况。比如一些连接的session啊等等。但是这种模式应用场景也较为有限,比如需要同步情况就难以胜任。

(2)Immutability(不变性)

Immutability在函数式中用得比较多,函数式编程的一个主要目的是要写出无副作用的代码,而无副作用的一个主要特点就是变量都是Immutability即不可变的,即创建对象后不会再修改对象,比如scala默认的变量和数据结构都是不可变的。而在java中,不变性变量即通过final修饰的变量,如String,Long,Double等类型都是Immutability的,它们的内部实现都是基于final关键字的。

那这又和并发编程有什么关系呢?其实啊,并发问题很大部分原因就是因为线程切换破坏了原子性,这又导致线程随意对变量的读写破坏了数据的一致性。而不变性就不必担心这个问题,因为变量都是不变,不可写只能读的。在这种编程模式下,你要修改一个变量,那么只能新生成一个。这样做的好处很明显,但坏处也是显而易见,那就是引入了额外的编程复杂度,丧失了代码的可读性和易用性。

不可变的类型:

  • final 关键字修饰的基本数据类型
  • String
  • 枚举类型enum 
  • Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。

(3)管程及其他工具

其实最早的操作系统中,解决并发问题用的是信号量,信号量通过两个原子操作wait(S),和signal(S)(俗称P,V操作)来实现访问资源互斥和同步。比如下面这个小例子:

//整型信号量定义
int S;//P操作
wait(S){while(S<=0);S--;
}//V操作
signal(S){S++;
}

虽然信号量方便有效,但信号量要对每个共享资源都实现对应的P和V操作,这使得并发编程中可能要出现大量的P,V操作,并且这部分内容难以抽象出来。

为了更好地实现同步互斥,于是就产生了管程(即Monitor,也有翻译为监视器),值得一提的是,管程也有几种模型,分别是:Hasen模型,Hoare模型和MESA模型。其中MESA模型应用最广泛,java也是参考自MESA模型。这里简单介绍下管程的理论知识,这部分内容参考自进程同步机制-----为进程并发执行保驾护航,希望了解更多管程理论知识的童鞋可以看看。

我们来通过一个经典的生产-消费队列来解释,如下图

我们先解释下图中右半部分的内容,右上角有一个等待调用的线程队列,管程中每次只能有一个线程在执行任务,所以多个任务需要等待。然后是各个名词的意思,生产-消费需要往队列写入和取出东西,这里的队列就是共享变量对共享资源进行操作称之为过程(入队和出队两个过程)。而向队列写入和取出是有条件的,写入的时候队列必须是非满的,取出的时候队列必须是非空的,这两个条件被称为条件变量

然后再来看看左半部分的内容,假设线程T1读取共享变量(即队列),此时发现队列为空(条件变量之一),那么T1此时需要等待,去哪里等呢?去条件变量队列不能为空对应的队列中去等待。此时另一个线程T2向共享变量队列写数据,通过了条件变量队列不能满,那么写完后就会通知线程T1。但因为管程的限制,管程中只能有一个线程在执行,所以T1线程不能立即执行,它会回到右上角的线程等待队列等待(不同的管程模型在这里是有分歧的,比如Hasen模型是立即中断T2线程让队列中下一个线程执行)。

解释完这个图,管程的概念也就呼之欲出了,

hansen对管程的定义如下:一个管程定义了一个数据结构和能力为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。

本质上,管程是对共享资源以及对共享资源的操作抽象成变量和方法,要操作共享变量仅能通过管程提供的方法(比如上面的入队和出队)间接访问。所以你会发现管程其实和面向对象的理念是十分相近的,在java中,主要提供了低层次了synchronized关键字和wait(),notify()等方法。同时还提供了高层次的ReenTrantLock和Condition来实现管程模型。

3、线程安全的实现方法

Java提供了以下方法来保证线程安全,根据是否需要同步,分为两种类型。

同步方案: 如果线程间需要协作,即一个线程的执行需要依赖其他线程执行的结果,那么就说线程之间是需要同步的。这种情况下一般用添加互斥锁的方式解决,主要有以下两种方法:

  1. synchronized:synchronized 关键字的作用和原理
  2. Lock:使用java.util.concurrent.Lock接口下的实现类,如ReentrantLock
  3. volatile:volatile 关键字作用及原理

无同步方案: 但是互斥锁会阻塞其他线程的执行,效率较低。所以如果线程间不需要协作,即一个线程的执行不依赖于其他线程的执行结果,不涉及共享数据,这种情况下就不需要使用互斥锁的方案,Java提供了以下无同步方案来处理这种场景下的问题:

  1. ThreadLocal:当多个线程共同操作一个对象,但是互不影响,不需要同步时,可以不用加互斥锁,使用ThreadLocal保证每个线程都有共享对象的一个备份,线程间数据互相隔离,互不影响,这样就避免了冲突。详见ThreadLocal简介


http://chatgpt.dhexx.cn/article/HKyjOmCM.shtml

相关文章

『图解Java并发编程系列』10张图告诉你Java并发多线程那些破事

目录 线程安全问题 活跃性问题 性能问题 有态度的总结 头发很多的程序员&#xff1a;『师父&#xff0c;这个批量处理接口太慢了&#xff0c;有什么办法可以优化&#xff1f;』架构师&#xff1a;『试试使用多线程优化』第二天头发很多的程序员&#xff1a;『师父&#xff…

Java基础-并发篇

3.1. JAVA 并发知识库 3.2. JAVA 线程实现/创建方式 3.2.1. 继承 Thread 类 ​ Thread 类本质上是实现了 Runnable 接口的一个实例&#xff0c;代表一个线程的实例。启动线程的唯一方 法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法&#xff0c;它将…

java并发总结

一、并发基础 ㅤ 1、进程与线程 ㅤ 进程 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至 CPU&#xff0c;数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 I…

pytorch 图片分类,python 图片分类,resnet18 图片分类

pytorch 图片分类&#xff0c;python 图片分类&#xff0c;resnet18 图片分类&#xff0c;深度学习 图片分类 pytorch版本&#xff1a;1.5.0cu101 全部源码&#xff0c;可以直接运行。 下载地址&#xff1a;https://download.csdn.net/download/TangLingBo/12598435 网络是…

深度学习图片分类实战学习

开始记录学习深度学习的点点滴滴 深度学习图片分类实战学习 前言一、深度学习二、使用步骤1. 自建网络模型2. 进行深度学习的学习迁移 注意事项 前言 随着人工智能的不断发展&#xff0c;这门技术也越来越重要&#xff0c;很多人都开启了学习人工智能&#xff0c;本人开始记录…

关于图片的多标签分类(1)

最近还在处理人脸附件&#xff08;眼镜&#xff0c;刘海&#xff0c;口罩&#xff0c;帽子&#xff09;的multi-label分类。给自己普及一下常识性问题&#xff1a; 1&#xff09;什么是multi-label分类&#xff1f; multi-label分类&#xff0c;常见一张图片中可以存在多个目…

svm实现图片分类(python)

目录 前言 knn vs. svm svm & linear classifier bias trick loss function regularization optimization 代码主体 导入数据及预处理 svm计算loss_function和梯度 验证梯度公式是否正确 比较运行时间 svm训练及预测&#xff0c;结果可视化 通过corss-validat…

图片分类-python

目的&#xff1a;做一个简易的图片分类。 使用到的算法&#xff1a;hog、surfsvm 图片集&#xff1a;cifar-10、cifar-100、stl-10、自制图片集 分类完整代码链接 使用说明&#xff1a; 1.cifar-10、cifar-100和stl-10直接解压 2.自制图片集文件夹结构&#xff1a; ├…

CNN图片分类

最近在阅读一些AI项目&#xff0c;写入markdown&#xff0c;持续更新&#xff0c;算是之后也能回想起做法 项目 https://github.com/calssion/Fun_AI image classify(图片分类) CNN classify dogs and cats(猫狗二分类) Tutorial(教程):https://developers.google.com/mach…

深度学习之图像分类

第一篇CSDN文章&#xff0c;写的不好&#xff0c;还请各位大佬指正。万事开头难&#xff0c;千里之行始于足下&#xff01; 1.什么是图像分类 图像分类&#xff0c;核心是从给定的分类集合中给图像分配一个标签的任务。实际上&#xff0c;这意味着我们的任务是分析一个输入图…

关于图像分类

https://www.zhihu.com/question/57075015/answer/194397802https://www.zhihu.com/question/57075015/answer/194397802 先定义一下图像分类&#xff0c;一般而言&#xff0c;图像分类分为通用类别分类以及细粒度图像分类 那什么是通用类别以及细粒度类别呢&#xff1f;这里…

(一)图像分类任务介绍 Image Classification

目录 一、什么是图像分类任务&#xff1f;它有哪些应用场景&#xff1f; 二、图像分类任务的难点&#xff1f; 三、基于规则的方法是否可行&#xff1f; 四、什么是数据驱动的图像分类范式&#xff1f; 数据集构建 分类器设计与学习 分类器决策 五、常用的分类任务评价指…

图像分类的数据集

图像分类的数据集 1. MNIST2. Fashion-MNIST3.CIFAR-10和CIFAR-1004. Caltech 1015. ImageNet5.1 ImageNet是什么&#xff1f;5.2 ILSVRC 6. 各个数据集上的最新进展其他参考资料 1. MNIST MNIST数据集的一个样例 一般机器学习框架都使用MNIST作为入门&#xff0c;就像"He…

机器学习——图像分类

1 图像分类的概念 1.1 什么是图像分类&#xff1f; 图像分类&#xff0c;根据图像信息中所反映出来的不同特征&#xff0c;把不同类别的目标区分开来的图像处理方法 1.2 图像分类的难度 ●任何拍摄情 况的改变都将提升分类的难度 1.3 CNN如何进行图像分类 ●数据驱动型方法通…

图像分类算法

图像分类 参考链接1.前言2.K近邻与KMeans算法比较KNN原理和实现过程(1) 计算已知类别数据集中的点与当前点之间的距离&#xff1a;(2) 按照距离递增次序排序(3) 选取与当前点距离最小的k个点(4) 确定前k个点所在类别的出现频率(5) 返回前k个点出现频率最高的类别作为当前点的预…

图像分类方法总结

1. 图像分类问题描述 图像分类问题是计算机视觉领域的基础问题&#xff0c;它的目的是根据图像的语义信息将不同类别图像区分开来&#xff0c;实现最小的分类误差。具体任务要求是从给定的分类集合中给图像分配一个标签的任务。总体来说&#xff0c;对于单标签的图像分类问题&…

9.图片分类数据集

1. 图像分类数据集 MNIST数据集 [LeCun et al., 1998] 是图像分类中广泛使用的数据集之一&#xff0c;但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集。 %matplotlib inline import torch import torchvision from torch.utils import data from t…

CNN实现花卉图片分类识别

CNN实现花卉图片分 前言 本文为一个利用卷积神经网络实现花卉分类的项目&#xff0c;因此不会过多介绍卷积神经网络的基本知识。此项目建立在了解卷积神经网络进行图像分类的原理上进行的。 项目简介 本项目为一个图像识别项目&#xff0c;基于tensorflow&#xff0c;利用C…

常用图像分类网络

想对图像分类网络写个简要的概括&#xff0c;如有介绍不当之处&#xff0c;还望指出。 一、VGG网络 更新于2018年10月20日 参考博客&#xff1a;深度学习经典卷积神经网络之VGGNet 论文地址&#xff1a;VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITIO…

干货——图像分类(上)

这是译自斯坦福CS231n课程笔记image classification notes&#xff0c;由课程教师Andrej Karpathy授权进行翻译。本篇教程由杜客翻译完成。非常感谢那些无偿奉献的大师&#xff0c;在此代表所有爱好学习者向您们致敬&#xff0c;谢谢&#xff01; 这是斯坦福大学的课程&#xf…