Java并发编程第3讲——线程安全

news2024/11/17 17:36:05

目录

1 线程安全

1.1 谈谈你对线程安全的理解

1.2 Java中操作共享数据分类

1.2.1 不可变(Immutable)

1.2.2 绝对线程安全(Thread-safe)

1.2.3 相对线程安全(Thread-compatible)

1.2.4 线程兼容(Thread-adaptive)

1.2.5 线程对立(Thread-hostile)

二、线程安全问题产生的原因及解决方式

2.1 原因

2.2 解决方案

三、临界区和竞态条件

3.1 临界区

3.1.1 临界区的定义

3.1.2 临界区和临界资源

3.2 竞态条件

3.2.1 竞态条件的定义

3.2.2 竞态条件的分类

3.3.3 竞态条件的解决方案

四、总结


1 线程安全

1.1 谈谈你对线程安全的理解

定义:线程安全是指某个函数在并发环境中被调用时,能够正确地处理多线程之间的共享变量,使程序功能正确完成。

ps:所谓的正确完成,其实就是要满足原子性、有序性和可见性

简单来说,就是多个线程同时访问共享变量时,得到的结果和我们预期的结果一样,就是线程安全。

1.2 Java中操作共享数据分类

ps:此小节参考了《深入理解Java虚拟机(第三版)》一书,第13章第13.2.1小节(第467页)。

线程安全是以多线程之间存在共享数据访问为前提的,为了更好的理解线程安全,我们不应该把它简单的理解为是一个非真即假的二元操作,而是按照“安全程度”由强到弱进行分类。在Java语言中,可以将共享数据的操作分为5类,下面就详细介绍一下。

1.2.1 不可变(Immutable)

定义:指一旦被创建就无法修改其状态的对象。

不可变(Immutable)的对象一定是线程安全的,不需要再进行任何线程安全保障措施。

这里不得不提一下Java中的关键字——final,final可以保证内存的可见性,也就是只要一个不可变的对象被正确的构建出来,其外部的可见状态都不会改变,永远都不会看到它在多个线程之间处于不一致的状态。"不可变"带来的安全性是最直接,最纯粹的。

ps:编辑器为final写(初始赋值)操作之后插入写屏障,在读操作之前插入读屏障。这个由编译器插入的内存屏障,要求处理机在“某些位置”禁用cpu指令重排进行优化。还有volatile、synchronized等关键字也是如此。

我们最熟悉的不可变Java API可能就是String类了,它是一个典型的不可变对象,substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。

另外还有很多类似的Java API也是不可变的对象:

  • 枚举类型的对象
  • java.lang.Number的部分子类:Integer、Long、Float、BigDecimal等
  • Java 8中的日期和时间API:LocalDate、LocalTime和LocalDateTime等

1.2.2 绝对线程安全(Thread-safe)

定义:指一个类要满足“不管运行环境如何,调用者都不需要额外的同步措施”。

可以看到,绝对线程安全的定义是很严格的,因此在Java中,并没有绝对线程安全的类,这是因为线程安全是一个相对的概念,它取决于代码的具体实现和运行环境。

1.2.3 相对线程安全(Thread-compatible)

定义:指一个类的方法在单独调用时是线程安全的,但对于多线程环境,调用序列需要额外的同步控制来保证线程安全。

相对线程安全就是我们通常意义上所讲的线程安全,在Java中,大部分线程安全的类都属于这种类型,例如我们熟知的java.util.concurrent包下的大部分类都是相对线程安全的。

尽管Vector类的所有方法都用了synchronized关键字修饰,但在一些情况下还是需要额外的同步手段来保证正确性。请看下面代码:

