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

Java内功修炼(2)——线程安全三剑客:synchronized、volatile与wait/notify

1.线程安全

1.1 概念&示例

概念:指在多线程环境下,某个代码、函数或对象能够被多个线程同时调用或访问时,仍能保持正确的行为和数据一致性。简单来说,线程安全的代码在多线程环境下运行可靠,不会因线程间的交互而产生不可预测的结果

示例:

public class ThreadDemo {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(()->{for (int i = 0; i < 500000; i++) {count++;}});Thread thread2 = new Thread(()->{for (int i = 0; i < 500000; i++) {count++;}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + count);//每次执行的结果都不一致}
}

按照上述代码的逻辑,期望得到的结果是1000000,但实际计算的结果与期望值不一致

线程不安全:当多个线程同时访问或修改共享资源时,由于缺乏适当的同步机制,可能导致程序行为不可预测、数据损坏或错误结果的现象

1.2 线程不安全的原因

  • 1.访问修改共享变量:当多个线程同时读写同一内存区域时,可能导致数据状态不一致
  • 2.原子性:原子性指一个操作是不可分割的单元,要么完全执行,要么完全不执行。如果操作不是原子的,在并发环境下,线程可能被中断在中间状态,导致部分修改在这里插入图片描述
  • 3.内存可见性:在多线程编程中,每个线程都有自己的工作内存(本地内存),用于存储共享变量的副本。由于CPU缓存、编译器优化等因素,操作可能只发生在工作内存中,而不是直接在主内存中进行,导致程序行为不符合预期在这里插入图片描述
  • 4.指令重排序:是计算机处理器或编译器为了提高程序执行效率,对指令执行顺序进行优化的一种技术。在保证程序最终结果正确的前提下,允许指令的执行顺序与代码编写的顺序不一致。但可能导致多线程下的逻辑错误在这里插入图片描述
  • 5.线程之间抢占式执行:这是操作系统层面的调度机制,线程的执行顺序是随机的和不可预测的。操作系统可能随时中断一个线程(抢占),切换到另一个线程执行。一般不轻易改变,当引发线程安全时优先考虑前4个原因

共享变量访问修改是线程安全问题的前提,但需结合2/3/4才会引发问题;抢占式执行是线程调度的特性,无法避免

2.synchronized关键字

2.1 概念

synchronized(监视器锁monitor lock):用于实现线程同步,确保多线程环境下对共享资源的访问安全。通过加锁机制,防止多个线程同时访问同步块代码或对象,避免数据不一致问题

2.2 特性

2.2.1 原子性

确保了代码块的原子性,即被同步的代码块在执行过程中不会被其他线程中断。这意味着在一个线程执行完整个同步块之前,其他线程无法进入同一个同步块,从而保证了操作的完整性在这里插入图片描述

public class ThreadDemo {//锁对象private static final Object locker = new Object();private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(()->{for (int i = 0; i < 500000; i++) {synchronized (locker) {count++;}}});Thread thread2 = new Thread(()->{for (int i = 0; i < 500000; i++) {synchronized (locker) {count++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count = " + count);//1000000}
}

2.2.2 内存可见性

  • 获取锁时:线程会将工作内存中的变量副本失效,强制从主内存重新读取最新值
  • 释放锁时:线程会将工作内存中修改过的变量刷新到主内存 在这里插入图片描述

这种机制确保了共享变量的修改对所有线程立即可见

2.2.3 互斥性

确保在同一时间只有一个线程可以进入被同步的代码块或方法,这意味着当一个线程进入同步块或方法时,其他试图进入同一同步块的线程会被阻塞,直到第一个线程退出同步块
在这里插入图片描述

2.2.4 可重入性

synchronized关键字是可重入的,这意味着如果一个线程已经持有某个对象的锁,那么它可以再次获取该对象的锁,而不会被阻塞

