news 2026/6/9 20:11:00

【多线程】多线程中的安全问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【多线程】多线程中的安全问题

目录

一、体会线程安全问题

二、线程安全的概念

三、线程安全问题的原因

四、解决线程安全问题的方法

4.1 synchronized 关键字


一、体会线程安全问题

当我们编写一个多线程程序,要求两个线程对同一个变量(共享变量)进行修改,得到的结果是否与预期一致?

创建两个线程,分别对共享变量(count)进行自增5万次操作,最后输出的结果理论上应为10万,但是实际上输出的结果是一个小于10万且不确定的数。

读者可以自行实现一下该多线程程序,运行后看看结果是否符合预期。

public class Demo14_threadSafety { private static int count = 0; public static void main1(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 理论上输出的结果应是100000,实际输出的结果是0 // 原因是主线程 main 运行太快了,当 t1 和 t2 线程还在计算时,主线程已经打印结果、运行完毕了 System.out.println(count); } // 让主线程等待 t1 和 t2 线程,等到它们两个都执行完成再打印,故使用 join 方法 public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 在主线程中,通过 t1 和 t2 对象调用 join 方法 // 表示让主线程 main 等待 t1 线程和 t2 线程 t1.join(); t2.join(); // 当两个线程都执行完毕后主线程再继续执行打印操作 System.out.println(count); // 实际输出的结果小于100000,仍不符合预期 } }

二、线程安全的概念

通过上面的一个例子,想必读者已经体会到线程安全问题了吧?那究竟什么是线程安全问题呢?其原因是什么?如何解决线程安全问题呢?

不要急,且听小编慢慢道来~

如果在多线程环境下运行的程序其结果符合预期或与在单线程环境下运行的结果一致,就说这个程序是线程安全的,否则是线程不安全的。

上面的例子在单线程环境下运行——比如来两个循环对共享变量进行自增操作,那么结果是符合预期的;但是在多线程环境下运行就不符合预期。因此该程序是线程不安全的,也可以说该程序存在线程安全问题。

三、线程安全问题的原因

究竟是哪里出问题导致程序出现线程安全问题呢?

究其根本,罪魁祸首是操作系统的线程调度有随机性/抢占式执行

由于操作系统的线程调度是有随机性的,这就会存在这种情况:某一个线程还没执行完呢,就调度到其他线程去执行了,从而导致数据不正确。

当然了,一个巴掌拍不响,还有以下三个导致线程不安全的原因:

  1. 原子性:指 Java 语句,一条 Java 语句可能对应不止一条指令,若对应一条指令,就是原子的。
  2. 可见性:一个线程对主内存(共享变量)的修改,可以及时被其他线程看到。
  3. 有序性:一个线程观察其他线程中指令的执行顺序,由于 JVM 对指令进行了重排序,观察到的顺序一般比较杂乱。因其原理与 CPU 及编译器的底层原理有关,暂不讨论。

之前的例子就是由于原子性没有得到保障而出现线程安全问题:

public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }

1. “count ++” 这条语句对应多条指令:读取数据、计算结果、存储数据。

2. t1 线程和 t2 线程分别执行一次“count++”语句,期望的结果是“count = 2”,其过程如下:

初始情况:

当线程执行“count ++”时,总共分三个步骤:load、update、save,由于线程调度的随机性/抢占式执行,可能会出现以下情况(可能出现的情况有很多种,这里只是其中一种):

这时候 t1 正在执行“count ++”这条语句,执行了“load 和 update”指令后,t1 的工作内存(寄存器)存着更新后的值,但是还未被写回内存中:

接着调度到 t2 线程并开始执行“count ++”语句,并且语句中包含的三条指令都执行。此时由于 t1 更新后的 count 的值还未写回内存,因此 t2 执行 load 操作所获取到的 count 仍是 0。接着 t2 执行 update 和 save 指令:

当 t2 执行完成,内存的 count 已被修改为 1 。此时调度回 t1 线程并继续执行 save 指令,但是 t1 线程寄存器中 count 的值也是 1 ,此时写回内存更新后 count 的值依然是 1 。

结果 count = 1,与预期的 count = 2 不符,因此存在线程安全问题,其原因是操作系统的随机线程调度和 count 语句存在非原子性。

四、解决线程安全问题的方法

从上面的例子我们知道,当一条语句的指令被拆开来执行的话是存在线程安全问题的,但是,当我们将“count ++”这条语句的三个指令都放在一起执行怎么样?

当线程调度的情况如下:

此时 t1 线程开始执行“count ++”语句的 load、update 和 save 指令。内存中的 count 为 0,t1 读取到内存中的 count 之后更新至 1 并写回内存中。当 t1 执行完成后内存的 count 由 0 更新至 1:

接着调度至 t2 线程,开始执行“count ++”语句的 load、update 和 save 指令。经过更新后内存中的 count 为 1,此时 t2 读取 count 并更新为 2,然后写回内存中。当 t2 执行完成,内存中的 count 就更新成 2 了:

可以发现,结果与预期相符!说明这个方法可行。

可以将操作顺序改成先让 t1 线程完成“count ++”操作,再让 t2 线程完成该操作——即串行执行

现在我们对之前的例子进行优化:

// 可以试着让 t1 线程先执行完后,再让 t2 线程执行,改成串行执行 public static void main3(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t1.join(); t2.start(); t2.join(); System.out.println(count); }

刚刚是让一个线程一次性执行“count ++”这条语句的三个指令,也就是说,我们是通过这样操作将原本是非原子的三条指令打包成了一个原子指令(即执行过程中不可被打断——调度走)。这样就有效的解决了线程安全问题。

而上述的操作,其实就是 Java 中的加锁操作

当一个线程执行一个非原子的语句时,通过加锁操作可以防止在执行过程中被调度走或被其他线程打断,若其他线程想要执行该语句,则要进入阻塞等待的状态,当线程执行完毕并将锁释放,操作系统这时唤醒等待中的线程,才可以执行该语句。

就相当于上厕所:当厕所内没有人时(没有线程加锁),就可以使用;当厕所内有人时(已经有线程加锁了),那么就必须等里面的人出来后才能使用。

注意:

