Java并发基础

并发经验

在单核时代,如果程序只有 CPU 计算,而没有 I/O 操作的话,多线程不但不会提升性能,还会使性能变得更差,原因是增加了线程切换的成本

在多核时代,如果程序只有 CPU 计算,那创建的线程数最好是等于cpu的核数,不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。

最佳线程数的设计不仅需要考虑CPU核数,还可以考虑一个方法(请求)的 I/O耗时/(CPU耗时 + 上下文切换开销) 的比值。最佳线程数最终还是得靠压测来确定,在线程增加的过程中,应关注线程数是如何影响吞吐量和延迟的。一般来讲,随着线程数的增加,吞吐量会增加,延迟最开始不变,到线程数超过cpu物理核数后,吞吐量会继续增加,延迟也会缓慢增加(因为有上下文切换开销);但是当线程数增加到一定程度,吞吐量就会开始下降,延迟会迅速增加。这个时候基本上就是线程能够设置的最大值了。

处理器写volatile变量的流程

1、写操作带有Lock前缀命令,锁定共享内存,让其他处理器无法读写这块共享内存(让其他处理器无法读写自己的缓存行)
2、将处理器缓存行中的数据写回内存
3、在总线上广播信号,其他处理器嗅探到这个信号后设置自己缓存行中的数据无效
4、其他处理器后续对这个共享数据进行读时重新从内存中加载最新的数据到缓存行

这条规则要求在工作内存中,每次使用读volatile前都必须先从主内存刷新最新的值(即便这个值没有修改过),用于保证能看见其他线程对变量V所做的修改。

对32位及其以下类型的数据进行读写是原子,但对64位(long, double)类型的数据进行读写不一定是原子的,但如果这个64位(long, double)类型的数据是volatile的,那对其进行读写也是原子的。

性能:volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下volatile的总开销仍然要比锁来得更低。另外,由于写volatile变量需要锁内存,如果两个处理器(线程)同时写同一个volatile变量,其中一个线程在写的时候另一个线程需要等待,所以volatile变量的并发写性能会较差。

伪共享问题:处理器的缓存是以缓存行(Cache Line,比如64字节) 为单位存储的,当多线程修改两个不同的volatile变量,如果这两个变量恰好共享同一个缓存行,就会出现相互等待导致写性能降低,就像这两个线程在写同一个volatile变量一样。

synchronized

可重入、非公平、互斥锁

基本原理:JVM会为每个对象分配一个monitor,而同时只能有一个线程可以获得该对象monitor的所有权。在线程进入时通过monitorenter尝试取得对象monitor所有权,退出时通过monitorexit释放对象monitor所有权。锁的相关信息存放在对象头中的Mark Word。

synchronized重量级锁的对象指向一个ObjectMonitor对象,所有尝试加锁的线程先进入他的entrySet里面,去cas抢锁,更改state加1拿锁,执行完代码,释放锁state减1,cas抢锁,没有队列,属于非公平锁。wait的时候,线程进waitSet休眠,等待notify唤醒

重量级锁的阻塞和唤醒需要进入内核态完成上下文切换,有很大的性能开销,同时考虑到同步块中的代码都是在很短的时间内完成,因此引入轻量锁(通过自旋来完成锁竞争);轻量级锁如果自旋次数太多会白白浪费CPU,因此JDK5中引入默认自旋次数为10,JDK6中更是引入了自适应自旋,自旋太多会升级为重量级锁,锁升级后不会降级,避免无用的自旋;无论是轻量级锁还是重量级锁,在进入与退出时都要通过CAS修改对象头中的Mark Word来进行加锁与释放锁,但是大多数情况是同一线程多次获得锁,如果每次加锁和放锁都使用CAS有点浪费性能,为了减少使用CAS的次数,引入偏向锁,偏向锁的首次加锁需要使用CAS修改Mark Word中的线程ID,之后该线程再获取偏向锁时只需要比较对象头中的Mark Word的Thread ID是否与自己的一致,如果一致说明已经取得锁,不用再CAS了。偏向锁用完后线程不用主动释放,只有另一个线程需要获取偏向锁时才释放,偏向锁的释放是由JVM在全局安全点(这个时间点没有正在执行的字节码)将Mark Word从偏向锁状态改为无锁状态。

