您好,登錄后才能下訂單哦!
這篇文章主要講解了“JDK序列化Bug難題如何解決”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“JDK序列化Bug難題如何解決”吧!
最近查看應用的崩潰記錄的時候遇到了一個跟 Java 序列化相關的崩潰,
從崩潰的堆棧來看,整個調用堆棧里沒有我們自己的代碼信息。崩潰的起點是 Android 系統自動存儲 Fragment 的狀態,也就是將數據序列化并寫入 Bundle 時。最終出現問題的代碼則位于 ArrayList 的 writeObject()
方法。
這里順帶說明一下,一般我們在使用序列化的時候只需要讓自己的類實現 Serializable 接口即可,最多就是為自己的類增加一個名為 SerialVersionUID
的靜態字段以標志序列化的版本號。但是,實際上序列化的過程是可以自定義的,也就是通過 writeObject()
和 readObject()
實現。這兩個方法看上去可能比較古怪,因為他們既不存在于 Object 類,也不存在于 Serializable 接口。所以,對它們沒有覆寫一說,并且還是 private 的。從上述堆棧也可以看出,調用這兩個方法是通過反射的形式調用的。
從堆棧看出來是序列化過程中報錯,并且是因為 Fragment 狀態自動保存過程中報錯,報錯的位置不在我們的代碼中,無法也不應該使用 hook 的方式解決。
再從報錯信息看,是多線程修改導致的,也就是因為 ArrayList 并不是線程安全的,所以,如果在調用序列化的過程中其他線程對 ArrayList 做了修改,那么此時就會拋出 ConcurrentModificationException
異常。
但是! 再進一步看,為了解決 ArrayList 在多線程環境中不安全的問題,我這里是用了同步容器進行包裝。從堆棧也可以看出,堆棧中包含如下一行代碼,
Collections$SynchronizedCollection.writeObject(Collections.java:2125)
這說明,整個序列化的操作是在同步代碼塊中執行的。而就在執行過程中,其他線程完成了對 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 這個方法執行 1 和 2 之間的代碼的時候,容器被修改了。
但是,該方法的調用是位于同步容器的同步代碼塊中的,這里出現同步錯誤,我首先想到的是如下幾個原因:
同步容器的同步鎖沒有覆蓋所有的方法:基本不可能,標準 JDK 應該還是嚴謹的 ...
外部通過反射直接調用了同步容器內的真實數據:一般不會有這種騷操作
執行序列化過程的過程跳過了鎖:雖然是反射調用,但是代碼邏輯的執行是在代碼塊內部的
執行序列化方法的過程中釋放了鎖
帶著上述問題,首先還是先復現該問題。
該異常還是比較容易復現,
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)); } }
這里創建了一個容量為 20 的線程池,遍歷 100 次循環,每次往線程池添加一個序列化的任務以及 19 個修改列表的操作。
按照上述操作,基本 100% 復現這個問題。
如果只是從堆棧看,這個問題非常“詭異”,它看上去是在執行序列化的過程中把線程的鎖釋放了。所以,為了找到問題的原因我做了幾個測試。
當然,我首先想到的是解決并發修改的問題,除了使用同步容器,另外一種方式是使用并發容器。ArrayList 對應的并發容器是 CopyOnWriteArrayList
。換了該容器之后可以修復這個問題。
此外,我用自定義同步鎖的形式在序列化操作的外部對整個序列化過程進行同步,這種方式也可以解決上述問題。
不過,雖然解決了這個問題,此時還存在一個疑問就是序列化過程中鎖是如何“丟”了的。為了更好地分析問題,我 Copy 了一份 JDK 的 SynchronizedList
的源碼,并使用 Copy 的代碼復現上述問題,試了很多次也沒有出現。所以,這成了“看上去一樣的代碼,但是執行起來結果不同”。感覺非常“詭異”。
最后,我把這個問題放到了 StackOverflow 上面。國外的一個開發者解答了這個問題,
就是說,
這是 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)); }
它會根據被包裝的容器是否實現了 RandomAccess
接口來判斷使用 SynchronizedRandomAccessList
還是 SynchronizedList
進行包裝。RandomAccess 的意思是是否可以在任意位置訪問列表的元素,顯然 ArrayList 實現了這個接口。所以,當我們使用同步容器進行包裝的時候,返回的是 SynchronizedRandomAccessList
這個類而不是 SynchronizedList
的實例.
對 SynchronizedRandomAccessList
,它有一個 writeReplace()
方法
private Object writeReplace() { return new SynchronizedList<>(list); }
這個方法是用來兼容 1.4 之前版本的序列化的,所以,當對 SynchronizedRandomAccessList 執行序列化的時候會先調用 writeReplace()
方法,并將被包裝的 list 對象傳入,然后使用該方法返回的對象進行序列化而不是原始對象。
對于 SynchronizedRandomAccessList,它是 SynchronizedList 的子類,它們對私有鎖的實現機制是相同的,即,兩者都是對自身的實例 (也就是 this
)進行加鎖。所以,兩者持有的 ArrayList 是同一實例,但是加鎖的卻是不同的對象。也就是說,序列化過程中加鎖的對象是 writeReplace()
方法創建的 SynchronizedList 的實例,其他線程修改數據時加鎖的是 SynchronizedRandomAccessList 的實例。
驗證的方式比較簡單,在 writeObject()
出打斷點獲取 this 對象和最初的同步容器返回結果做一個對比即可。
感謝各位的閱讀,以上就是“JDK序列化Bug難題如何解決”的內容了,經過本文的學習后,相信大家對JDK序列化Bug難題如何解決這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。