欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 焦点 > 【JUC】共享模型之不可变

【JUC】共享模型之不可变

2025/5/26 8:11:08 来源:https://blog.csdn.net/weixin_62533201/article/details/148206586  浏览:    关键词:【JUC】共享模型之不可变

1. 本章内容

本章聚焦于 “不可变” 相关内容,之前提到过线程安全模型针对于 “共享变量” 的多线程读写操作,因此只需要在源头上遏制变量的共享即可实现类的线程安全。本章内容概括如下:

  • 不可变类的使用:探究 SimpleDateFormat 类和 DateTimeFormatter 的线程安全性
  • 不可变类的设计:探究不可变类线程安全的本质原因以及享元模式的使用
  • final 关键字的原理以及无状态类:探究 final 关键字背后的原理以及无状态类的概念

2. 日期类带来的问题

先来看看在多线程环境下使用 SimpleDateFormat 类所导致的问题,相关测试代码如下:

/*** 验证SimpleDateFormat的线程不安全问题* @author ricejson*/
@Slf4j
public class TestSimpleDateFormat {public static void main(String[] args) throws ParseException {// 创建sdf对象SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");// 创建多个线程并发访问for (int i = 0; i < 10; i++) {new Thread(() -> {try {log.debug("{}", sdf.parse("2025-05-21"));} catch (ParseException e) {e.printStackTrace();}}).start();}}
}

在上述代码中,我们创建了一个 SimpleDateFormat 日期格式化类,然后在多个线程下并发调用 parse 方法,观察实验结果如下图所示:

可以发现在多线程环境下调用 parse 方法会抛出异常,表明 SimpleDateFormat 类不是线程安全的,想要解决这个问题可以通过手动加锁,但是性能开销太大,实际上我们完全可以通过保证类的属性不可变(不存在并发修改)保证线程安全,因此 JDK8 也提供了线程安全的日期类可供使用,即 DateTimeFormatter 类,改造代码如下:

/*** 解决思路:不可变类DatetimeFormatter* @author ricejson*/
@Slf4j
public class TestDateTimeFormatter {public static void main(String[] args) {// 1. 创建DateTimeFormatter对象DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");// 2. 创建多个线程并发访问for (int i = 0; i < 10; i++) {new Thread(() -> {log.debug("{}", dtf.parse("2025-05-21"));}).start();}}
}

代码运行结果如下图所示:

表明在多线程环境下使用 DateTimeFormatter 能够保证线程安全,实际上我们查看 DateTimeForamtter 类的源码可以看到以下官方描述:

3. 不可变类的设计

3.1 设计原理

在 Java 标准库当中,提供了非常多不可变类的实现,比如包装类都是不可变类,比如 Integer、Long、Byte、Short、Character、Boolean 等等,再或者 String 类也是常见的不可变类,现在我们就通过查看相关类的源码来探究不可变类相关设计理念

3.1.1 final 关键字

我们先来回顾 final 关键字的作用:

  • 修饰变量:表明该变量不可被二次修改
  • 修饰成员方法:表明该方法不可被子类重写
  • 修饰类:表明该类无法被继承

我们通过观察Long类的源码,可以发现大量运用到了 final 关键字:比如在类上

目的是防止通过子类继承的方式破坏父类的不可变性特点,另外我们还可以发现其属性以及内部类的属性也是用 final 关键字修饰的:

3.1.2 保护性拷贝

我们再来看看不可变类String的相关源码,其中大量运用到了保护性拷贝的特点,比如来看substring方法

我们可以发现,内部创建了一个新的字符串对象,我们继续追踪观察是否修改了内部的value字符数组

可以发现,内部实际上调用了Arrays.copyOfRange复制了一个新的字符数组返回,这种在设计到操作但是不修改自身而是通过拷贝返回的方式就被称为 “保护性拷贝” ,在不可变类中涉及到大量该操作

3.2 享元模式

3.2.1 概念

前面我们提到过不可变类大量运用到“保护性拷贝”这种设计理念,但是带来的影响就是创建对象的频率太高,对于内存占用与 GC 开销是很大的,因此需要引入一种设计模式:享元模式

📖 享元模式:该模式出自 GoF(Gang Of Four)23种设计模式,英文名称为 FlyWeight Pattern,用于需要复用数量有限的一批对象

3.2.2 源码示例

我们直接来看享元模式的使用场景,在不可变类Long中就使用到了享元模式的思想,观察valueOf方法:

我们通过分析发现,内部类属性 cache 数组缓存了(-128 - 127) 范围内的数值,在 valueOf 方法当中,如果参数值在该范围内,直接通过缓存数组 cache 来获取,超过这个范围才会使用到“保护性拷贝”的方式创建新对象

3.2.3 线程池实战

现在我们就通过实战的方式实现一个自定义线程池来体会享元模式:

/*** 自定义连接池-享元模式* @author ricejson*/
@Slf4j
public class TestConnectionPool {public static void main(String[] args) {// 创建大小为2的连接池ConnectionPool pool = new ConnectionPool(2);// 创建5个线程并发执行for (int i = 0; i < 5; i++) {new Thread(() -> {// 占用连接Connection conn = pool.borrow();try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}// 归还连接pool.free(conn);}).start();}}
}@Slf4j
class ConnectionPool {// 连接池数目private final int poolSize;// 连接数组private Connection[] connections;// 状态数组(0-空闲,1-占用)private AtomicIntegerArray states;public ConnectionPool(int poolSize) {this.poolSize = poolSize;this.connections = new Connection[this.poolSize];for (int i = 0; i < this.poolSize; i++) {this.connections[i] = new MockConnection("conn" + i);}this.states = new AtomicIntegerArray(this.poolSize);}public Connection borrow() {// 如果连接数全部占用了怎么办??while (true) {for (int i = 0; i < this.poolSize; i++) {if (states.get(i) == 0) {// 可以占用该连接// 这里存在并发安全问题(借助cas机制解决)if (states.compareAndSet(i, 0, 1)) {log.debug("borrow conn: {}", connections[i]);return connections[i];}}}// 阻塞synchronized (this) {try {log.debug("waiting...");wait();} catch (InterruptedException e) {e.printStackTrace();}}}}public void free(Connection conn) {// 谁占用的谁释放(前面已经保证只有一个线程占有连接)for (int i = 0; i < this.poolSize; i++) {if (connections[i] == conn) {states.set(i, 0);log.debug("free conn: {}", conn);// 唤醒所有阻塞线程synchronized (this) {notifyAll();}break;}}}
}class MockConnection implements Connection {private String name;public MockConnection(String name) {this.name = name;}@Overridepublic String toString() {return "MockConnection{" +"name='" + name + '\'' +'}';}// ... 忽略重写方法
}

相关注意要点如下:

  • 线程池状态数组 states 需要使用原子数组来保证并发线程安全
  • 通过 wait、notify 机制防止 CPU 空转无效循环运行
  • 通过享元模式创建一定量的连接,使用完后放回池中以便复用

但是上述代码还是可以优化的,比如读者可以考虑引入以下特性:

  1. 连接数的动态增长与收缩
  2. 连接的保活特性
  3. 超时处理机制(比如连接占用时间过长则直接断开)

4. 无状态类

在学习 servlet 阶段,我们都知道 Servlet 类是没有成员变量的,这可以保证线程安全,因为成员变量可以看做是类的状态信息,因此我们把没有成员变量的类称为“无状态类”,无状态类也是线程安全的

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词