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

Java线程安全类设计思路总结

记录一下怎么在Java中设计一个线程安全类。

1. 什么是线程安全类?

所谓线程安全类,是指当多个线程并发访问某个类的同一个实例时,无需额外同步措施,也能保证行为正确、结果一致。换句话说,这个类在多线程环境下不会出现竞态条件(Race Condition)或数据不一致的问题。

注意:只有在类的对象可能被多个线程同时访问的情况下,才需要考虑线程安全。如果某个类在实际使用中不会被并发访问,那就没必要为了“线程安全”而引入额外的同步开销。

1.1 竞态条件

这里补充一下对这个术语的说明。从定义上来说,竞态条件(Race Condition)是指程序中某段逻辑的执行结果依赖于多个线程执行的相对时序。换句话说,程序的行为取决于“谁先执行”,而不是业务逻辑本身。这种不确定性,往往会导致程序出现难以复现、难以调试的 Bug。

Race condition的两种常见模式

  1. Check-Then-Act(检查再执行)
    这是最常见的一类模式,典型结构是:先判断某个条件,再基于判断结果执行某个操作。但在并发场景下,检查和执行之间存在“时间窗”,其他线程可能在这段间隙中修改了状态,导致最终行为和预期不符。
  2. Read-Modify-Write(读取-修改-写入)
    在Java中,对变量的更新并不是原子操作,而是拆分成读取 → 修改 → 写入三个步骤。如果多个线程同时对同一个变量执行这三步中的任意一环,就可能造成竞态,尤其当至少一个线程涉及“修改”时,比如下面这个例子。
public class UnsafeClass {private int value = 0;public void incre() {// 非原子操作value++;}
}
/*
假设初始 value = 0,两个线程A和B同时调用incre()可能会发生以下情况:
1. 线程A读取value(0)
2. 线程B读取value(0)
3. 线程A对value加1并写入value=1
4. 线程BA对value加1并写入value=1
最终:在并发下,两个线程执行这段代码会互相覆盖对方的写入,导致数值错误。
*/

3. 构建线程安全类的策略

3.1 无状态类

所谓无状态类,是指这个类不包含任何实例字段,也就意味着它不维护任何可以被多个线程同时访问和修改的状态。换句话说,它的行为完全由传入的参数决定,而不是依赖于内部数据。正因为“无状态”,所以它天然是线程安全的。

// 这个类没有任何字段,方法只是执行操作,不依赖也不修改类的内部状态,多个线程调用它不会有任何冲突。
public class MathUtils {public int add(int a, int b) {return a + b;}
}
3.2 不可变类

不可变性是实现线程安全的一种十分有效的方式。原理非常简单:如果一个对象在创建之后就不能再被修改,那自然也就没有“并发修改”的问题了。正所谓:“只读,最安全”。
Java 中最有名的不可变类就是 String。你可以放心地在多个线程中共享字符串实例,而不必担心线程安全问题——这是因为 String 被final关键字修饰,在构造后就无法更改(通常的更改会创建一个新的String对象)。

// 一个例子
public final class User {private final String username;private final int age;public User(String username, int age) {this.username = username;this.age = age;}public String getUsername() { return username; }public int getAge() { return age; }//修改返回一个新对象,而不是修改现有状态public User updateAge(int newAge) {return new User(this.username, newAge);}
}
3.3. 封装和同步

对于字段值需要变化的类,可以通过适当的封装 + 同步来保障线程安全。主要是以下两个步骤

3.3.1 两步操作保障线程安全

Step1:将字段设为private

字段设置成public是违背线程安全的,比如下面的例子。

public class UnsafeClass {// 可直接访问,可被任何线程修改,非线程安全public int value; 
}

Step2:识别非原子性操作并增加同步措施

类字段设置成私有后,还需要确保修改字段的方法是原子性的。

public class SafeClass {private int value; // 使用synchronized加锁同步,确保只有一个线程可以在特定实例上执行这个方法public synchronized void incre() {value++;}
}
3.3.2 对同步的进一步说明
  1. volatile关键字

如果你的程序只需要确保一个线程所做的更改对其他线程立即可见,可以理解成“部分同步”,那可以将这个字段用volatile关键字修饰。但是这只能解决可见性问题,不能解决原子性问题。
2. 粗粒度锁 vs 细粒度锁

  • 粗粒度锁:锁的范围大,通常保护整个对象、整个方法或一大段代码。
  • 细粒度锁:锁的范围小,尽量只锁住真正需要同步的那部分代码或数据。
  • 主要区别如下表,实际开发需要根据需求在简单性和性能要求之间取得平衡
对比项粗粒度锁细粒度锁
并发性能冲突大,容易成为瓶颈更灵活,允许更高并发和吞吐量
实现复杂度实现简单,不易出错实现复杂,需要精细设计,容易出现死锁等问题
资源开销管理成本低,但线程等待成本可能高管理成本高,但可降低线程之间的互斥冲突
3.4 线程安全库

在构建线程安全类时,不一定非得自己造锁。Java 的 java.util.concurrent 包已经为我们提供了大量高性能、线程安全的工具类,它们通常通过细粒度锁机制或无锁算法,帮助我们提高并发性能、降低开发复杂度。

/* 
SafeUserManager没有显式加锁,却是线程安全的。
ConcurrentHashMap 内部采用了分段锁或 CAS 技术,支持高并发访问。
AtomicInteger 通过底层 Unsafe 类实现无锁自增操作,避免了传统的 synchronized。
*/
public class SafeUserManager {// java.util.concurrent包提供的线程安全HashMapprivate final ConcurrentHashMap<String, User> userMap = new ConcurrentHashMap<>();// 线程安全的原子类计数器private final AtomicInteger userCount = new AtomicInteger();public void register(String id, User user) {userMap.put(id, user);userCount.incrementAndGet();}
}

Java中常用的线程安全组件

类型代表类场景用途
集合类ConcurrentHashMapCopyOnWriteArrayList读多写少、读写并发访问
原子变量AtomicIntegerAtomicLongAtomicReference原子计数器、状态控制
并发队列LinkedBlockingQueueArrayBlockingQueue生产者-消费者模型,线程通信
同步工具类CountDownLatchCyclicBarrierSemaphore线程协调、限流、批处理等场景
3.5 线程封闭

当前线程的变量不与其他线程共享,只在自己的线程中使用。也就是每个线程都有自己的副本

Java中使用线程封闭技术的两个典型例子

