最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

JDK序列化Bug難題解決示例詳解

 更新時間:2023年03月17日 09:40:29   作者:開發(fā)者如是說  
這篇文章主要為大家介紹了JDK序列化Bug難題解決示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

1、背景

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

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

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

2、分析

從堆棧看出來是序列化過程中報錯,并且是因為 Fragment 狀態(tài)自動保存過程中報錯,報錯的位置不在我們的代碼中,無法也不應該使用 hook 的方式解決。

再從報錯信息看,是多線程修改導致的,也就是因為 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)同步錯誤,我首先想到的是如下幾個原因:

  • 同步容器的同步鎖沒有覆蓋所有的方法:基本不可能,標準 JDK 應該還是嚴謹?shù)?...
  • 外部通過反射直接調(diào)用了同步容器內(nèi)的真實數(shù)據(jù):一般不會有這種騷操作
  • 執(zhí)行序列化過程的過程跳過了鎖:雖然是反射調(diào)用,但是代碼邏輯的執(zhí)行是在代碼塊內(nèi)部的
  • 執(zhí)行序列化方法的過程中釋放了鎖

3、復現(xiàn)

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

該異常還是比較容易復現(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),每次往線程池添加一個序列化的任務以及 19 個修改列表的操作。

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

4、解決

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

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

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

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

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

就是說,

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

bugs.openjdk.org/browse/JDK-…

這是因為當我們使用 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)了這個接口。所以,當我們使用同步容器進行包裝的時候,返回的是 SynchronizedRandomAccessList 這個類而不是 SynchronizedList 的實例.

SynchronizedRandomAccessList,它有一個 writeReplace() 方法

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

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

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

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

總結(jié)

一個略坑的問題,問題解決比較簡單,但是分析過程有些曲折,主要是被“鎖在序列化過程被釋放了”這個想法誤導。而實際上之所以出現(xiàn)這個問題是因為加鎖的是不同的對象。此外,還有一個原因是,序列化過程許多操作是反射執(zhí)行的,比如 writeReplace()writeObject() 這些方法。如果對 JDK 的序列化過程不了解,很難想到這兩個 private 的方法。

從這個例子中可以得出的另一個結(jié)論就是,同步容器和并發(fā)容器實現(xiàn)邏輯不同,看來在有些情形下兩者起到的效果還是有區(qū)別的。序列化可能是一個極端的例子,但是下次序列化一個列表的時候是否應該考慮到 JDK 的這個 bug 呢?

以上就是JDK序列化Bug難題解決示例詳解的詳細內(nèi)容,更多關(guān)于JDK序列化Bug難題解決的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 基于Java回顧之I/O的使用詳解

    基于Java回顧之I/O的使用詳解

    我計劃在接下來的幾篇文章中快速回顧一下Java,主要是一些基礎(chǔ)的JDK相關(guān)的內(nèi)容
    2013-05-05
  • Spring MVC整合Shiro權(quán)限控制的方法

    Spring MVC整合Shiro權(quán)限控制的方法

    這篇文章主要介紹了Spring MVC整合Shiro權(quán)限控制,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-05-05
  • SpringAop攔截參數(shù)帶注解的方法

    SpringAop攔截參數(shù)帶注解的方法

    這篇文章主要介紹了SpringAop攔截參數(shù)帶注解的方法,本文給大家介紹的非常詳細,感興趣的朋友一起看看吧
    2024-12-12
  • 用java實現(xiàn)掃雷游戲

    用java實現(xiàn)掃雷游戲

    這篇文章主要為大家詳細介紹了用java實現(xiàn)掃雷游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-06-06
  • Java集合系列之ArrayList源碼分析

    Java集合系列之ArrayList源碼分析

    這篇文章主要為大家詳細介紹了Java集合系列之ArrayList源碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-02-02
  • 使用Sentinel自定義返回和實現(xiàn)區(qū)分來源方式

    使用Sentinel自定義返回和實現(xiàn)區(qū)分來源方式

    這篇文章主要介紹了使用Sentinel自定義返回和實現(xiàn)區(qū)分來源方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2025-04-04
  • WebSocket+Vue+SpringBoot實現(xiàn)語音通話的使用示例

    WebSocket+Vue+SpringBoot實現(xiàn)語音通話的使用示例

    本文主要介紹了WebSocket+Vue+SpringBoot實現(xiàn)語音通話的使用示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-11-11
  • 基于binarywang封裝的微信工具包生成二維碼

    基于binarywang封裝的微信工具包生成二維碼

    這篇文章主要介紹了基于binarywang封裝的微信工具包生成二維碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-11-11
  • Spring\SpringBoot配置連接數(shù)據(jù)庫的方法

    Spring\SpringBoot配置連接數(shù)據(jù)庫的方法

    最近在學習SpringBoot,第一步就是要配置數(shù)據(jù)庫,本文詳細的介紹了Spring\SpringBoot配置連接數(shù)據(jù)庫的方法,有需要的朋友們下面隨著小編來一起學習學習吧
    2021-06-06
  • 深度解析Java中的ReentrantLock原理

    深度解析Java中的ReentrantLock原理

    這篇文章主要介紹了關(guān)于ReentrantLock的原理解析,文章通過代碼示例介紹的非常詳細,具有一定的參考價值,需要的朋友可以參考下
    2023-07-07

最新評論

虞城县| 河西区| 鸡东县| 时尚| 鞍山市| 孝昌县| 桦川县| 东山县| 溧水县| 合作市| 亚东县| 连州市| 云阳县| 大冶市| 明星| 永昌县| 东莞市| 榆社县| 白沙| 大竹县| 丹阳市| 东阿县| 太白县| 长春市| 华蓥市| 玉溪市| 徐州市| 共和县| 拉孜县| 武川县| 长沙市| 于田县| 清丰县| 长丰县| 闽清县| 朝阳区| 扎囊县| 句容市| 东安县| 牡丹江市| 成都市|