共享模型之不可变

目录

问题的提出

思路 - 同步锁

思路 - 不可变

不可变设计

final 的使用

保护性拷贝

设置 final 变量的原理

获取final变量的原理


问题的提出

日 期 转 换 的 问 题

public class ThreadText {
    public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    System.out.println(sdf.parse("1951-04-21"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

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

共享模型之不可变_第1张图片

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

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

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

思路 - 同步锁

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

public class ThreadText {
    public static void main(String[] args) throws InterruptedException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                synchronized (sdf) {
                    try {
                        System.out.println(sdf.parse("1951-04-21"));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

思路 - 不可变

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

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

共享模型之不可变_第2张图片

 共享模型之不可变_第3张图片

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

不可变设计

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

public final class String 
    implements java.io.Serializable, Comparable, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    // ...
}

final 的使用

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

保护性拷贝

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

共享模型之不可变_第4张图片

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

共享模型之不可变_第5张图片

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

设置 final 变量的原理

public class TestFinal {
    final int a = 20;
}

字节码

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

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

 获取final变量的原理

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

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

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

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

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

你可能感兴趣的:(JUC并发编程,java,开发语言)