  • 可重入锁通常会维护一个计数器,记录当前线程获取锁的次数。每次获取锁时,计数器加一;释放锁时,计数器减一。当计数器为零时,锁才真正被释放
public class Reentry_Lock {public static void main(String[] args) {Object locker = new Object();Thread thread = new Thread(()->{synchronized (locker){System.out.println("第一层锁");synchronized (locker){System.out.println("第二层锁");}}});thread.start();}
}

2.3 类型

2.3.1 实例锁

作用于对象实例,每个对象实例拥有自己的锁。当一个线程访问对象的synchronized实例方法或代码块时,其他线程无法访问该对象的其他synchronized方法或代码块,但可以访问非synchronized方法或代码块

public class Example {// 实例方法锁public synchronized void instanceMethod() {// 同步代码}// 实例代码块锁public void anotherMethod() {synchronized (this) {// 同步代码}}
}

2.3.2 静态锁

作用于类的Class对象,所有实例共享同一把锁。当一个线程访问synchronized静态方法或代码块时,其他线程无法访问该类的其他synchronized静态方法或代码块,但可以访问非synchronized静态方法或代码块

public class Example {// 静态方法锁public static synchronized void staticMethod() {// 同步代码}// 静态代码块锁public static void anotherStaticMethod() {synchronized (Example.class) {// 同步代码}}
}

2.4 死锁

概念:指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。这种情况下,系统资源被占用,但程序无法继续运行在这里插入图片描述

死锁产生的必要条件:

  • 1.互斥条件(Mutual Exclusion):至少有一个资源必须处于非共享模式,即一次只能被一个线程使用。如果另一个线程请求该资源,那么请求线程必须等待,直到该资源被释放
  • 2.请求与保持条件(Hold and Wait):一个线程已经持有至少一个资源,并且正在等待获取其他被其他线程占用的资源
  • 3.不可剥夺条件(No Preemption):资源一旦被分配给某个线程,就不能被强制性地剥夺,只能由占有该资源的线程自行释放
  • 4.环路等待条件(Circular Wait):存在一个线程资源的循环等待链,其中每个线程都在等待下一个进程所持有的资源

预防死锁:通过破坏死锁的四个必要条件之一,可以预防死锁的发生

  • 1.破坏互斥条件:尽量使用可共享的资源
  • 2.破坏占有且等待:一次性申请所有需要的资源,避免部分持有
  • 3.破坏非抢占条件(不建议):允许系统强行剥夺某些进程已占有的资源,分配给其他进程。这种方法可能导致进程执行的不稳定性
  • 4.破坏循环等待条件:对资源进行排序,按固定顺序申请资源

3.volatile

3.1 概念

volatile:是编程语言中的关键字,用于修饰变量,告知编译器该变量可能被意外修改。其核心作用是防止编译器优化导致的数据不一致问题(在Java中仅能修饰成员变量)

3.2 特性

3.2.1 内存可见性

对volatile变量的每次访问都会强制从主内存读取,每次修改都会立即写回主内存

public class demo_volatile {//每次访问都会强制从主内存读取,每次修改都会立即写回主内存//去除volatile关键字会导致thread1线程在访问num时不从主内存读取public static volatile int num = 0;public static void main(String[] args) {//thread1线程的生命周期掌握在thread2手中Thread thread1 = new Thread(()->{while (num == 0){}System.out.println("Over thread1");});Thread thread2 = new Thread(()->{System.out.println("请输入一个整数");Scanner in = new Scanner(System.in);num = in.nextInt();});thread1.start();thread2.start();}
}

3.2.2 禁止指令重排序

在多线程场景下,指令重排序可能会导致线程间数据同步问题。volatile变量通过插入内存屏障(Memory Barrier)来禁止重排序

  • 读操作前插入“LoadLoad”屏障,读操作后插入“LoadStore”屏障在这里插入图片描述
  • 写操作前插入“StoreStore”屏障,写操作后插入“StoreLoad”屏障在这里插入图片描述
public class FixedReorderingExample {int a = 0;//普通变量int b = 0;//普通变量volatile boolean flag = false;//标志变量使用volatile// 写线程方法public void writer() {a = 1;b = 1;flag = true;//volatile 写,插入写屏障:确保flag写操作在a、b写操作之后}boolean demo = true// 读线程方法public void reader() {if (flag) {//volatile 读,插入读屏障:确保println读操作不会再if读操作之前int r1 = a;int r2 = b;System.out.println("r1: " + r1 + ", r2: " + r2);//总是输出 r1: 1, r2: 1}}
}

3.2.3 不保证原子性

volatile不保证操作的原子性,多线程环境下仍需结合锁或原子操作

4.wait/notify

概念:wait()和notify()是Java中用于线程间通信的机制,属于Object类的方法。它们必须在同步代码块(如synchronized块)中使用,否则会抛出IllegalMonitorStateException

