JAVA中的多线程安全问题及解决方案

ops/2025/3/14 7:16:40/

一.线程安全的定义

线程安全是指在多线程环境下,对共享资源进行并发访问时,程序能够正确地处理,不会出现数据不一致、逻辑错误等问题,确保程序的执行结果与单线程环境下的执行结果相同,或者符合预期的并发逻辑。

有些代码在多线程环境执行下会出现问题,这样的问题就称为线程不安全

二.synchronized 关键字

1.作用

核心特性

  • 互斥性:同一时间只有一个线程持有锁,其他线程阻塞等待。
  • 自动释放锁:退出同步块或方法时,锁自动释放。

synchronized 关键字在 Java 里用于实现同步机制,保证同一时刻只有一个线程可以访问被保护的代码块或方法。 

2.用法

1.修饰方法  2.修饰代码块 (必须指定锁对象!!)

java">// 同步方法
public synchronized void syncMethod() {// 操作共享资源
}// 同步代码块
public void syncBlock() {synchronized (lockObject) {// 操作共享资源}
}

三.什么是锁对象

1. synchronized 修饰静态方法

Counter 类的 increase 方法被 static synchronized 修饰,其锁对象是 Counter 类的 Class 对象,也就是 Counter.class。示例代码如下:

1.1示例代码

java">class Counter {// 将 count 定义为静态变量private static int count = 0;// 使用 synchronized 保证线程安全public static synchronized void increase() {count++;}public static int getCount() {return count;}
}class Demo {public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {Counter.increase();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {Counter.increase();}});thread1.start();thread2.start();// 等待两个线程执行完毕thread1.join();thread2.join();// 打印计数器的结果System.out.println("最终计数结果: " + Counter.getCount());}
}

1.2 代码结果 

 代码结果:(因为所持有的锁对象为同一个) 

1.3 代码解释 

具体到你的代码,Counter 类的 increase 方法:

在多线程环境下,当一个线程进入 increase 方法时,它会尝试获取 Counter.class 这个锁对象。如果该锁对象当前没有被其他线程持有,那么这个线程就可以对这个锁对象进行加锁--执行 count++ 操作;在这个线程执行期间,其他线程如果也想进入 increase 方法,就会被阻塞,直到持有锁的线程执行完 increase 方法并释放 Counter.class 锁(对Counter.class这个锁对象进行解锁)。 

2. synchronized 修饰实例方法

若 synchronized 修饰的是实例方法,锁对象是调用该方法的实例对象(即 this)。示例如下:

2.1 示例代码

java">class Counter {public int count = 0;public synchronized void increase() {count++;}
}class Demo14 {private static Counter counter1 = new Counter();private static Counter counter2 = new Counter();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter1.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter2.increase();}});t1.start();t2.start();t1.join();t2.join();// 修正输出,打印已定义的变量System.out.println("counter1 count: " + counter1.count);System.out.println("counter2 count: " + counter2.count);}
}

2.2 代码结果 

代码结果:(各自计数为 50000

2.3 代码解释 

此时的两个线程持有的锁对象不是同一个。线程t1,t2持有的锁对象是counter1,counter2变量指向的不同Counter对象

3. synchronized 修饰代码块

这里的this表示谁调用了increase()方法里面,synchroized修饰的代码块就针对谁进行加锁

此时的两个线程的锁对象为同一个——>counter变量指向的Counter对象

题外话:

static 修饰引用对象变量。首先,static 修饰的变量属于类,所有实例共享。示例里的 Counter counter 被 static 修饰,是类变量,整个类共享这一个实例

java">class Counter {public int count = 0;public void increase() {synchronized (this) {count++;}}
}class Demo14 {private static Counter counter = new Counter();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();// 主线程等待子线程执行完毕t1.join(); t2.join(); System.out.println("输出结果:" + counter.count);}
}

综上所述,锁对象的选择取决于 synchronized 的使用方式,不同的锁对象会影响同步的范围和效果。

四.原子性问题

先来看一段问题代码

1. 问题代码