hashCode与锁机制

在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法(非用户自定义)第一次被调用时,JVM会生成对应的identity hash code值,并将该值存储到Mark Word中。后续如果该对象的hashCode()方法再次被调用则不会再通过JVM进行计算得到,而是直接从Mark Word中获取。只有这样才能保证多次获取到的identity hash code的值是相同的(以jdk8为例,JVM默认的计算identity hash code的方式得到的是一个随机数,因而我们必须要保证一个对象的identity hash code只能被底层JVM计算一次)。

对于轻量级锁,获取锁的线程的栈帧中有锁记录(Lock Record)空间,用于存储原始Mark Word的拷贝,官方称之为Displaced Mark Word,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code共存;对于重量级锁,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中也可以存储identity hash code的值,所以重量级锁也可以和identity hash code共存。
对于偏向锁,在线程获取偏向锁时,会用Thread ID和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法已经被调用过一次之后,这个对象还能被设置偏向锁么?答案是不能。因为如果可以的话,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。因此偏向锁不能和identity hash code共存。

HotSpot VM的锁实现机制是:
当一个无锁状态的对象已经计算过identity hash code,它就无法进入偏向锁状态;
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
原文链接:https://blog.csdn.net/Saintyyu/article/details/108295657

参考
https://blog.dreamtobe.cn/2015/11/13/java_synchronized/
https://www.cnblogs.com/aspirant/p/11470858.html
https://www.cnblogs.com/twoheads/p/10150063.html

happens-before规则:

(1)程序顺序规则:一个线程内,按照代码书写顺序,书写在前面的操作happens-before于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。这条规则保证重排序不会 改变单线程内的程序执行结果。
(2)synchronized锁定规则:对一个锁的解锁unlock,happens-before于后续对这个锁的加锁lock。
(3)volatile变量规则:对一个volatile变量的写,happens-before于 后续对这个volatile变量的读。
(4)传递规则:如果操作A happens-before 操作B,且操作B happens-before 操作C,那么操作A happens-before 操作C。
(5)线程启动规则:主线程A执行 threadB.start() 启动子线程B,那么主线程A的 threadB.start() 操作 happens-before于子线程B中的任意操作。子线程B能够看到主线程A在启动子线程B 前的操作。
(6)线程终结规则:如果主线程A执行 threadB.join() 等待子线程B执行结束并成功返回(或者主线程A通过Thread::isAlive()方法返回false发现线程B已经结束),那么子线程B中的任意操作 happens-before于主线程A的 threadB.join() 操作。主线程A在join后能够看到子线程B对共享变量的操作。
(7)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread::interrupted()方法检测到是否有中断发生。
(8)对象终结规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

synchronized如何实现happens-before?
synchronized通过插入内存屏障指令确保synchronized块内的指令不会被重排序到synchronized块最后的结束指令之后,并在synchronized块结束时像volatile变量一样,将处理器缓存中的内容刷到内存,同时让其他处理器缓存中的共享内存的缓存行失效。

final变量的重排序规则

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。JMM禁止编译器把final域的写重排序到构造函数之外,编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障 禁止处理器把final域的写重排序到构造函数的return之外。

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被 正确初始化过了,而对象的普通域不具有这个保障。当其他线程看到对象引用时,很可能对象还没有构造完成(对普通域的写操作被重排序到构造函数外,此时初始值还没有写入普通域,普通域此时是0或null)。

如果对象的final域是引用类型,增加了如下约束:
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