  1. 前一个线程解锁之后,并不是后一个线程立刻获取到锁。而是需要靠操作系统唤醒阻塞等待中的线程的。
  2. 若 t1、t2 和 t3 三个线程竞争同一个锁,当 t1 线程获取到锁,t2 线程再尝试获取锁,接着 t3 线程尝试获取锁,此时 t2 和 t3 线程都因获取锁失败而处于阻塞等待状态。当 t1 线程释放锁之后,t2 线程并不会因为先进入阻塞状态在被唤醒后比 t3 先拿到锁,而是和 t3 进行公平竞争。(不遵循先来后到原则)

4.1 synchronized 关键字

加锁 / 解锁这些操作本身是在操作系统所提供的 API 中的,很多编程语言对其进行了封装,Java 中使用 synchronized 关键字来进行加锁 / 解锁操作,其底层是使用操作系统的mutex lock来实现的。Java 中的任何一个对象都可以用作“锁”

synchronized (锁对象){——> 进入代码块,相当于加锁操作

// 一些需要保护的逻辑

}——> 出了代码块,相当于解锁操作

当多个线程针对同一个锁对象竞争的时候,加锁操作才有意义。

对之前的例子进行加锁操作:

public class Demo15_synchronized { private static int count = 0; public static void main1(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }

synchronized 关键字用来修饰普通方法时,相当于给this加锁;

synchronized 关键字用来修饰静态方法时,相当于给类对象加锁。

于是可以使用另一种写法:

// 写法二: // 将 count++ 所包含的三个操作封装成一个 add 方法 // 使用 synchronized 修饰 add 方法 class Counter { private int count = 0; synchronized public void add () { // synchronized 修饰普通方法相当于给 this 加锁 count++; } // 相当于: // public void add () { // synchronized (this) { // count++; // } // } public int get () { return count; } } 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.get()); }

这样一来,就成功解决了多线程程序中的线程安全问题。

今天暂且到这吧~


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/9 15:55:13

橙单低代码平台2025:企业级应用开发的效率革命

橙单低代码平台2025&#xff1a;企业级应用开发的效率革命 【免费下载链接】orange-form 橙单中台化低代码生成器。可完整支持多应用、多租户、多渠道、工作流 (Flowable & Activiti)、在线表单、自定义数据同步、自定义Job、多表关联、跨服务多表关联、框架技术栈自由组合…

作者头像 李华
网站建设 2026/6/8 11:37:53

终极免费AI对话工具:零基础搭建完整指南

终极免费AI对话工具&#xff1a;零基础搭建完整指南 【免费下载链接】freegpt-webui GPT 3.5/4 with a Chat Web UI. No API key required. 项目地址: https://gitcode.com/gh_mirrors/fre/freegpt-webui 想要体验最先进的AI对话能力却苦于没有API密钥&#xff1f;这个基…

作者头像 李华
网站建设 2026/6/8 21:56:45

React useContextSelector性能优化深度解析

React useContextSelector性能优化深度解析 【免费下载链接】use-context-selector React useContextSelector hook in userland 项目地址: https://gitcode.com/gh_mirrors/us/use-context-selector 项目概述 use-context-selector是一个专为解决React Context性能问题…

作者头像 李华
网站建设 2026/6/9 20:06:52

bibliometrix:从文献数据中挖掘科研趋势的完整解决方案

你是否曾经面对数千篇文献数据感到手足无措&#xff1f;想要了解某个研究领域的发展趋势&#xff0c;却不知从何入手&#xff1f;在信息爆炸的科研时代&#xff0c;文献计量分析已成为每个研究者的必备技能。而bibliometrix作为R语言生态中的科学计量分析利器&#xff0c;为你提…

作者头像 李华
网站建设 2026/6/9 20:03:37

通过企业微信ipad协议接口发送名片消息功能

请求方式POSTContentType:”application/json”参数参数名必选类型说明uuid是String每个实例的唯一标识&#xff0c;根据uuid操作具体企业微信send_userid是long要发送的人或群idisRoom是bool是否是群消息请求示例{"uuid":"81eddc1771c293e933cf6ef9b17e5f87&qu…

作者头像 李华
网站建设 2026/6/8 17:42:06

Maxwell电机多目标尺寸优化 Ansys Maxwell 和OptiSlang 有案例电机

Maxwell电机多目标尺寸优化 Ansys Maxwell 和OptiSlang 有案例电机&#xff0c;永磁同步电机内嵌式 满足电机多尺寸参数入手&#xff0c;满足多尺寸联动优化&#xff0c;最终达到多参数优化效果 提供源文件&#xff0c;提供操作视频项目概述 本项目是一个基于Ansys Maxwell平台…

作者头像 李华