Java 多线程系列Ⅱ(线程安全)

news2025/1/17 3:54:46

线程安全

  • 一、线程不安全
    • 线程不安全的原因:
  • 二、线程不安全案例与解决方案
    • 1、修改共享资源
      • synchronized 使用
      • synchronized 特性
    • 2、内存可见性
      • Java内存模型(JMM)
      • 内存可见性问题
    • 3、指令重排列
    • 4、synchronized 和 volatile
    • 5、拓展知识:修饰符顺序规范

一、线程不安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。否则就称之为线程不安全。

线程不安全的原因:

  1. 抢占式执行,可以说“线程的无序调度”是罪魁祸首,万恶之源!!!(是操作系统内核来实现的,程序员无法控制)
  2. 多个线程修改同一个变量。
  3. 修改操作,不是原子(不可分割的最小单位)的。某个操作对应单个cpu指令就是原子的,如果单个操作对应多个CPU指令,大概率不是原子的。正是因为不是原子的,导致多个线程的指令排列存在更多的变数。
  4. 内存可见性,引起的线程不安全。
  5. 指令重排列,引起的线程不安全。

二、线程不安全案例与解决方案

1、修改共享资源

即针对于多个线程修改同一个变量的情况,由于修改操作可能不是原子的(单条cpu指令),在多线程的随机调度下,就会导致多个线程的指令排列存在更多变数。

例如如下代码:

class Counter {
    private int count = 0;
	public void add() {
            count++;
    }

    public int getCount() {
        return count;
    }
}

public class ThreadExample_unsafe {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        //等两个线程结束后查看结果
        t1.join();
        t2.join();

        System.out.println(counter.getCount());
    }
}

结果分析:
对于如上代码,两个线程 t1、t2 各自对 count 自增 50000 次,理论情况下结果应为100000,但是实际运行结果小于100000,尽管多次运行依旧如此。以上现象正是因为,在 t1、t2 两个线程修改 count 时,由于每个 ++ 操作都不是原子的,可以分割为(1.读取 2.修改 3.写入),在系统随机调度的加持下就会导致 t1、t2 线程++操作实际指令排列顺序有多种可能,最终导致结果异常。如下图绘制了两种可能出现的情况:

解决方案-加锁

对于以上场景,在保证并发执行的情况下,由于线程的随机调度是系统内核来实现的,程序员不可控,而多个线程修改同一变量又是业务需求,所以要保证该场景下的线程安全我们可以考虑将修改操作变成原子的。而“加锁”可以保证原子性效果synchronized 是 Java 中用于实现锁的关键字,下面我们详细介绍:

synchronized 使用

Java中使用synchronized针对“对象头”加锁,synchronized 势必要搭配一个具体的对象来使用

(1)synchronized对普通方法加锁

// 给实例方法加锁
public void add() {
    synchronized (this) {
        count++;
    }
}

//如果直接给方法使用synchronized修饰,此时就相当于以this为锁对象
synchronized public void add() {
       count++;
}

(2)synchronized对静态方法加锁

//给静态方法加锁
public static void test2() {
	// Counter.class相当于类对象
	synchronized (Counter.class) {
		
	}
}
//如果直接给方法使用synchronized修饰,此时就相当于以Counter.class为锁对象
synchronized public static void test() {

}

(3)synchronized对任意代码块加锁

// 自定义锁对象
Object locker = new Object();

synchronized (locker) {
    // 代码逻辑
    // . . .
}

拓展:被 synchronized 修饰的方法又叫同步方法;被 synchronized 修饰的代码块又叫同步代码块。

