日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Hashtable多线程遍历问题

發布時間:2025/3/21 编程问答 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Hashtable多线程遍历问题 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

If a thread-safe implementation is not needed, it is recommended to use HashMap in place of code Hashtable. If a thread-safe highly-concurrent implementation is desired, then it is recommended to use ConcurrentHashMap in place of code Hashtable.

如上一段摘自Hashtable注釋。雖然Hashtable已經不被推薦使用了,但某種情況下還是會被使用。我們知道Hashtable與HashMap一個很大的區別就是是否線程安全,Hashtable相對于HashMap來說是線程安全的。但Hashtable在使用過程中,真的是線程安全么?

最近在處理Wlan Framework中的一段邏輯,該部分邏輯使用了Hashtable存儲設備列表。該設備列表在自己的工作線程中分別有添加、刪除操作,并通過binder提供了查詢操作。查詢操作需要遍歷設備列表,由于是通過binder跨進程調用的,因此獲取列表的線程與添加、刪除操作的線程并不是同一個線程,從而遇到了ConcurrentModificationException。Hashtable雖說是線程安全的,但是它僅僅是在添加、刪除等操作時是線程安全的,如果遍歷操作處理不好,同樣會拋出異常。

出問題的遍歷方式如下

  • Iterator it;
  • it = mDeviceMap.keySet().iterator();
  • while(it.hasNext()) {
  • String key = (String)it.next();
  • ......
  • }
  • 查看Hashtable源碼,keySet返回的是Collections.SynchronizedSet對象。創建該對象時新建了一個KeySet對象,該KeySet為Hashtable的非靜態內部類。此外還傳入了Hashtable.this賦值給了SynchronizedSet的mutex,作為同步對象。

  • public Set<K> keySet() {
  • if (keySet == null)
  • keySet = Collections.synchronizedSet(new KeySet(), this);
  • return keySet;
  • }
  • 如下為Collections.SynchronizedSet的實現,鑒于篇幅原因省略了部分方法及實現內容。

  • static class SynchronizedCollection<E> implements Collection<E>, Serializable {
  • final Collection<E> c; // Backing Collection
  • final Object mutex; // Object on which to synchronize
  • SynchronizedCollection(Collection<E> c, Object mutex) {
  • this.c = Objects.requireNonNull(c);
  • this.mutex = Objects.requireNonNull(mutex);
  • }
  • public Object[] toArray() {
  • synchronized (mutex) {return c.toArray();}
  • }
  • public <T> T[] toArray(T[] a) {
  • synchronized (mutex) {return c.toArray(a);}
  • }
  • public Iterator<E> iterator() {
  • return c.iterator(); // Must be manually synched by user!
  • }
  • }
  • 如上mutex即為Hashtable的實例,與Hashtable中的add、remove等操方法用的是同一把鎖。此外,通過注釋可知,使用iterator遍歷時,必須要自己進行同步操作。

    Hashtable遍歷的方法

    Hashtable遍歷的方法雖然有很多,但均是大同小異,這里主要介紹兩種方案。
    第一種方案,通過Hashtable的源碼可知,其put、remove等方法的同步是直接作用在方法上的,等價于使用Hashtable實例作為同步鎖,因此如下遍歷方式是線程安全的。

  • synchronized(mDeviceMap) {
  • Iterator it;
  • it = mDeviceMap.keySet().iterator();
  • while(it.hasNext()) {
  • String key = (String)it.next();
  • ......
  • }
  • }
  • 由于使用迭代器遍歷拋出異常的根本原因是expectedModCount != modCount,因此第二種方案便是不使用迭代器,而是重新創建一個數組,數組內容即是Hashtable中values保存的實例。這樣的好處是無需自己再做同步,代碼和邏輯看起來簡潔,當然也會帶來占用額外空間以及效率方面的代價。

  • int size = mDeviceMap.size();
  • Device[] devices = mDeviceMap.values().toArray(new Device[size]);
  • for (Device device: devices) {
  • Log.d(TAG, "name = " + device.mName);
  • ......
  • }
  • 兩種toArray轉換的區別

    上面第二種遍歷方式,在monkey測試的時候居然還是拋出了異常,只不過這次是Device變量空指針異常??吹竭@個異常的時候一臉的懵逼。Hashtable的put方法在最開始的時候明明對value判空了,key和value都不允許為空,那這個轉換來的value數組為什么會有空的成員?

    雖然這個問題使用ConcurrentHashMap就可以避免,但總是要弄個明白心里才會踏實。那就一點點分析源碼吧。

    既然是報Device為空,那就說明轉換來的Device數組中有空成員。先分析mDeviceMap.values(),該方法同上面分析的keySet方法,返回的是SynchronizedCollection實例,這個應該沒問題,那就繼續分析后面的toArray方法了。

  • public Object[] toArray() {
  • synchronized (mutex) {return c.toArray();}
  • }
  • public <T> T[] toArray(T[] a) {
  • synchronized (mutex) {return c.toArray(a);}
  • }
  • 通過上面可以看出這里的mutex便是Hashtable實例,c便是創建的Hashtable內部類ValueCollection的實例。SynchronizedCollection支持兩種toArray方法,且均進行了同步,也就是整個轉換過程中都有做同步操作。到這有點更懵了,既然做了同步,為啥還會有value為空的問題,只能接著往下看。上面c.toArray(a)調用的是ValueCollection的方法,ValueCollection繼承自AbstractCollection,那就轉到AbstractCollection的toArray(T[] a)方法。

  • public <T> T[] toArray(T[] a) {
  • // Estimate size of array; be prepared to see more or fewer elements
  • int size = size();
  • //注意,這里對傳入的數組length與size做了比較
  • T[] r = a.length >= size ? a :
  • (T[])java.lang.reflect.Array
  • .newInstance(a.getClass().getComponentType(), size);
  • Iterator<E> it = iterator();
  • for (int i = 0; i < r.length; i++) {
  • if (! it.hasNext()) { // fewer elements than expected
  • if (a == r) {
  • r[i] = null; // null-terminate
  • } else if (a.length < i) {
  • return Arrays.copyOf(r, i);
  • } else {
  • System.arraycopy(r, 0, a, 0, i);
  • if (a.length > i) {
  • a[i] = null;
  • }
  • }
  • return a;
  • }
  • r[i] = (T)it.next();
  • }
  • // more elements than expected
  • return it.hasNext() ? finishToArray(r, it) : r;
  • }
  • 注意到最終返回的是數組r,且在for循環中,確實有對r中內容賦值為null的情況,問題應該就出在這里了。如果我們調用toArray(T[] a)時,提供的數組a長度比實際長度大,多出的部分就會被null填充;如果數組a的長度比實際長度小,則會新建一個數組,并一一填充。

    那么最開始的空指針是怎么出現的呢?

  • int size = mDeviceMap.size();
  • Device[] devices = mDeviceMap.values().toArray(new Device[size]);
  • 上面兩條語句,雖然各自都進行了同步,但是這兩條語句整體并未進行同步,當獲取size之后,其他線程此時剛好調用了remove操作,進而導致在調用toArray的時候,實際size比我們提供的數組a的長度要小,從而導致返回的數組多出部分會被null填充。

  • public Object[] toArray() {
  • // Estimate size of array; be prepared to see more or fewer elements
  • Object[] r = new Object[size()];
  • Iterator<E> it = iterator();
  • for (int i = 0; i < r.length; i++) {
  • if (! it.hasNext()) // fewer elements than expected
  • return Arrays.copyOf(r, i);
  • r[i] = it.next();
  • }
  • return it.hasNext() ? finishToArray(r, it) : r;
  • }
  • 再來看不帶參數的toArray方法。該方法比較簡單,直接根據實際的size創建數組,并進行填充。由于該方法調用時進行了同步,因此整個轉換過程都是同步的,從而直接使用toArray()轉換是線程安全的。

    總結

  • Hashtable已經不推薦使用了,如果無需考慮線程安全,直接使用Hashmap;需要考慮線程安全時,使用ConcurrentHashMap。
  • Hashtable遍歷時,還是需要注意線程安全問題。
  • SynchronizedCollection的兩種toArray方法是不同的,如無特殊要求,建議使用無參的方法。
  • 遇到問題要多看源碼實現。
  • 轉載于:https://www.cnblogs.com/zqq-blog/p/10771978.html

    總結

    以上是生活随笔為你收集整理的Hashtable多线程遍历问题的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。