溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JDK序列化Bug難題如何解決

發(fā)布時間:2023-03-17 10:23:36 來源:億速云 閱讀:139 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“JDK序列化Bug難題如何解決”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“JDK序列化Bug難題如何解決”吧!

1、背景

最近查看應(yīng)用的崩潰記錄的時候遇到了一個跟 Java 序列化相關(guān)的崩潰,

JDK序列化Bug難題如何解決

從崩潰的堆棧來看,整個調(diào)用堆棧里沒有我們自己的代碼信息。崩潰的起點是 Android 系統(tǒng)自動存儲 Fragment 的狀態(tài),也就是將數(shù)據(jù)序列化并寫入 Bundle 時。最終出現(xiàn)問題的代碼則位于 ArrayList 的 writeObject() 方法。

這里順帶說明一下,一般我們在使用序列化的時候只需要讓自己的類實現(xiàn) Serializable 接口即可,最多就是為自己的類增加一個名為 SerialVersionUID 的靜態(tài)字段以標(biāo)志序列化的版本號。但是,實際上序列化的過程是可以自定義的,也就是通過 writeObject()readObject() 實現(xiàn)。這兩個方法看上去可能比較古怪,因為他們既不存在于 Object 類,也不存在于 Serializable 接口。所以,對它們沒有覆寫一說,并且還是 private 的。從上述堆棧也可以看出,調(diào)用這兩個方法是通過反射的形式調(diào)用的。

2、分析

從堆??闯鰜硎切蛄谢^程中報錯,并且是因為 Fragment 狀態(tài)自動保存過程中報錯,報錯的位置不在我們的代碼中,無法也不應(yīng)該使用 hook 的方式解決。

再從報錯信息看,是多線程修改導(dǎo)致的,也就是因為 ArrayList 并不是線程安全的,所以,如果在調(diào)用序列化的過程中其他線程對 ArrayList 做了修改,那么此時就會拋出 ConcurrentModificationException 異常。

但是! 再進一步看,為了解決 ArrayList 在多線程環(huán)境中不安全的問題,我這里是用了同步容器進行包裝。從堆棧也可以看出,堆棧中包含如下一行代碼,

Collections$SynchronizedCollection.writeObject(Collections.java:2125)

這說明,整個序列化的操作是在同步代碼塊中執(zhí)行的。而就在執(zhí)行過程中,其他線程完成了對 ArrayList 的修改。

再看一下報錯的 ArrayList 的代碼,

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount; // 1
    s.defaultWriteObject();
    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);
    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) { // 2
        throw new ConcurrentModificationException();
    }
}

也就是說,在 writeObject 這個方法執(zhí)行 1 和 2 之間的代碼的時候,容器被修改了。

但是,該方法的調(diào)用是位于同步容器的同步代碼塊中的,這里出現(xiàn)同步錯誤,我首先想到的是如下幾個原因:

  • 同步容器的同步鎖沒有覆蓋所有的方法:基本不可能,標(biāo)準(zhǔn) JDK 應(yīng)該還是嚴(yán)謹?shù)?...

  • 外部通過反射直接調(diào)用了同步容器內(nèi)的真實數(shù)據(jù):一般不會有這種騷操作

  • 執(zhí)行序列化過程的過程跳過了鎖:雖然是反射調(diào)用,但是代碼邏輯的執(zhí)行是在代碼塊內(nèi)部的

  • 執(zhí)行序列化方法的過程中釋放了鎖

3、復(fù)現(xiàn)

帶著上述問題,首先還是先復(fù)現(xiàn)該問題。

該異常還是比較容易復(fù)現(xiàn),