synchronized 特性

  1. 进入 synchronized 修饰的代码块, 相当于 加锁。 退出 synchronized 修饰的代码块, 相当于 解锁。
  2. synchronized修饰的代码块具有原子性效果。即加锁是让多个线程的某个部分进行串行。
  3. synchronized()其中()里的对象,可以是任意一个Object对象,这个对象也被称为锁对象。synchronized用的锁是存在Java对象头里的,可以粗略理解成:每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态,如果当前是 “解锁” 状态, 那么就可以使用, 使用时需要设为 “加锁” 状态,如果当前是 “加锁” 状态, 那么其他线程无法使用, 只能阻塞等待
  4. synchronized是互斥锁,所谓互斥,即同一时间多个线程不能对同一对象加锁。而是同一时刻只能有一个线程获取锁,其他线程阻塞等待。因此多个线程尝试对同一个锁对象加锁,此时就会产生锁竞争,针对不同对象加锁,就不会有锁竞争。
  5. 阻塞等待:针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。
  6. 获取锁原则:上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”。 这也就是操作系统线程调度的一部分工作.。假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则。
  7. 拓展:synchronized 既是悲观锁,也是乐观锁。既是轻量级锁,也是重量级锁。轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。是互斥锁不是读写锁,是非公平锁。(后续介绍)

2、内存可见性

Java内存模型(JMM)

介绍内存可见性之前,我们先简单了解一下java内存模型:

  • 工作内存-work memory :CPU寄存器 + 缓存
  • 主内存-main memory :内存
  1. 线程之间的共享变量存在 主内存 (Main Memory).
  2. 每一个线程都有自己的 “工作内存” (Working Memory) .
  3. 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  4. 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

为什么引入工作内存?

这里引入工作内存主要是因为CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度。在某些情况下,这也是提高效率的一种重要手段。比如某个代码中要连续 10000 次读取某个变量的值, 如果 10000 次都从内存读, 速度是很慢的。但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9999 次读数据就不必直接访问内存了。效率就大大提高了。

内存可见性问题

内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。

什么是内存可见性引起的多线程安全问题?

一般来说由内存可见性引发的多线程问题,是由于编译器的优化。例如:

public class ThreadExample_unsafe2 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (flag == 0) {
                //空转
            }
            System.out.println("循环结束,t1结束!");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入一个整数:");
            flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

结果分析
如上代码,t1线程中flag == 0涉及到两个CPU指令,假设这两个指令分别是load-从内存读取数据到工作内存(CPU寄存器),cmp-比较寄存器中的值是否为0。对于这两个操作,load的时间开销远远高于cmp。此时编译器在处理的时候发现,load的开销很大,每次load的结果都一样,此时编译器就做了一个非常大胆的决定,即只有第一次load执行从内存读取到工作内存,后续循环的load直接从工作内存读取。所以尽管输入了不为0的整数,因为工作内存数据不变,程序依然继续运行。

关于编译器优化:

针对以上线程安全问题,是编译器优化的结果,关于编译器优化,这是一个很普遍的事,编译器优化就是能够智能调整你代码的执行逻辑,保证程序结果不变的情况下,通过加减语句,通过语句变换等一系列操作,让整个程序的执行效率大大提升。但是对于编译器优化在单线程情况下一般是不会出现任何问题的,但是多线程下不能保证。

解决方案

使用volatile修饰:被关键字volatile修饰的变量,此时编译器就会禁止例如上述优化,能够保证每次都是从内存重新读取数据到工作内存,保证了内存可见性。

3、指令重排列

指令重排,也是程序优化的一种手段,和编译器的优化有直接的关系,也和线程不安全直接相关。如果是单线程的情况下,这样的调整没问题,但是在多线程的情况下就会发生线程安全问题。

例如下面伪代码:

其中线程t1中s = new Student();大体可以分为3步:

  1. 申请内存空间
  2. 调用构造方法(初始化内存数据)
  3. 把对象的引用赋值给s(内存地址的赋值)

如果是单线程下,上述操作很容易保证,如果是多线程下,指令2,3重排先执行3后执行2,在刚执行完指令3后,t2线程执行s.learn();就会出现bug。

解决方案

  1. 当前场景下可使用volatile修饰,因为volatile具有防止指令重拍的作用,可以解决上述可能出现的问题。
  2. 可以对new操作加锁-synchronized

4、synchronized 和 volatile

  1. synchronized 保证原子性,volatile 不保证原子性。
  2. 一般情况下 volatile 适用于一个线程读一个线程写的情况。
  3. 一般情况下 synchronized 适用于多个线程写的情况。

5、拓展知识:修饰符顺序规范