  1. Swing 的可视化组件和数据模型对象:这二者都不是线程安全的,Swing 通过将它们封闭到 Swing 的事件分发线程中来实现线程安全性;为了进一步简化对 Swing 的使用,Swing 还提供了 invokeLater 机制,用于将一个 Runnable 实例调度到事件线程中执行。
  2. JDBC 的 Connection 对象:在典型的服务器应用程序中,线程从连接池中获得一个 Connection 对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。在这个过程中,大多数请求(例如 Servlet 请求 或 EJB 调用)都是由单个线程采用同步的方式来处理,并且在 Connection 对象返回之前,连接池不会再将它分配给其他线程。也就是说,这种连接管理模式在处理请求时隐含地将 Connection 对象封闭在线程中。

一个传统的线程封闭工具是ThreadLocal,它为每个线程维护独立副本。但它的生命周期不透明(容易引起内存泄露)并且语义不清晰(值是全局静态字段绑定,容易被误用)。在Java 21中引入了ScopedValue作为是ThreadLocal的替代品,提供了更好的性能和更清晰的语义,特别是对于虚拟线程。

// ScopedValue用法示例
private static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();public static void main(String[] args) {ScopedValue.where(CURRENT_USER, "Alice").run(() -> {// 输出:Current user is AlicelogCurrentUser(); });
}private static void logCurrentUser() {System.out.println("Current user is " + CURRENT_USER.get());
}
3.6 防御性拷贝

简单来说,就是在将可变对象暴露给外部之前,先拷贝一份副本,这样即使外部修改了返回的对象,也不会影响类的内部状态。

public final class Person {private final Date birthDate;public Person(Date birthDate) {// 场景1:构造函数中防御性拷贝this.birthDate = new Date(birthDate.getTime());}public Date getBirthDate() {// 场景2:getter方法中防御性拷贝return new Date(birthDate.getTime());}
}

如果不使用防御性复制,调用者即使在将Date对象传递给你的类后也可以修改它,同时破坏了封装性和线程安全性。

4. 总结

在 Java 中构建线程安全的类,关键在于理解并发访问可能带来的风险,并根据类的使用场景选择合适的防护策略。以下是对前面所讲内容的总结。

  • 无状态类:从根源上消除共享状态,天然线程安全。
  • 不可变类:对象一旦创建即不可更改,避免并发修改带来的不确定性。
  • 封装 + 同步:通过封装可变状态,并在必要处加锁,保护共享数据不被并发破坏。
  • 线程安全库:使用 ConcurrentHashMap、AtomicInteger 等工具类,简化并发控制。
  • 线程封闭(Thread Confinement):将数据限制在当前线程内部,避免共享。
  • 防御性拷贝:对传入和返回的可变对象进行复制,防止外部修改。
http://www.dtcms.com/a/313296.html

相关文章:

  • 深入理解Python的`__missing__`方法:动态处理字典中不存在的键: Effective Python 第18条
  • 网络规划与设计5个阶段内容
  • 大模型学习--第一天
  • Linux命令基础(上)
  • day 44 文件的规范书写与拆分
  • LCL滤波器及其电容电流前馈有源阻尼设计软件【LCLAD_designer】
  • 机器学习——决策树(DecisionTree)
  • 分享两个问题及其解决方法:发送AT没反应和wifi模块连接不上热点
  • Java设计模式之行为型模式(访问者模式)应用场景分析
  • MATLAB小波分析工具包进行时间序列的小波功率谱分析
  • 基于Matlab的深度学习智能行人检测与统计系统
  • FastAPI入门:安全性
  • 网安-逻辑漏洞-23登陆验证
  • 【系统编程】错误处理、读写缓冲区及位图
  • 文章分享---《Keil 再升级,修复了这些bug》
  • [自动化Adapt] 录制引擎
  • Nginx 相关实验(1)
  • C语言数据结构(7)贪吃蛇项目2.贪吃蛇项目实现
  • 分离还是统一,这是个问题
  • STM32F103_Bootloader程序开发13 - 巧用逆向拷贝,实现固件更新的“准原子”操作,无惧升级中的意外掉电
  • 时间空间复杂度
  • 高质量数据集|从武汉光谷《面向科技情报大模型的高质量数据集建设》招标项目谈起
  • 实现游戏排行榜
  • SpringBoot项目数据脱敏(自定义注解)
  • 关于corn
  • SpringAI无人机智能灌溉、本地化AI推理、分析气象站、分析球场草皮系统实践
  • Python操作Excel——从入门到精通
  • QML 将一个qml文件定义为公共的全局单例
  • 外设数据到昇腾310推理卡 之五 3403ATU
  • 【分析学】Hilbert 空间