共享模型之不可变

简介: 共享模型之不可变

问题的提出

日 期 转 换 的 问 题

1. public class ThreadText {
2. public static void main(String[] args) throws InterruptedException {
3. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
4. for (int i = 0; i < 10; i++) {
5. new Thread(() -> {
6. try {
7.                     System.out.println(sdf.parse("1951-04-21"));
8.                 } catch (ParseException e) {
9.                     e.printStackTrace();
10.                 }
11.             }).start();
12.         }
13.     }
14. }

有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果,例如:

上述代码会出现线程安全问题,因为SimpleDateFormat对象并不是线程安全的,即多个线程同时调用SimpleDateFormat实例的同一个方法时可能会产生冲突。

在多线程环境下,如果多个线程同时访问SimpleDateFormat对象的同一个方法,那么会出现问题:

线程安全问题:SimpleDateFormat是非线程安全的类,如果多个线程同时访问,可能会导致解析错误或者计算结果混乱

思路 - 同步锁

这样虽能解决问题,但带来的是性能上的损失,并不算很好:  

1. public class ThreadText {
2. public static void main(String[] args) throws InterruptedException {
3. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
4. for (int i = 0; i < 50; i++) {
5. new Thread(() -> {
6. synchronized (sdf) {
7. try {
8.                         System.out.println(sdf.parse("1951-04-21"));
9.                     } catch (Exception e) {
10.                         e.printStackTrace();
11.                     }
12.                 }
13.             }).start();
14.         }
15.     }
16. }

思路 - 不可变

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:

1. public class ThreadText {
2. public static void main(String[] args) throws InterruptedException {
3. DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
4. for (int i = 0; i < 10; i++) {
5. new Thread(() -> {
6. LocalDate date = dtf.parse("2023-06-15", LocalDate::from);
7.                 System.out.println(date);
8.             }).start();
9.         }
10.     }
11. }

不可变对象,实际是另一种避免竞争的方式。

不可变设计

另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素

1. public final class String
2. implements java.io.Serializable, Comparable<String>, CharSequence {
3. /** The value is used for character storage. */
4. private final char value[];
5. /** Cache the hash code for the string */
6. private int hash; // Default to 0
7. // ...
8. }

final 的使用

发现该类、类中所有属性都是 final 的 属性用 final 修饰保证了该属性是只读的,不能修改 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

保护性拷贝

但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是 如何实现的,就以 substring 为例:

发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出 了修改:

结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避 免共享的手段称之为【保护性拷贝(defensive copy)】

设置 final 变量的原理

1. public class TestFinal {
2. final int a = 20;
3. }

字节码

1. 0: aload_0
2. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
3. 4: aload_0
4. 5: bipush 20
5. 7: putfield #2 // Field a:I
6. <-- 写屏障
7. 10: return

发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况

获取final变量的原理

首先,final变量会被显式初始化或在构造函数中初始化。在编译期间,final变量的值就已经确定并被存储在常量池中,而不是在运行时通过初始化代码计算得到。

因为final变量的值已经确定,Java虚拟机在读取final变量的值时,会直接从常量池中读取,而不是从堆内存中读取。因此,获取final变量的过程可以看作是一个常量折叠过程:编译器在编译期间把所有引用final变量的地方替换成该变量的值。

这种优化方式的好处在于可以加快程序的执行速度,同时也可以避免线程安全问题,因为final变量的值不可修改,也不需要进行同步处理。

除此之外,final变量也具有内存可见性,即使在多线程环境下,其值也能够保证对其他线程是可见的。这是因为,当一个线程将final变量的值写入主内存后,其他线程读取该变量时,会从主内存加载该变量的值,而不是从自己线程内部的缓存中加载,从而保证了线程之间final变量值的可见性。

综上,获取final变量的原理是通过常量池来实现的,其值在编译期间被确定并存储在常量池中。这种方式具有较高的执行效率和线程安全性,同时也保证了final变量值在多线程环境下的可见性。


目录
打赏
0
0
0
0
64
分享
相关文章
实际应用中如何有效地避免伪共享问题
实际应用中如何有效地避免伪共享问题
管理模型
【6月更文挑战第15天】管理模型。
60 8
|
9月前
|
共享模型之管程(5)
共享模型之管程
37 0
|
9月前
|
共享模型之管程(3)
共享模型之管程
65 0
阿里云共享流量包和共享带宽区别对比及选择方法
阿里云共享带宽和共享流量包有什么区别?将按流量计费的ECS、EIP、SLB和NAT网关加入到共享带宽中,可以实现带宽复用;而共享流量包是用来抵扣按流量计费的ECS、EIP、SLB和NAT网关产生的流量。
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等