public class FinalReferenceExample {
    final int[] intArray; // final是引用类型,引用一个int型的数组对象
    static FinalReferenceExample obj; 
    public FinalReferenceExample () { // 构造函数
        intArray = new int[1]; // 1
        intArray[0] = 1; // 2 
        // 在构造函数return之前,上面的语句1和2必须执行完
    } 
    public static void writerOne () { // 线程A先执行writerOne
        obj = new FinalReferenceExample (); // 3 
    } 
    public static void writerTwo () { // 然后线程B执行writerTwo
        obj.intArray[0] = 2; // 4 
    } 
    public static void reader () { // 最后线程C执行reader,线程C至少能看到语句1和2的结果,但可能看不到语句4的结果
        if (obj != null) { // 5
            int temp1 = obj.intArray[0]; // 6
        } 
    }
}

对上面的示例程序,假设首先线程A执行writerOne()方法,然后线程B执行 writerTwo()方法,最后线程C执行reader()方法。JMM可以确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入,即C至少能看到数组下标0的值为1。而写线程B对数组元素的写入,读线程C可能看得到,也可能看不到。JMM不保证线程B的写入对读线程C可见。

但是final域的重排序规则要生效还有一个条件,在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是被构造对象的引用不能在构造函数中“逸出”。

public class FinalReferenceEscapeExample {
    final int i; 
    static FinalReferenceEscapeExample obj; 
    public FinalReferenceEscapeExample () {
        i = 1; // 1 写final域
        obj = this; // 2 this引用在此"逸出" 
        // 注意语句1和2的顺序可以重排序
    } 
    public static void writer() { // 线程A先执行writer()方法
        new FinalReferenceEscapeExample (); 
    } 
    public static void reader() { // 然后线程B执行reader()方法
        if (obj != null) { // 3
            int temp = obj.i; // 4 线程B可能看到变量i的值为0
        } 
    }
}

假设线程A先执行writer()方法,然后线程B执行reader()方法。这里的操作2使得对象还未完成构造前就为线程B可见,此时线程B可能看到final域初始化前的值,也就是线程B可能看到变量i的值为0

final引用的重排序规则可以通过String来理解,正是因为String的成员char value[]是final的,保证其他线程看到的String的值一定是初始化以后的,且String的值不会发生改变。

public final class String {
    /** The value is used for character storage. */
    private final char value[];
    ......
    // 在String的构造函数中完成对value[]的赋值和value[]的成员(数组元素)的赋值
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
}
public class Arrays {
    public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
}

单例延迟初始化

  • 原始方案 没有使用任何同步技术 有问题
public class UnsafeLazyInitialization {
    private static Instance instance;
    public static Instance getInstance() {
        if (instance == null) {
            instance = new Instance();
        }
        return instance;
    }
}
  • 改进方案A 对整个方法进行synchronized 并发性不好,但没问题
public class SafeLazyInitialization {
    private static Instance instance;
    public synchronized static Instance getInstance() {
        if (instance == null) {
            instance = new Instance();
        }
        return instance;
    }
}
  • 改进方案B 双重检查 + 部分synchronized技术 有问题
public class DoubleCheckedLocking {
    private static Instance instance;
    public static Instance getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (DoubleCheckedLocking.class) { // 加锁
                if (instance == null) { // 第二次检查
                    instance = new Instance(); // 执行构造函数
                }
            }
        }
        return instance;
    }
}

问题在于:某个线程在第一次检查时发现instance不为null然后直接返回,此时返回的instance变量可能未完成初始化
执行构造函数可以分解为如下3行代码:
memory = allocate();  // 1:分配对象的内存空间
initiate(memory);  // 2:初始化对象
instance = memory;   // 3:设置instance指向刚刚分配的内存地址

由于指令重排序,导致构造函数变成:
memory = allocate();  // 1:分配对象的内存空间
instance = memory;   // 2:设置instance指向刚刚分配的内存地址
initiate(memory);  // 3:初始化对象