在Java中,修饰符的顺序可以任意排列,但是为了方便阅读和代码的一致性,一般会按照以下的顺序进行排列:

  1. 可见性修饰符(public, protected, private)
  2. 非可见性修饰符(static, final, abstract)
  3. 类型修饰符(class, interface, enum)
  4. 其他修饰符(synchronized, transient, volatile,native, strictfp)

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

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

相关文章

Python学习 -- 异常堆栈追踪技术

在编写Python代码时&#xff0c;出现异常是不可避免的。异常堆栈追踪是一种强大的工具&#xff0c;可以帮助我们定位错误发生的位置以及调用栈信息。Python的traceback模块提供了多种方法来获取和展示异常的堆栈信息。本文将详细介绍traceback模块中的print_exc()方法&#xff…

spring:事务失效+事务传播行为

一、事务失效 1.Transactional作用在非public上 Transactionalvoid transferAccounts(){adminDao.sub();System.out.println(10/0);adminDao.add();} 只执行sub&#xff08;&#xff09; 2.异常被try catch捕获 Transactionalpublic void transferAccounts(){adminDao.sub(…

vue3项目导入异常Error: @vitejs/PLUGIN-vue requires vue (>=3.2.13)

vue3项目导入异常 1、异常提示如下&#xff1a; failed TO LOAD config FROM D:\ws-projects\vite.co nfig.js error WHEN STARTING dev SERVER: Error: vitejs/PLUGIN-vue requires vue (>3.2.13) OR vue/compiler-sfc TO be pre sent IN the dependency tree.2、解决办法…

校园气象站的功能和作用

校园气象站是一种用于监测和记录气象数据的设备&#xff0c;下面将从功能和作用两个方面来详细介绍校园气象站。 一、校园气象站的功能 ①气象数据监测 校园气象站可以监测多种气象数据&#xff0c;包括温度、湿度、气压、风速、风向等。这些数据会通过4G方式上传至环境监控…

金蝶云星空二开,公有云执行SQL

功能背景&#xff1b; 金蝶公有云执行sql工具&#xff0c;因官方为云部署 用户无法连接数据库增删改查 天梯维护网页仅支持增删改操作 二开单据已支持根据sql动态生成单据体 与sql可视化界面操作一致 功能实现及场景&#xff1a; 1.可用于公有云执行sql类操作 2.私有云部署&am…

OB Cloud助力泡泡玛特打造新一代分布式抽盒机系统

作为中国潮玩行业的领先者&#xff0c;泡泡玛特凭借 MOLLY、DIMOO、SKULLPANDA 等爆款 IP&#xff0c;以及线上线下全渠道营销收获了千万年轻人的喜爱&#xff0c;会员数达到 2600 多万。2022 年&#xff0c;泡泡玛特实现 46.2 亿元营收&#xff0c;其中线上渠道营收占比 41.8%…

java 浅谈ThreadLocal底层源码(通俗易懂)

目录 一、ThreadLocal类基本介绍 1.概述 : 2.作用及特定 : 二、ThreadLocal类源码解读 1.代码准备 : 1.1 图示 1.2 数据对象 1.3 测试类 1.4 运行测试 2.源码分析 : 2.1 set方法解读 2.2 get方法解读 一、ThreadLocal类基本介绍 1.概述 : (1) ThreadLocal&#xff0c;本…

YOLO数据集划分(训练集、验证集、测试集)

1.将训练集、验证集、测试集按照7:2:1随机划分 1.项目准备 1.在项目下新建一个py文件&#xff0c;名字就叫做splitDataset1.py 2.将自己需要划分的原数据集就放在项目文件夹下面 以我的为例&#xff0c;我的原数据集名字叫做hatDataXml 里面的JPEGImages装的是图片 Annota…

安达发|APS软件排程规则及异常处理方案详解

随着科技的发展&#xff0c;工业生产逐渐向智能化、自动化方向发展。APS(高级计划与排程)软件作为一种集成了先进技术和理念的工业软件&#xff0c;可以帮助企业实现生产过程的优化和控制。其中&#xff0c;排程规则是APS软件的核心功能之一&#xff0c;它可以帮助企业合理安排…

港联证券|什么是北上资金?北上资金连续流入的股票好不好?