private static final int TOTAL_TEST_LOOP = 100;
private static final int TOTAL_THREAD_COUNT = 20;
private static volatile int writeTaskNo = 0;
private static final List<String> list = Collections.synchronizedList(new ArrayList<>());
private static final Executor executor = Executors.newFixedThreadPool(TOTAL_THREAD_COUNT);
public static void main(String...args) throws IOException {
    for (int i = 0; i < TOTAL_TEST_LOOP; i++) {
        executor.execute(new WriteListTask());
        for (int j=0; j<TOTAL_THREAD_COUNT-1; j++) {
            executor.execute(new ChangeListTask());
        }
    }
}
private static final class ChangeListTask implements Runnable {
    @Override
    public void run() {
        list.add("hello");
        System.out.println("change list job done");
    }
}
private static final class WriteListTask implements Runnable {
    @Override
    public void run() {
        File file = new File("temp");
        OutputStream os = null;
        ObjectOutputStream oos = null;
        try {
            os = new FileOutputStream(file);
            oos = new ObjectOutputStream(os);
            oos.writeObject(list);
            oos.flush();
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                oos.close();
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println(String.format("write [%d] list job done", ++writeTaskNo));
    }
}

這里創(chuàng)建了一個容量為 20 的線程池,遍歷 100 次循環(huán),每次往線程池添加一個序列化的任務(wù)以及 19 個修改列表的操作。

按照上述操作,基本 100% 復(fù)現(xiàn)這個問題。

4、解決

如果只是從堆棧看,這個問題非?!霸幃悺?,它看上去是在執(zhí)行序列化的過程中把線程的鎖釋放了。所以,為了找到問題的原因我做了幾個測試。

當(dāng)然,我首先想到的是解決并發(fā)修改的問題,除了使用同步容器,另外一種方式是使用并發(fā)容器。ArrayList 對應(yīng)的并發(fā)容器是 CopyOnWriteArrayList。換了該容器之后可以修復(fù)這個問題。

此外,我用自定義同步鎖的形式在序列化操作的外部對整個序列化過程進行同步,這種方式也可以解決上述問題。

不過,雖然解決了這個問題,此時還存在一個疑問就是序列化過程中鎖是如何“丟”了的。為了更好地分析問題,我 Copy 了一份 JDK 的 SynchronizedList 的源碼,并使用 Copy 的代碼復(fù)現(xiàn)上述問題,試了很多次也沒有出現(xiàn)。所以,這成了“看上去一樣的代碼,但是執(zhí)行起來結(jié)果不同”。感覺非常“詭異”。

最后,我把這個問題放到了 StackOverflow 上面。國外的一個開發(fā)者解答了這個問題,

JDK序列化Bug難題如何解決

就是說,

這是 JDK 的一個 bug,并且到 OpenJDK 19.0.2 還沒有解決的一個問題。bug 單位于,

bugs.openjdk.org/browse/JDK-&hellip;

這是因為當(dāng)我們使用 Collections 的方法 synchronizedList 獲取同步容器的時候(代碼如下),

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

它會根據(jù)被包裝的容器是否實現(xiàn)了 RandomAccess 接口來判斷使用 SynchronizedRandomAccessList 還是 SynchronizedList 進行包裝。RandomAccess 的意思是是否可以在任意位置訪問列表的元素,顯然 ArrayList 實現(xiàn)了這個接口。所以,當(dāng)我們使用同步容器進行包裝的時候,返回的是 SynchronizedRandomAccessList 這個類而不是 SynchronizedList 的實例.

SynchronizedRandomAccessList,它有一個 writeReplace() 方法

private Object writeReplace() {
    return new SynchronizedList<>(list);
}

這個方法是用來兼容 1.4 之前版本的序列化的,所以,當(dāng)對 SynchronizedRandomAccessList 執(zhí)行序列化的時候會先調(diào)用 writeReplace() 方法,并將被包裝的 list 對象傳入,然后使用該方法返回的對象進行序列化而不是原始對象。

對于 SynchronizedRandomAccessList,它是 SynchronizedList 的子類,它們對私有鎖的實現(xiàn)機制是相同的,即,兩者都是對自身的實例 (也就是 this)進行加鎖。所以,兩者持有的 ArrayList 是同一實例,但是加鎖的卻是不同的對象。也就是說,序列化過程中加鎖的對象是 writeReplace() 方法創(chuàng)建的 SynchronizedList 的實例,其他線程修改數(shù)據(jù)時加鎖的是 SynchronizedRandomAccessList 的實例。

驗證的方式比較簡單,在 writeObject() 出打斷點獲取 this 對象和最初的同步容器返回結(jié)果做一個對比即可。

感謝各位的閱讀,以上就是“JDK序列化Bug難題如何解決”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對JDK序列化Bug難題如何解決這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

jdk
AI