线程A进入synchronized同步块执行重排序后的构造函数时,当线程A执行完上面的第2行代码(设置instance指向刚刚分配的内存地址)还未执行第3行代码(初始化对象)时,如果线程B此时在第一次检查发现instance不为null然后直接返回,此时返回的instance变量还未完成初始化。

如果线程A与线程B在第一个if分支都发现instance为null,这个程序就没有问题,此时假设线程A先进入synchronized块并完成初始化,当线程B再次获得synchronized锁时,可以得到如下happens-before规则:线程A执行构造函数 happens-before 线程A对synchronized的解锁 happens-before 线程B对synchronized的加锁,因此线程B在第二次检查时会发现instance不为null然后返回,此时返回的instance是完成初始化的。

  • 改进方案C 双重检查 + 部分synchronized + volatile变量 没问题

这个方案的正确性与instance变量是否是static的无关

public class DoubleCheckedLocking {
    private static volatile Instance instance; 
    public static Instance getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (DoubleCheckedLocking.class) { // 加锁
                if (instance == null) { // 第二次检查
                    instance = new Instance(); // 执行构造函数
                }
            }
        }
        return instance;
    }
}

这个方案本质是禁止构造函数的第2行代码(初始化对象)和第3行代码(设置instance指向刚刚分配的内存地址)间的重排序,并且第3行代码执行完后将处理器缓存内容刷到内存。这个方案的正确性可以通过happens-before规则分析:当线程B在第一次检查发现instance不为null然后直接返回时,此时一定有另一个线程A已经在构造函数中完成“设置instance指向刚刚分配的内存地址”这个操作(第3行代码),此时有 线程A的“初始化对象”操作(第2行代码) happens-before “设置instance指向刚刚分配的内存地址”这个操作(第3行代码) happens-before 线程B对volatile变量的读

  • 改进方案D 使用static初始化 没问题

这个方案允许构造函数內代码的重排序,但不允许非构造线程“看到”这个重排序。
这个方案只能用于静态字段的延迟初始化,上面的基于volatile的方案还可以用于对实例字段的初始化。

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        // 当读取InstanceHolder的静态变量时,会立即触发InstanceHolder类的初始化。
        // 每个类或接口有一个初始化锁(Class对象的初始化锁),第一个获取到锁的线程会完成初始化,后面获取到锁的线程发现类已经完成初始化则不会再初始化,直接使用类
        return InstanceHolder.instance; 
    }
}
  • 我的改进方案E 使用双重检查 + 部分synchronized + final 技术,没问题
class DoubleCheckFinalSingletonLazyInit {
    private static class FinalSingletonHolder {
        private final Singleton singleton = new Singleton(); // 这里不是static的,否则和静态初始化是一样的
    }
    private static FinalSingletonHolder finalSingletonHolder;

    public static Singleton getSingleton() {
        if (finalSingletonHolder == null) {
            synchronized (DoubleCheckFinalSingletonLazyInit.class) {
                if (finalSingletonHolder == null) {
                    finalSingletonHolder = new FinalSingletonHolder(); // 这里new的是FinalSingletonHolder
                }
            }
        }
        return finalSingletonHolder.singleton;
    }
}

通用线程生命周期

线程的引入,可以把一个进程的资源分配和 执行调度分开,进程成为资源分配的基本单位,线程成为 处理器调度的基本单位。
Java的每个用户线程都是直接映射到一个操作系统的内核线程的(1:1线程模型,Golang是M : N线程模型),而且中间没有额外的间接结构,所以HotSpot不会也无法去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议,Thread::yield()可以建议操作系统主动让出执行时间),全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理 器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统 全权决定的。1:1线程模型也具有它的局限性:首先,由于 是基于内核线程实现的,所以各种线程操作,如创建、析构、同步、切换,都需要进行系统调用,而系统调用的代价相对较高,需要在用户态和内核态来回切换。其次,一个系统支持的内核线程的数量是十分有限的,内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。

  • 初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
  • 可运行状态(就绪),指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
  • 运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
  • 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

