posts - 8, comments - 13, trackbacks - 0, articles - 43
            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理

          在平時(shí)開發(fā)中,我們經(jīng)常采用HashMap來作為本地緩存的一種實(shí)現(xiàn)方式,將一些如系統(tǒng)變量等數(shù)據(jù)量比較少的參數(shù)保存在HashMap中,并將其作為單例類的一個(gè)屬性。在系統(tǒng)運(yùn)行中,使用到這些緩存數(shù)據(jù),都可以直接從該單例中獲取該屬性集合。但是,最近發(fā)現(xiàn),HashMap并不是線程安全的,如果你的單例類沒有做代碼同步或?qū)ο箧i的控制,就可能出現(xiàn)異常。

          首先看下在多線程的訪問下,非現(xiàn)場安全的HashMap的表現(xiàn)如何,在網(wǎng)上看了一些資料,自己也做了一下測試:

           1public class MainClass {
           2    
           3    public static final HashMap<String, String> firstHashMap=new HashMap<String, String>();
           4    
           5    public static void main(String[] args) throws InterruptedException {
           6        
           7        //線程一
           8        Thread t1=new Thread(){
           9            public void run() {
          10                for(int i=0;i<25;i++){
          11                    firstHashMap.put(String.valueOf(i), String.valueOf(i));
          12                }

          13            }

          14        }
          ;
          15        
          16        //線程二
          17        Thread t2=new Thread(){
          18            public void run() {
          19                for(int j=25;j<50;j++){
          20                    firstHashMap.put(String.valueOf(j), String.valueOf(j));
          21                }

          22            }

          23        }
          ;
          24        
          25        t1.start();
          26        t2.start();
          27        
          28        //主線程休眠1秒鐘,以便t1和t2兩個(gè)線程將firstHashMap填裝完畢。
          29        Thread.currentThread().sleep(1000);
          30        
          31        for(int l=0;l<50;l++){
          32            //如果key和value不同,說明在兩個(gè)線程put的過程中出現(xiàn)異常。
          33            if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){
          34                System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));
          35            }

          36        }

          37        
          38    }

          39
          40}

          上面的代碼在多次執(zhí)行后,發(fā)現(xiàn)表現(xiàn)很不穩(wěn)定,有時(shí)沒有異常文案打出,有時(shí)則有個(gè)異常出現(xiàn):


          為什么會出現(xiàn)這種情況,主要看下HashMap的實(shí)現(xiàn):
           1public V put(K key, V value) {
           2    if (key == null)
           3        return putForNullKey(value);
           4        int hash = hash(key.hashCode());
           5        int i = indexFor(hash, table.length);
           6        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
           7            Object k;
           8            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
           9                V oldValue = e.value;
          10                e.value = value;
          11                e.recordAccess(this);
          12                return oldValue;
          13            }

          14        }

          15
          16        modCount++;
          17        addEntry(hash, key, value, i);
          18        return null;
          19    }

          我覺得問題主要出現(xiàn)在方法addEntry,繼續(xù)看:
          1void addEntry(int hash, K key, V value, int bucketIndex) {
          2    Entry<K,V> e = table[bucketIndex];
          3        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
          4        if (size++ >= threshold)
          5            resize(2 * table.length);
          6    }

          從代碼中,可以看到,如果發(fā)現(xiàn)哈希表的大小超過閥值threshold,就會調(diào)用resize方法,擴(kuò)大容量為原來的兩倍,而擴(kuò)大容量的做法是新建一個(gè)Entry[]:
           1void resize(int newCapacity) {
           2        Entry[] oldTable = table;
           3        int oldCapacity = oldTable.length;
           4        if (oldCapacity == MAXIMUM_CAPACITY) {
           5            threshold = Integer.MAX_VALUE;
           6            return;
           7        }

           8
           9        Entry[] newTable = new Entry[newCapacity];
          10        transfer(newTable);
          11        table = newTable;
          12        threshold = (int)(newCapacity * loadFactor);
          13    }

          一般我們聲明HashMap時(shí),使用的都是默認(rèn)的構(gòu)造方法:HashMap<K,V>,看了代碼你會發(fā)現(xiàn),它還有其它的構(gòu)造方法:HashMap(int initialCapacity, float loadFactor),其中參數(shù)initialCapacity為初始容量,loadFactor為加載因子,而之前我們看到的threshold = (int)(capacity * loadFactor); 如果在默認(rèn)情況下,一個(gè)HashMap的容量為16,加載因子為0.75,那么閥值就是12,所以在往HashMap中put的值到達(dá)12時(shí),它將自動擴(kuò)容兩倍,如果兩個(gè)線程同時(shí)遇到HashMap的大小達(dá)到12的倍數(shù)時(shí),就很有可能會出現(xiàn)在將oldTable轉(zhuǎn)移到newTable的過程中遇到問題,從而導(dǎo)致最終的HashMap的值存儲異常。

          JDK1.0引入了第一個(gè)關(guān)聯(lián)的集合類HashTable,它是線程安全的。HashTable的所有方法都是同步的。
          JDK2.0引入了HashMap,它提供了一個(gè)不同步的基類和一個(gè)同步的包裝器synchronizedMap。synchronizedMap被稱為有條件的線程安全類。
          JDK5.0util.concurrent包中引入對Map線程安全的實(shí)現(xiàn)ConcurrentHashMap,比起synchronizedMap,它提供了更高的靈活性。同時(shí)進(jìn)行的讀和寫操作都可以并發(fā)地執(zhí)行。

          所以在開始的測試中,如果我們采用ConcurrentHashMap,它的表現(xiàn)就很穩(wěn)定,所以以后如果使用Map實(shí)現(xiàn)本地緩存,為了提高并發(fā)時(shí)的穩(wěn)定性,還是建議使用ConcurrentHashMap。


          ====================================================================

          另外,還有一個(gè)我們經(jīng)常使用的ArrayList也是非線程安全的,網(wǎng)上看到的有一個(gè)解釋是這樣:
          一個(gè) ArrayList 類,在添加一個(gè)元素的時(shí)候,它可能會有兩步來完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
          在單線程運(yùn)行的情況下,如果 Size = 0,添加一個(gè)元素后,此元素在位置 0,而且 Size=1;
          而如果是在多線程情況下,比如有兩個(gè)線程,線程 A 先將元素存放在位置 0。但是此時(shí) CPU 調(diào)度線程A暫停,線程 B 得到運(yùn)行的機(jī)會。線程B也將元素放在位置0,(因?yàn)閟ize還未增長),完了之后,兩個(gè)線程都是size++,結(jié)果size變成2,而只有items[0]有元素。
          util.concurrent包也提供了一個(gè)線程安全的ArrayList替代者CopyOnWriteArrayList。
          
          


          評論

          # re: 并發(fā)實(shí)踐之一:HashMap的非線程安全性和ConcurrentHasMap  回復(fù)  更多評論   

          2010-09-06 12:27 by xylz
          "如果兩個(gè)線程同時(shí)遇到HashMap的大小達(dá)到12的倍數(shù)時(shí),就很有可能會出現(xiàn)在將oldTable轉(zhuǎn)移到newTable的過程中遇到問題,從而導(dǎo)致最終的HashMap的值存儲異常"

          如果只有一個(gè)線程到達(dá)閾值,另一個(gè)線程讀寫同樣會有問題,因?yàn)閞ehash的時(shí)候同一個(gè)元素的索引index會變化(不是固定的)。

          # re: 并發(fā)實(shí)踐之一:HashMap的非線程安全性和ConcurrentHasMap  回復(fù)  更多評論   

          2010-09-06 17:19 by tanlun
          @xylz
          是的,多次出現(xiàn)25=null,不是巧合。

          # re: 并發(fā)實(shí)踐之一:HashMap的非線程安全性和ConcurrentHasMap[未登錄]  回復(fù)  更多評論   

          2014-01-04 10:08 by xx
          這個(gè)例子是錯(cuò)誤的呀!

          # re: 并發(fā)實(shí)踐之一:HashMap的非線程安全性和ConcurrentHasMap[未登錄]  回復(fù)  更多評論   

          2014-01-04 10:10 by xx
          import java.util.HashMap;
          import java.util.Map;

          public class TestHashMap {

          public static final HashMap<String, String> firstHashMap=new HashMap<String, String>();
          public static void main(String[] args) throws InterruptedException {

          //線程一
          Thread t1=new Thread(){
          public void run() {
          for(int i=0;i<25;i++){
          firstHashMap.put(String.valueOf(i), String.valueOf(i));
          }
          }
          };

          //線程二
          Thread t2=new Thread(){
          public void run() {
          for(int j=25;j<50;j++){
          firstHashMap.put(String.valueOf(j), String.valueOf(j));
          }
          }
          };

          t1.start();
          t2.start();

          //主線程休眠1秒鐘,以便t1和t2兩個(gè)線程將firstHashMap填裝完畢。
          Thread.currentThread().sleep(10000);

          for(int l=0;l<50;l++){
          //如果key和value不同,說明在兩個(gè)線程put的過程中出現(xiàn)異常。
          //System.out.println(firstHashMap.get(String.valueOf(l))+" "+l);
          if(!String.valueOf(l).equals(firstHashMap.get(String.valueOf(l)))){
          System.err.println(String.valueOf(l)+":"+firstHashMap.get(String.valueOf(l)));
          //System.out.println("===="+firstHashMap.get(String.valueOf(l))+" "+l);
          }
          }
          System.out.println(firstHashMap.size());
          System.out.println("ddd==========");
          for(Map.Entry<String,String> item:firstHashMap.entrySet()){
          System.out.println(item.getKey()+" "+item.getValue());
          }
          }

          }

          遍歷map結(jié)果完全正確,多次執(zhí)行也還是正確的。源程序輸出異常是因?yàn)閮蓚€(gè)線程還沒有執(zhí)行完吧。

          # re: 并發(fā)實(shí)踐之一:HashMap的非線程安全性和ConcurrentHasMap  回復(fù)  更多評論   

          2015-04-01 11:19 by xxxx
          不是很懂!
          主站蜘蛛池模板: 庆云县| 定边县| 花莲市| 揭东县| 文成县| 平顶山市| 察雅县| 吉林市| 巢湖市| 英德市| 客服| 汕尾市| 沾益县| 佛坪县| 武定县| 灌阳县| 平乐县| 肥城市| 濉溪县| 乌兰县| 蒲江县| 乌兰浩特市| 长白| 嘉定区| 绵阳市| 五寨县| 汶上县| 涞源县| 平邑县| 德江县| 西盟| 根河市| 昌乐县| 汝城县| 华阴市| 宝兴县| 岳池县| 麻城市| 蓬莱市| 昌邑市| 商都县|