java">class Counter {// 将 count 定义为静态变量private static int count = 0;//increase定义为静态方法public static void increase() {count++;}public static int getCount() {return count;}
}class Demo {public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {Counter.increase();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {Counter.increase();}});thread1.start();thread2.start();// 等待两个线程执行完毕thread1.join();thread2.join();// 打印计数器的结果System.out.println("最终计数结果: " + Counter.getCount());}
}

1.1 运行结果

( 每次运行结果都不一样)

2.出现问题的原因

    接下来说明什么是原子性 

    2.1 什么是原子性?

    原子性指的是一个操作是不可中断的,要么全部执行,要么都不执行。在多线程环境下,如果多个线程同时修改共享变量,可能会导致数据不一致。比如 i++ 这样的操作,虽然看起来是一条语句,但实际上分为读取、增加和写入三个步骤,这就不是原子的。

    原子性问题源于 CPU 指令的非原子性。例如,i++ 看似是一条语句,但实际分为三步:

    1. 读取:从内存读取变量 i 的值。
    2. 增加:在 CPU 寄存*器中执行 +1 操作。
    3. 写入:将结果写回内存。

    2.2 问题代码解释


    count++ 操作并非原子操作,其底层执行分为 “读取值 → 计算新值 → 写入值” 三个步骤。多线程环境下,若多个线程同时执行 count++,可能出现以下场景:

    1. 线程 A 读取 count 值为 10,还未执行写入;
    2. 线程 B 也读取 count 值为 10(此时线程 A 的修改未生效);
    3. 线程 A、B 分别计算新值为 11 并写入。最终 count 只增加了 1,而非预期的增加 2,导致计数丢失。

    3.解决方案

    跳转到———目录1.二.三

    五.内存可见性问题

    • 每个线程有自己的工作内存(缓存),共享变量存储在主内存中。
    • 线程操作变量时需先将变量从主内存复制到工作内存,修改后再写回主内存。
    • 示例:线程 A 修改主内存中的变量 x,若未及时写回,线程 B 的工作内存中仍保留旧值 x=0,导致可见性问题。

    1. 问题代码 

    java">public class VisibilityProblem {private static boolean flag = false;public static void main(String[] args) throws InterruptedException {// 修改线程:500ms后修改flag为truenew Thread(() -> {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}flag = true;System.out.println("修改线程:flag已设为true");}).start();// 读取线程:循环检查flagnew Thread(() -> {while (!flag) {// 空循环,等待flag变为true}System.out.println("读取线程:接收到flag为true");}).start();}
    }

    1.1 预期输出

    修改线程:flag已设为true

    读取线程:接收到flag为true

    1.2 实际输出

    修改线程:flag已设为true (程序不终止,读取线程无法感知flag的修改)

    2. 问题原因

    • 读取线程的工作内存(缓存)读取的是 flag 的旧值(false),未从主内存更新,导致循环无法终止

    3.解决办法

    1.使用volatile 关键字

    原理

    • volatile 强制读取线程每次从主内存获取 flag 的最新值,确保可见性。

    修改后的代码 

    java"> private static volatile boolean flag = false; // 添加volatile关键字

     2.使用 synchronized

    原理

    • synchronized 保证同一时间只有一个线程操作 flag,且退出同步块时强制将 flag 写回主内存,确保可见性。

    修改后的代码

    java">public class VisibilityProblem {private static boolean flag = false;// 静态锁对象(所有线程共享同一把锁)private static final Object LOCK = new Object();public static void main(String[] args) throws InterruptedException {// 修改线程:500ms后修改flag为truenew Thread(() -> {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (LOCK) { // 获取锁flag = true;System.out.println("修改线程:flag已设为true");}}).start();// 读取线程:循环检查flagnew Thread(() -> {while (true) { // 死循环+锁内检查synchronized (LOCK) { // 获取同一把锁if (flag) { // 锁内读取,保证可见性System.out.println("读取线程:接收到flag为true");break; // 退出循环}}// 锁外可添加短暂休眠避免空转(非必须)// Thread.yield();}}).start();}
    }

     3.两者区别

    • 简单场景用 volatile,保证可见性。但不保证原子性
    • 复杂场景用 synchronized 或 Lock,同时保证原子性和可见性。

    六.抢占式执行问题

    1.作用

    waitnotifyObject类的两个重要方法,用于实现线程间的通信与协作(调度线程执行顺序),它们通常和synchronized关键字配合使用

     2.wait和notify和notifyAll

    1.wait():使当前线程进入等待状态,同时释放该线程持有的对象锁。线程会进入等待队列 ,直到其他线程调用同一对象的notify()notifyAll()方法将其唤醒

    2.notify()唤醒等待在同一对象上的一个线程。被唤醒的线程不会立即执行,而是进入阻塞队列,等待调用notify()的线程释放锁之后,再重新竞争锁,获取到锁后才能继续执行。

    3.notifyAll()唤醒等待在同一对象上的所有线程。这些被唤醒的线程同样会进入阻塞队列竞争锁。通常情况下,为避免某些线程长时间处于等待状态导致死锁,推荐使用notifyAll()

    .使用注意事项 

    • 必须在同步块或同步方法中调用waitnotify方法必须在synchronized修饰的代码块或方法中使用。因为它们依赖于对象的监视器锁(monitor),只有持有该对象锁的线程才能调用这两个方法,否则会抛出IllegalMonitorStateException异常。
    • 调用线程需持有对象锁:当线程调用wait方法时,它必须已经持有该对象的锁;同样,调用notifynotifyAll方法的线程也需要持有对象锁。

    3. 极简案例:

    《两个线程交替打印数字和字母(5 轮)》:

    java">public class SimpleAlternatePrint {private static final Object LOCK = new Object();private static int turn = 0; // 0=数字线程,1=字母线程public static void main(String[] args) {// 数字线程(打印1-5)new Thread(() -> {for (int i = 1; i <= 5; i++) {synchronized (LOCK) {while (turn != 0) { // 不是自己的回合,等待try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); }}System.out.print(i + " "); // 打印数字turn = 1; // 切换回合LOCK.notify(); // 唤醒字母线程}}}).start();// 字母线程(打印A-E)new Thread(() -> {for (char c = 'A'; c <= 'E'; c++) {synchronized (LOCK) {while (turn != 1) { // 不是自己的回合,等待try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); }}System.out.print(c + " "); // 打印字母turn = 0; // 切换回合LOCK.notify(); // 唤醒数字线程}}}).start();}
    }

    4.复杂案例

    《三个线程按顺序执行任务》:

    java">class ThreadSequence {private boolean isFirstDone = false;private boolean isSecondDone = false;// 第一个线程执行的方法public synchronized void firstTask() throws InterruptedException {System.out.println("First task started");// 模拟任务执行Thread.sleep(1000);System.out.println("First task completed");//把isFirstDone标志为true,第二个线程结束等待循环了,开始进入下一任务isFirstDone = true;notifyAll();// 1.唤醒等待在同一个对象上的所有线程 //2.第一个线程释放锁}// 第二个线程执行的方法public synchronized void secondTask() throws InterruptedException {while (!isFirstDone) {// 等待第一个线程完成wait();}System.out.println("Second task started");// 模拟任务执行Thread.sleep(1000);System.out.println("Second task completed");isSecondDone = true;// 唤醒等待的第三个线程notifyAll();}// 第三个线程执行的方法public synchronized void thirdTask() throws InterruptedException {while (!isSecondDone) {// 等待第二个线程完成wait();}System.out.println("Third task started");// 模拟任务执行Thread.sleep(1000);System.out.println("Third task completed");}
    }

    4.1 运行结果

    4.2 运行顺序说明 

    1. First 线程执行 firstTask

    • 持有锁 → 打印First task started → 睡眠 1 秒 → 打印First task completed → 设置isFirstDone=true → 调用notifyAll()

    • 关键:唤醒所有等待在ThreadSequence对象上的线程(包括 Second、Third 线程),但此时 Second/Third 尚未进入等待状态(因未获取锁)。

    2. Second 线程执行 secondTask

    • 竞争锁 → 进入循环:while (!isFirstDone) → 第一次检查isFirstDone=false → 调用wait() → 释放锁,进入等待队列

    • 等待:直到 First 线程调用notifyAll()后,Second 线程被唤醒 → 重新竞争锁 → 检查isFirstDone=true → 退出循环

    • 执行:打印Second task started → 睡眠 1 秒 → 打印Second task completed → 设置isSecondDone=true → 调用notifyAll()

    3. Third 线程执行 thirdTask

    • 竞争锁 → 进入循环:while (!isSecondDone) → 第一次检查isSecondDone=false → 调用wait() → 释放锁,进入等待队列

    • 等待:直到 Second 线程调用notifyAll()后,Third 线程被唤醒 → 重新竞争锁 → 检查isSecondDone=true → 退出循环

    • 执行:打印Third task started → 睡眠 1 秒 → 打印Third task completed


    http://www.ppmy.cn/ops/165602.html

    相关文章

    Node.js 模块化概念详细介绍

    目录 模块化的概念 模块化的好处&#xff1a; 实现模块化 代码实现 1. 创建计算器模块 2. 使用计算器模块 3. 运行结果 总结 常见的Node.js核心模块 模块化的应用场景 Node.js 采用了模块化的设计&#xff0c;使得开发者能够将代码拆分成多个独立的模块&#xff0c;便…

    利用java实现数据分析

    1 问题 在日常生活中&#xff0c;对于数据的处理&#xff0c;为了使数据更加直观&#xff0c;我们可以使用柱状图&#xff0c;饼图&#xff0c;折线图等来呈现&#xff0c;同时也可以对数据直接进行一些处理&#xff0c;那怎样用java来处理这类问题呢&#xff1f; 2 方法 代码清…

    机器视觉工程师红外相机的选择:红外长波工业相机和短波红外工业相机玄机大总结

    红外长波(LWIR)和短波(SWIR)工业相机在原理、应用场景和技术特点上有显著差异。以下是它们的对比分析: 1. 波长范围与成像原理 2. 技术特点 3. 典型应用场景 4. 优缺点对比 LWIR优势: 无需光照,适用于完全黑暗环境。 直接反映物体温度分布。 对烟雾、灰尘穿透能力强。…

    大型语言模型与强化学习的融合:迈向通用人工智能的新范式

    1. 引言 大型语言模型&#xff08;LLM&#xff09;在自然语言处理领域的突破&#xff0c;展现了强大的知识存储、推理和生成能力&#xff0c;为人工智能带来了新的可能性。强化学习&#xff08;RL&#xff09;作为一种通过与环境交互学习最优策略的方法&#xff0c;在智能体训…

    基于Vue3的流程图绘制库

    流程图组件的革命者&#xff0c;带你探索无限可能Vue Flow 基于Vue3的流程图绘制库

    传球游戏(蓝桥云课)

    题目描述 上体育课的时候&#xff0c;小蛮的老师经常带着同学们一起做游戏。这次&#xff0c;老师带着同学们一起做传球游戏。 游戏规则是这样的&#xff1a;nn 个同学站成一个圆圈&#xff0c;其中的一个同学手里拿着一个球&#xff0c;当老师吹哨子时开始传球&#xff0c;每个…

    【面试题系列】 Redis 核心面试题(二)答案

    本文主要介绍Redis 的面试题&#xff0c;涵盖持久化、集群、缓存策略、事务等方面 一、持久化机制 1. RDB 与 AOF 的核心区别及适用场景&#xff1f; 答案&#xff1a; 特性RDBAOF存储内容内存快照&#xff08;二进制文件&#xff09;写命令日志&#xff08;文本格式&#x…

    如何将ipynb文件转换为pdf文件

    事情起因&#xff1a; 基本我所有的code以及代码注释&#xff0c;以及出图说明都统一放在jupyter notebook中&#xff0c; 代码注释&#xff0c;或者文档说明&#xff0c;实际上就是markdown所做的那一切&#xff0c;都是在markdown中写的&#xff1b; 代码的话&#xff0c;…