这五种状态在不同编程语言里会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。除了简化合并,这五种状态也有可能被细化,比如,Java 语言里就细化了休眠状态。

Java线程状态

Java 中线程共有六种状态,分别是:

  • NEW(初始化状态)
    线程被构建,还没调用start()方法
  • RUNNABLE(可运行(就绪) / 运行状态)
  • BLOCKED(阻塞状态)
  • WAITING(无时限等待)
  • TIMED_WAITING(有时限等待)
  • TERMINATED(终止状态)
    线程进入终止状态有两种情况:1、Runnable的run方法正常执行结束 2、异常在run方法中没有捕获而抛出run方法之外

Java 中的 BLOCKED、WAITING、TIMED_WAITING 是操作系统层面休眠状态的子集(有些在操作系统层面是休眠状态的,在Java中却是RUNNABLE),处于这三种状态的 Java 线程没有 CPU 的使用权。

当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态并抛出 InterruptedException 异常(InterruptedException 异常只能在线程处于RUNNABLE 状态下才能抛出来)。 sleep、join等会立即抛出InterruptedException异常,wait会将线程从等待队列移到阻塞队列(此时线程状态是BLOCKED),并在线程重新获得锁后(处于RUNNABLE 状态后)才会抛出异常,另外,如果因为InterruptedException导致退出同步代码块会自动释放当前线程持有的synchronized锁)

Java的RUNNABLE包括了通用线程生命周期里面的可运行(就绪) 、运行状态、部分休眠状态。

RUNNABLE 与 BLOCKED 的状态转换只有一种场景会触发,就是线程等待 synchronized 的隐式锁。阻塞在 java.concurrent包中Lock接口的线程状态却是WAITING/TIMED_WAITING状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。

当线程调用阻塞式 API 时,在操作系统层面,线程是会转换到休眠状态的,但是在 JVM 层面,Java 线程的状态不会发生变化,依然保持 RUNNABLE 状态。JVM 层面并不关心操作系统调度相关的状态,因为在 JVM 看来,等待 CPU 使用权(操作系统层面处于可执行状态)与等待 I/O(操作系统层面处于休眠状态)没有区别,都是在等待某个资源,所以都归入了 RUNNABLE 状态。所以我们平时所谓的 Java 在调用阻塞式 API 时,线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。

上面的Java线程状态变迁图中,从WAITING、TIMED_WAITING返回后其实会立即进入到BLOCKED状态

LockSupport.park, Object.wait, Thread.sleep的区别

  • Object.wait, Thread.sleep, Thread.join会响应中断,会自动清除其中断标志位然后抛出InterruptedException,LockSupport.park方法收到中断信号会直接返回,不会抛出InterruptedException,不会自动清除其中断标志位。
  • 如果notify先于wait调用,则没有用。但如果先调用unpark方法,后续调用park时会直接返回。但多次调用unpark没用,只相当于调用了一次,因为最多只有一个许可证。
  • Thread.sleep()和LockSupport.park()阻塞时不会释放当前线程占有的锁资源;而Object.wait阻塞时会释放当前线程占有的锁资源
  • 使用wait、notify、notifyAll需要先获取对象的锁,否则会抛出IllegalMonitorStateException。wait唤醒线程的顺序是随机的,非公平的

由于存在中断和假唤醒(spurious wakeup)的情况,因此需要在while循环中调用wait和LockSupport.park

synchronized(对象) {
    while(条件不满足) {
        wait();
    }
}

Daemon线程

当Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会自动退出,Daemon线程会被立即终止,Daemon线程代码中的finally块可能都不会执行

Serilizable接口与transient关键字

一个类实现Serilizable接口最好显式定义一个变量