一般在股市收盘之后&#xff0c;公司会对当日的股市资金变化做一个资金总结&#xff0c;比如说北上资金的流入或许流出。那么什么是北上资金&#xff1f;北上资金连续流入的股票好不好&#xff1f;下面就由港联证券为大家剖析&#xff1a; 什么是北上资金&#xff1f; 北上资金…

Java on VS Code 8月更新|反编译器用户体验优化、新 Maven 项目工作流、代码高亮稳定性提升

作者&#xff1a;Nick Zhu 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎来到 Visual Studio Code for Java 的 8 月更新&#xff01;在这篇博客中&#xff0c;我们将为您提供有关反编译器支持的更多改进。此外&#xff0c;我们将展示如何创建没有原型的 Maven 项目以及一…

LabVIEW计算测量路径输出端随机变量的概率分布密度

LabVIEW计算测量路径输出端随机变量的概率分布密度 今天&#xff0c;开发算法和软件来解决计量综合的问题&#xff0c;即为特定问题寻找最佳测量算法。提出了算法支持&#xff0c;以便从计量上综合测量路径并确定所开发测量仪器的测量误差。测量路径由串联的几个块组成&#x…

用于设计和分析具有恒定近心点半径的低推力螺旋轨迹研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

ShardingJDBC——基于JPA的读写分离实战

摘要 本博文主要介绍基于JPA的读写分离实战&#xff0c;帮助大家更好的学会使用读写分离。透明化读写分离所带来的影响&#xff0c;让使用方尽量像使用一个数据库一样使用主从数据库集群&#xff0c;是ShardingSphere读写分离模块的主要设计目标。 一、读写分离库的场景和设计…

一文了解tcp/ip协议的运行原理

接触代理ip的人都了解https/sock5等ip协议&#xff0c;那么TCP/IP 协议又是什么&#xff1f; 一、什么是TCP/IP 协议&#xff1f; TCP/IP 协议实际上是一系列网络通信协议的一个统称&#xff0c;他负责具体的数据传输工作&#xff0c;核心的两个协议包括TCP以及IP&#xff0c…

启动服务报错:Command line is too long Shorten command line for xxx or also for Spri

ommand line is too long. Shorten command line for ProjectApprovalApplication or also for Spring Boot default configuration. 启动springboot 项目的时候报错 解决方案&#xff1a; 点击提示中的&#xff1a;default&#xff1a;然后在弹出窗口中选择&#xff1a;JAR xx…

Apache Struts2漏洞复现之s2-005漏洞复现

0x01 声明&#xff1a; 仅供学习参考使用&#xff0c;请勿用作违法用途&#xff0c;否则后果自负。 0x02 简介&#xff1a; Apache Struts 2是一个用于开发Java EE网络应用程序的开放源代码网页应用程序架构。它利用并延伸了Java ServletAPI&#xff0c;鼓励开发者采用MVC架构…

LNMT架构

所谓的LNMT架构 指的就是Linux操作系统上部署Nginx web服务器、MySQL数据库服务器、Tomcat中间件服务器 L linux N nginx M mysql T tomcat 单机部署 1&#xff0c;安装 apache-tomcat 2&#xff0c;移动目录 3&#xff0c;复制第二个tomcat 4&#xff0c;…

Annual Inspection

机动车年检流程【交警12123】APP 到【检查地方】门口墙上贴着 然后上缴钥匙&#xff0c;等待&#xff0c;本次等待不到半小时搞定&#xff0c;速度很满意&#xff0c; 发现检测人员把你的里程数纠正了。 给你的行驶证&#xff0c;打印这些字样&#xff1a;检验有效期至XXXX 再给…

代码仓库必知:git忽略文件规则。问题解决: gitignore文件没有忽略某个文件,就是提交不了

文章目录 问题背景一、Git种的忽略文件有哪些&#xff1f;二、设置忽略文件的方法 问题背景 假设我的文件叫a.tsx&#xff0c;在 git status 时检测不到它的改动记录&#xff0c;并且 git commit 不了&#xff01; 强制推才可以。这样会影响开发进度&#xff0c;因为我们经常需…