  • wait(): 让当前线程进入等待状态,释放锁,直到其他线程调用notify()或notifyAll()唤醒它
  • notify(): 随机唤醒一个等待该对象锁的线程
  • notifyAll(): 唤醒所有等待该对象锁的线程
public class Demo {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread thread1 = new Thread(()->{//thread1拿到锁synchronized (locker) {System.out.println("thread1线程wait之前");try {//thread1释放锁,进入waiting状态,等待被唤醒locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("thread1线程wait之后");}});Thread thread2 = new Thread(()->{//thread1进入waiting之后,thread2拿到锁synchronized (locker){System.out.println("thread2线程notify之前");//虽然notify执行之后thread1被唤醒了,但此时仍处于thread2的synchronized中//同一对象才能唤醒locker.notify();System.out.println("thread2线程notify之后");}});thread1.start();Thread.sleep(1000);thread2.start();}
}

wait与sleep的区别

  • 概念
    • wait是Object类的方法,用于线程间的通信,必须配合synchronized使用,调用wait的线程会释放锁
    • sleep是Thread类的静态方法,用于暂停当前线程的执行,调用sleep的线程不会释放锁
  • 锁的释放行为差异
    • wait会释放当前线程持有的锁,允许其他线程获取该锁并执行同步代码块,这一特性使得wait适用于多线程协作的场景
    • sleep不会释放任何锁,即使线程休眠,其他线程也无法获取该线程持有的锁,这可能导致死锁或性能问题
  • 唤醒机制
    • wait需要通过notify或notifyAll主动唤醒,否则线程会一直等待(可以设置最大等待时间),唤醒后线程需要重新获取锁才能继续执行
    • sleep无需外部唤醒,到达指定时间后自动恢复,恢复执行的线程直接从sleep调用处继续执行
http://www.lryc.cn/news/626746.html

相关文章:

  • Web前端调试与性能优化,Charles抓包工具的高效应用
  • YOLOv11 到 C++ 落地全流程:ONNX 导出、NMS 判别与推理实战
  • Vue透传 Attributes(详细解析)2
  • 极其简单二叉树遍历JAVA版本
  • CMake1:概述
  • 查看磁盘占用情况和目录大小
  • 企业架构及战略价值
  • 如何让FastAPI任务系统在失败时自动告警并自我修复?
  • 从零实现自定义顺序表:万字详解 + 完整源码 + 图文分析
  • 从“怀疑作弊”到“实锤取证”:在线面试智能监考重塑招聘公信力
  • 河南萌新联赛2025第六场 - 郑州大学
  • 数据库优化提速(一)之进销存库存管理—仙盟创梦IDE
  • 开源模型应用落地-安全合规篇-深度合成隐式标识的技术实现(五)
  • 无人机感知系统详解
  • Tomcat 性能优化终极指南
  • C++ std::sort的应用总结
  • Vue2封装Axios
  • Google Chrome v139.0.7258.139 便携增强版
  • 嵌入式音频开发(3)- AudioService核心功能
  • 嵌入式开发学习———Linux环境下网络编程学习(四)
  • 04-认证授权服务开发指南
  • 读《精益数据分析》:规模化(Scale)—— 复制成功,进军新市场
  • Kafka如何保证消费确认与顺序消费?
  • Python爬虫实战:研究dark-fantasy,构建奇幻文学数据采集分析系统
  • GitHub宕机生存指南:从应急协作到高可用架构设计
  • BM25 vs TF-IDF:经典文本检索方法的对比
  • 《算法导论》第 34 章 - NP 完全性
  • RK Android14 新建分区恢复出厂设置分区数据不擦除及开机动画自定义(二)
  • 细说数仓中不同类型的维度
  • 哈希:字母异位词分组