private static final long serialVersionUID = -2052381772192998351L; 

序列化时会将serialVersionUID写入序列化文件,反序列化时会检查目标类的serialVersionUID是否与序列化文件中的一样,如果不一样则报错,如果一样则会尝试反序列化(即便这两个类完全不同)。 如果没有定义这个变量,JVM会自动根据类的定义生成这个变量,这样当反序列化类的定义和序列化类有一点点不相同时便无法反序列化。

一个对象只要实现了Serilizable接口,这个对象就可以被序列化,但是如果类的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化。static静态变量(不管有没有transient)一定不会序列化,一个对象初始化时static变量的值是此时JVM中该类的静态变量值。final变量前加了transient,这个final变量的值就不会序列化,当反序列化时,如果这个final变量有默认初始值(声明该变量时在同一行中指定了初始值),那反序列化后这个final变量的值是其默认初始值,如果这个final变量没有默认初始值,反序列化后这个final变量的值是null。反序列化时,如果一个transient变量在声明时指定了默认初始值,这个初始值也没用,此时这个transient变量的初始值是0或null。

子类实现了 Serializable 接口只能序列化子类的成员,只有父类实现了 Serializable 接口才能序列化父类的成员,如果父类没有实现Serializable接口,那父类必须有个无参构造函数,通过这个无参构造函数为父类的成员赋值。

一个类如果没有实现Serilizable接口则不能序列化。一个类如果实现了Serilizable接口,但有成员类未实现Serilizable接口,且这个成员是非null的,则序列化时会报错,解决办法是给这个成员加上transient或重写 writeObject 和 readObject方法。

在Java中,对象的序列化可以通过实现两种接口来实现,若实现的是Serializable接口,且类未实现writeObject 和 readObject方法,则会调用默认提供的序列化方法,此时transient才会起作用。若实现的是Externalizable接口,则没有任何变量可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,需要在readExternal方法中手工指定要从序列化文件中读入的变量,这与成员变量是否被transient修饰无关。

class Book implements Serializable {
    private static final long serialVersionUID = -2936687026040726549L;
    // 如果 People 没有实现 Serializable接口且不为null,那Book 类的序列化会失败
    // private People people;
    private static int BOOK_NO = 123;
    private String bookName = "Java";
    private transient int bookPrice = 20;

    private transient final String description = "Java description";
    private transient final String author;

    public Book(String author) {
        this.author = author;
//        this.people = new People();
}

    public static void main(String[] args) throws Exception {
        Book book1 = new Book("xiaoming");
        book1.bookName = "Go";
        book1.bookPrice = 30;
        String fileName = "/Users/testfile";
        FileOutputStream file = new FileOutputStream(fileName);
        ObjectOutputStream out = new ObjectOutputStream(file);
        // 如果Book 没有实现 Serializable 接口会抛异常
        out.writeObject(book1);
        out.close();
        file.close();
        // 反序列化前修改静态变量
        Book.BOOK_NO = 789;
        FileInputStream file1 = new FileInputStream(fileName);
        ObjectInputStream in = new ObjectInputStream(file1);
        Book book2 = (Book) in.readObject();
        in.close();
        file1.close();
        System.out.println(book2.BOOK_NO); // 789
        System.out.println(book2.bookName); // Go
        System.out.println(book2.bookPrice); // 0,bookPrice不是final变量,所以有初始值也没用
        System.out.println(book2.description); // Java description,因为final变量有默认初始值
        System.out.println(book2.author); // null, author 是null,因为final变量没有默认初始值
    }
}

参考:
https://www.cnblogs.com/lanxuezaipiao/p/3369962.html
https://www.baeldung.com/java-transient-keyword
https://z.itpub.net/article/detail/0C1A0D0554C079E0D882253855CBB0B9

posted @ 2022-10-12 11:25  zoo-keeper  阅读(38)  评论(0)    收藏  举报