public static void main(String[] args) {
        Vector<Integer> vector=new Vector<>();
        for (int i = 0; i < 1000; i++) {
            System.out.println("循环第"+i+"次");
            new Thread(()->{
                if(vector.size()==0){
                    vector.add(1);
                }else {
                    //添加:最后一个元素加1的值
                    vector.add(vector.lastElement()+1);
                }
            }).start();
        }

        try {
            System.out.println("======休眠5秒======");
            //休眠5秒,让主线程最后执行完毕
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("vector中最后一个元素的值为:"+vector.lastElement());
}

输出结果如下:

我们的预期值应该是1000,但最后的值是897。所以尽管Vector类的add()、lastElement()方法都是同步的,但是在多线程的环境中,如果对这段代码不做额外的同步措施,那么它也是线程不安全的。这段代码类似于i++操作,出现了“漏加”的情况。

如果要保证正确性,那么可以额外的增加同步措施,代码如下:

public static void main(String[] args) {
        Vector<Integer> vector=new Vector<>();

        for (int i = 0; i < 1000; i++) {
            System.out.println("循环第"+i+"次");
            new Thread(()->{
                synchronized (vector){
                    if(vector.size()==0){
                        vector.add(1);
                    }else {
                        //添加:最后一个元素加1的值
                        vector.add(vector.lastElement()+1);
                    }
                }
            }).start();
        }
        try {
            System.out.println("======休眠5秒======");
            //休眠5秒,让主线程最后执行完毕
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("vector中最后一个元素的值为:"+vector.lastElement());
    }

这里就不展示输出结果了,感兴趣的可以去实操一下😁。

1.2.4 线程兼容(Thread-adaptive)

定义:指对象本身并不是线程安全的,但可以通过额外的同步处理,以确保线程安全。

平常我们说一个类不是线程安全的,通常指的就是这种情况。Java API中大部分的类都是线程兼容的,比如ArrayList和HashMap。

1.2.5 线程对立(Thread-hostile)

定义:指不管调用端是否采取了同步措施,都无法在多线程中并发使用代码。

一个线程对立的例子是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,那就肯定要产生死锁了。

ps:正因为这个原因,这两个方法都被声明废弃了。

二、线程安全问题产生的原因及解决方式

读到这,相信你一定对线程安全有了一定了解,那么下面介绍一下导致线程安全问题原因及常见的解决方案。

2.1 原因

主要原因有以下5点:

  • 竞态条件:当多个线程访问共享数据并且尝试同时更改数据时,由于执行顺序的不确定性,可能会导致数据的不一致问题。比如,两个线程同时读取共享数据并进行修改,结果可能是其中一个线程的修改被覆盖或丢失。
  • 不正确的同步:多个线程在访问共享数据时,没有进行正确的同步操作(如锁、信号量等),没有对临界区代码进行保护,这样可能导致多个线程同时修改数据,产生不可预料的结果。
  • 不可见性:多个线程对同一个共享资源进行读写操作时,由于操作的顺序或时机不一致,可能会导致数据的不一致性。例如,一个线程正在修改数据,而另一个线程同时读取了这个数据,就可能读到一个中间状态的数据。
  • 非原子性:某些操作需要多个步骤才能完成,如果在这个过程中发生线程切换,可能会导致数据不一致。比如,一个线程对一个整数进行递增操作,需要先读取当前值,然后加一,最后写回,如果在这个过程中发生线程切换,可能导致多个线程同时读取、修改、写入相同的值,结果会出现错误。
  • 数据依赖关系破坏:某些操作需要依赖于数据的特定状态,如果多个线程同时修改了这个状态,可能会导致操作的失败或数据的不一致。比如,一个线程在进行某个条件操作之前,需要数据满足一定的要求,但是其他线程可能会同时修改这个数据,导致条件无法满足。

2.2 解决方案

以下是几个常见的解决方案(思想):

  • 单线程:既然多线程用问题,干脆就使用单线程执行,从根本上杜绝线程安全问题。比如Redis,就是这种思想,可以参考作者的这篇文章😁。
  • 互斥锁:如果一定要用多线程,那么最常见的就是加锁。比如数据库的乐观锁、悲观锁,synchronized、ReentrantLock这种单机锁,还有Redis的分布式锁。
  • 读写分离:除了加锁之外,还有一种做法,就是读写分离。比如CopyOnWriteArrayList当有新元素add的时候,先从原有的数组中copy一份出来,然后在新的数组做写操作,写完之后再将原来的数组引用指向新数组。
  • 原子操作:原子操作是不可中断的操作,要么全部执行成功,要么全部失败。在多线程环境中,可以使用原子操作来实现对公共资源的安全访问。比如AtomicInteger。
  • 不可变模式:上面也提到过,不可变的对象一定是线程安全的。
  • 数据不共享:像前面说的,数据共享是线程安全的前提。如果没有数据共享,那么也就没线程安全问题了,那么就可以用ThreadLocal类来解决。(通过为每一个线程创建一份共享变量的副本,来保证各个线程之间的变量的访问和修改互不影响)

三、临界区和竞态条件

上面提到了两个关键词“临界区”和“竞态条件”,下面就简单介绍一下这两个关键词的含义。

3.1 临界区

3.1.1 临界区的定义

定义:指在多线程编程中,访问共享资源的那部分代码区域

ps:如果多个线程同时访问临界区代码(共享资源),可能会导致数据不确定的结果,因此需要同步机制来限制对临界区的访问,以避免竞态条件和数据不一致问题。

3.1.2 临界区和临界资源

举个例子:假设有A、B两个线程,他们都往一个数组中的index位置存入一个数据并且执行index+1。 A线程存入“hello”,然后index++;B线程存入“world”,然后index++。

public class TestString {
    private static int index =0;
    public static void main(String[] args) throws InterruptedException {
        String[] strings=new String[5];
        //存入hello,A线程
        new Thread(()->{
            strings[index]="hello";
            index++;
        }).start();
        
        //存入world,B线程
        new Thread(()->{
            strings[index]="world";
            index++;
        }).start();

        //休眠两秒,让主线程最后执行
        System.out.println("=====休眠2秒=====");
        Thread.sleep(2000);
        System.out.println(Arrays.toString(strings));
    }
}

多次执行代码,发现得到的结果并不一致,有可能出现[hello,world,null,null,null],也有可能出现[world,null,null,null,null];以第二个结果为例,当线程A存入hello之后,CPU马上就被线程B所夺,B存入了world覆盖了A存的hello,之后才执行了各自的index++。

线程在运行过程中,会与同一进程内的其他线程共享资源,我们把同时只允许一个线程使用的资源成为临界资源,临界资源的访问可以分为以下四个部分:

  1. 进入区:为了进入临界区使用临界资源,在进入区要检查是否可以进入临界区;如果可以进入临界区,通常设置响应的“正在访问临界区”标志,以阻止其他线程同时进入临界区。
  2. 临界区:线程用于访问临界资源的代码。
  3. 退出区:临界区后用于将“正在访问临界区”标志清除部分
  4. 剩余区:上述3个部分以外的其他部分。

简单来说,临界资源是一种系统资源,需要不同的线程互斥访问,例如前文代码中的数组;临界区则是每个线程中访问临界资源的一段代码。是属于对象线程的,前面代码中存hello和存world以及index++就可以看成两个临界区。

3.2 竞态条件

3.2.1 竞态条件的定义

定义:指多线程环境中,多个线程对共享资源进行读写操作的顺序不确定,从而导致结果的准确性和一致性无法确定的一种情况。

参考上述A、B线程向数组中分别存hello和world的例子,没有用同步机制来保护临界区,每次的运行结果会出现不一致的情况,因为两个线程同时进行了读写操作,并且执行顺序也不确定,于是就发生了竞态条件。

3.2.2 竞态条件的分类

  • 写-写:多个线程同时进行写操作,可能导致数据的丢失或混乱。

  • 读-写:一个线程在读取共享资源的同时,其他线程进行写操作,可能导致读取到过期或无效的数据。

  • 写-读:一个线程在写操作的同时,其他线程进行读取操作,可能导致读取到中间状态的数据。

3.3.3 竞态条件的解决方案

说到底还是线程安全问题,参考2.2节介绍的解决方案。

四、总结

本篇文章从对线程安全的理解开始,讲述了Java中操作共享数据的5大分类,并用concurrent包下的Vector类举例解释了相对线程安全;接着详细介绍了线程安全问题产生的5大原因及6种常见解决方案;最后通过一个String[]数组存值的代码示例,介绍了临界区、临界资源以及竞态条件。

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。 

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

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

相关文章

外观模式——提供统一入口

1、简介 1.1、概述 在软件开发中&#xff0c;有时候为了完成一项较为复杂的功能&#xff0c;一个类需要和多个其他业务类交互&#xff0c;而这些需要交互的业务类经常会作为一个完整的整体出现&#xff0c;由于涉及的类比较多&#xff0c;导致使用时代码较为复杂。此时&#…

leetcode剑指offer75题

1 替换空格 var replaceSpace function(s) {const str s.split( );return str.join(%20) };2 左旋转字符串 var reverseLeftWords function(s, n) {const s1 s.slice(n)const s2 s.slice(0,n)return s1s2 };3 表示数值的字符串 //\d 匹配整数 1次或多次 //(\.\d*)? 满足小…

【腾讯云 Cloud studio 实战训练营】真正做到让你的开发成本只在编码

文章目录 写在前面CODINGCloud studio工具在线编码运行项目代码上传Cloud Studio 开发贪吃蛇写在最后 写在前面 期待已久的体验活动终于来了&#xff0c;Clound Studio用了才知道有多爽&#xff0c;Cloud Studio 是基于浏览器的集成式开发环境 (IDE)&#xff0c;为开发者提供了…

第二课:数据类型与变量

一. 数据类型 整型 byte short int long 小数 float double 字符 char 布尔 boolean 1.不论在16位&#xff0c;32位还是64位系统&#xff0c;int都占用4个字节&#xff0c;long都占用8个字节 &#x1f446;可移植性&#xff0c;可以跨平台运行&#xf…

【黑马头条之redis实现延迟任务】

本笔记内容为黑马头条项目的延迟任务精准发布文章部分 目录 一、实现思路 二、延迟任务服务实现 1、搭建heima-leadnews-schedule模块 2、数据库准备 3、安装redis 4、项目集成redis 5、添加任务 6、取消任务 7、消费任务 8、未来数据定时刷新 1.reids key值匹配 …

“智能算式批改系统”开发与部署优化

“智能算式批改系统”开发与部署优化 摘要 本次大作业搭建并实现了“智能算式批改系统”的开发与部署优化。“智能算式批改系统”是一款集yolo目标检测、paddleocr识别和四则运算判别算法的智能批改系统。该系统能够对上传包含四则运算题的页面进行批改&#xff0c;包括识别出…

【Docker】Docker安装Kibana服务

文章目录 1. 什么是Kibana2. Docker安装Kibana2.1. 前提2.2. 安装Kibana Docker安装MySQL、Redis、RabbitMQ、Elasticsearch、Nacos等常见服务全套&#xff08;质量有保证&#xff0c;内容详情&#xff09; 1. 什么是Kibana Kibana 是一款适用于Elasticsearch的数据可视化和管…

PyTorch深度学习实战(7)——批大小对神经网络训练的影响

PyTorch深度学习实战&#xff08;7&#xff09;——批大小对神经网络性能的影响 0. 前言1. 批处理概念2. 批处理的优势3. 批大小对神经网络性能的影响3.1 批大小为 323.2 批大小为 30,000 小结系列链接 0. 前言 在神经网络中&#xff0c;批( batch )是指一次输入网络进行训练或…

微服务体系<2> ribbon

1. 什么是负载均衡 比如说像这样 一个请求打在了nginx上 基于nginx进行负载分流 这就是负载均衡但是负载均衡分 服务端负载均衡和客户端负载均衡 客户端负载均衡 我user 从注册中心拉取服务 拉取order列表&#xff0c;然后发起getOne()调用 这就是客户端负载均衡 特点就是我…

【echarts】用js与echarts数据图表化,折线图、折线图堆叠、柱状图、折柱混合、环形图

echarts 是一个基于 JavaScript 的开源可视化库&#xff0c;用于构建交互式和自定义的图表&#xff0c;使数据更加直观和易于理解&#xff0c;由百度开发并于2018年捐赠给 Apache 软件基金会&#xff0c;后来改名为Apache ECharts 类似的还有Chart.js Chart.js地址&#xff1…

从此告别涂硅脂 利民推出新款CPU固态导热硅脂片:一片26.9元

利民(Thermalright)近日推出了新款Heilos CPU固态导热硅脂片&#xff0c;其中Intel版为26.9元&#xff0c;AMD版售价29.9元。 以往向CPU上涂硅脂&#xff0c;需要先挤一粒绿豆大小的硅脂&#xff0c;然后用塑料片涂匀&#xff0c;操作和清理对新手都极不友好。 该固态导热硅脂片…

string【2】模拟实现string类

string模拟实现 引言&#xff08;实现概述&#xff09;string类方法实现默认成员函数构造函数拷贝构造赋值运算符重载析构函数 迭代器beginend 容量size、capacity、emptyreserveresize 访问元素operator[] 修改insert插入字符插入字符串 appendpush_backoperatoreraseclearswa…

Python web实战 | 使用 Flask 实现 Web Socket 聊天室

概要 今天我们学习如何使用 Python 实现 Web Socket&#xff0c;并实现一个实时聊天室的功能。本文的技术栈包括 Python、Flask、Socket.IO 和 HTML/CSS/JavaScript。 什么是 Web Socket&#xff1f; Web Socket 是一种在单个 TCP 连接上进行全双工通信的协议。它是 HTML5 中的…

SAMBA 文件分享相关 笔记

目标说明 在Linux 安装Samba&#xff0c;然后在Windows端映射为网络硬盘 流程 Linux 端命令 apt install samba -y 默认情况下软件会询问是否迁移系统网络设置以搭建协议&#xff0c;选择迁移即可修改配置文件 vim /etc/samba/smb.conf Samba 的配置文件中会带一个名为 prin…

【Mybatis】Mybatis架构简介

文章目录 1.整体架构图2. 基础支撑层2.1 类型转换模块2.2 日志模块2.3 反射工具模块2.4 Binding 模块2.5 数据源模块2.6缓存模块2.7 解析器模块2.8 事务管理模块 3. 核心处理层3.1 配置解析3.2 SQL 解析与 scripting 模块3.3 SQL 执行3.4 插件 4. 接口层 1.整体架构图 MyBatis…

第5集丨webpack 江湖 —— 项目发布 和 source map

目录 一、webpack项目发布1.1 新增发布(build)命令1.2 优化js和图片文件的存放路径1.3 执行1.4 效果 二、clean-webpack-plugin插件2.1 安装2.2 配置2.3 执行 三、source map3.1 配置3.2 生成的source map文件 四、定义符4.1 配置4.2 使用 五、工程附件汇总5.1 webpack.config.…

大麦订单一键生成 仿大麦订单生成

后台一键生成链接&#xff0c;独立后台管理 教程&#xff1a;修改数据库config/Conn 不会可以看源码里有教程 下载程序&#xff1a;https://pan.baidu.com/s/16lN3gvRIZm7pqhvVMYYecQ?pwd6zw3

强化学习(EfficientZero)(应用于图像和声音)

目录 摘要 1.背景介绍 2.MCTS&#xff08;蒙特卡洛树搜索&#xff09;&#xff08;推理类模型&#xff0c;棋类效果应用好&#xff0c;控制好像也不错&#xff09; 3.MUZERO 4.EfficientZero&#xff08;基于MUZERO&#xff09; 展望 参考文献 摘要 在文中&#xff0c;基于…

【雕爷学编程】MicroPython动手做(20)——掌控板之三轴加速度5

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

数学建模 好文章和资源推荐

数学建模入门篇(0基础必看&#xff0c;全是自己的经验) 【竞赛|数学建模】Part 1&#xff1a;什么是数学建模和各模块介绍 0基础小白&#xff0c;如何入门数学建模&#xff1f; 数学建模入门篇(0基础必看&#xff0c;全是自己的经验) 什么是数学建模 重申了一下题目&#xff…