当前位置: 首页 > article >正文

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

一.线程安全的定义

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

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

二.synchronized 关键字

1.作用

核心特性

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

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

2.用法

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

// 同步方法
public synchronized void syncMethod() {
    // 操作共享资源
}

// 同步代码块
public void syncBlock() {
    synchronized (lockObject) {
        // 操作共享资源
    }
}

三.什么是锁对象

1. synchronized 修饰静态方法

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

1.1示例代码

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 示例代码

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 修饰,是类变量,整个类共享这一个实例

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. 问题代码

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. 问题代码 

    public class VisibilityProblem {
        private static boolean flag = false;
    
        public static void main(String[] args) throws InterruptedException {
            // 修改线程:500ms后修改flag为true
            new Thread(() -> {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                flag = true;
                System.out.println("修改线程:flag已设为true");
            }).start();
    
            // 读取线程:循环检查flag
            new 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 的最新值,确保可见性。

    修改后的代码 

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

     2.使用 synchronized

    原理

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

    修改后的代码

    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为true
            new Thread(() -> {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (LOCK) { // 获取锁
                    flag = true;
                    System.out.println("修改线程:flag已设为true");
                }
            }).start();
    
            // 读取线程:循环检查flag
            new 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 轮)》:

    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.复杂案例

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

    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.kler.cn/a/584266.html

    相关文章:

  1. 计算机网络-网络存储技术
  2. MySql数据库等级考试学习分享2(Day5)
  3. 深度学习----激活函数
  4. 什么是SWAP虚拟内存?使用服务器如何开启SWAP虚拟内存
  5. vue启动 localhost无法访问
  6. 【Android】‘adb shell input text‘ 模拟器输入文本工具使用教程
  7. OpenCV(应用) —— 凸包检测的实战应用
  8. petalinux环境下给linux-xlnx源码打补丁
  9. 计算机视觉|具身智能技术详解:视觉-动作联合建模的原理与实践
  10. Linux动态监控系统
  11. 公路工程减碳对策匹配知识图谱问答系统
  12. SvelteKit 最新中文文档教程(2)—— 路由
  13. 文本转语音-音画适时推送rtsp并播放
  14. 基金交易系统的流程
  15. Windows软件插件-视频渲染器
  16. 硬件驱动——51单片机、LED、动态数码管
  17. 概率论的基本知识
  18. Java版本切换~Linux
  19. K8S学习之基础二十八:k8s中的configMap
  20. Java 大视界 -- Java 大数据在智慧文旅虚拟导游与个性化推荐中的应用(130)