慕課網實戰·高併發探索(八):線程不安全類、同步容器

特別感謝:慕課網jimin老師的《Java併發編程與高併發解決方案》課程,以下知識點多數來自老師的課程內容。
jimin老師課程地址:Java併發編程與高併發解決方案


1、線程不安全的類

如果一個類的對象同時可以被多個線程訪問,並且你不做特殊的同步或併發處理,那麼它就很容易表現出線程不安全的現象。比如拋出異常、邏輯處理錯誤…
下面列舉一下常見的線程不安全的類及對應的線程安全類:

(1)StringBuilder 與 StringBuffer

StringBuilder是線程不安全的,而StringBuffer是線程安全的。分析源碼:StringBuffer的方法使用了synchronized關鍵字修飾。

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
(2)SimpleDateFormat 與 jodatime插件

SimpleDateFormat 類在處理時間的時候,如下寫法是線程不安全的:

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

//線程調用方法
private static void update() {
    try {
        simpleDateFormat.parse("20180208");
    } catch (Exception e) {
        log.error("parse exception", e);
    }
}

但是我們可以變換其爲線程安全的寫法:在每次轉換的時候使用線程封閉,新建變量

private static void update() {
    try {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        simpleDateFormat.parse("20180208");
    } catch (Exception e) {
        log.error("parse exception", e);
    }
}

另外我們也可以使用jodatime插件來轉換時間:其可以保證線程安全性
Joda 類具有不可變性,因此它們的實例無法被修改。(不可變類的一個優點就是它們是線程安全的)

private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

private static void update(int i) {
    log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
}

分析源碼:(不可變性)

public class DateTimeFormatter {
    //均使用final聲明
    private final InternalPrinter iPrinter;
    private final InternalParser iParser;
    private final Locale iLocale;
    private final boolean iOffsetParsed;
    private final Chronology iChrono;
    private final DateTimeZone iZone;
    private final Integer iPivotYear;
    private final int iDefaultYear;
    ...
    private InternalParser requireParser() {
        InternalParser var1 = this.iParser;
        if (var1 == null) {
            throw new UnsupportedOperationException("Parsing not supported");
        } else {
            return var1;
        }
    }
    public DateTime parseDateTime(String var1) {
        InternalParser var2 = this.requireParser();
        Chronology var3 = this.selectChronology((Chronology)null);
        DateTimeParserBucket var4 = new DateTimeParserBucket(0L, var3, this.iLocale, 
                                                            this.iPivotYear, this.iDefaultYear);
        ...
    }
(3)ArrayList,HashSet,HashMap 等Collection類

像ArrayList,HashSet,HashMap 等Collection類均是線程不安全的,我們以ArrayList舉例分析一下源碼:
1、ArrayList的基本屬性:
在聲明時使用了transient 關鍵字,此關鍵字意爲在採用Java默認的序列化機制的時候,被該關鍵字修飾的屬性不會被序列化。而ArrayList實現了序列化接口,自己定義了序列化方法(在此不描述)。

//對象數組:ArrayList的底層數據結構
private transient Object[] elementData;
//elementData中已存放的元素的個數
private int size;
//默認數組容量
private static final int DEFAULT_CAPACITY = 10;

2、初始化

/**
 * 創建一個容量爲initialCapacity的空的(size==0)對象數組
 */
 public ArrayList(int initialCapacity) {
    super();//即父類protected AbstractList() {}
    if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity);
    this.elementData = new Object[initialCapacity];
}

/**
 * 默認初始化一個容量爲10的對象數組
 */
 public ArrayList() {
    this(10);
 }

3、添加方法(重點)

//每次添加時將數組擴容1,然後再賦值
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

4、總結:ArrayList每次對內容進行插入操作的時候,都會做擴容處理,這是ArrayList的優點(無容量的限制),同時也是缺點,線程不安全。(以下例子取材於魚笑笑博客
一個 ArrayList ,在添加一個元素的時候,它可能會有兩步來完成:

  • 在 Items[Size] 的位置存放此元素;
  • 增大 Size 的值。

在單線程運行的情況下,如果 Size = 0,添加一個元素後,此元素在位置 0,而且 Size=1;
而如果是在多線程情況下,比如有兩個線程,線程 A 先將元素存放在位置 0。但是此時 CPU 調度線程A暫停,線程 B 得到運行的機會。線程B也向此 ArrayList 添加元素,因爲此時 Size 仍然等於 0 (注意,我們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1),所以線程B也將元素存放在位置0。然後線程A和線程B都繼續運行,都增加 Size 的值。 那好,現在我們來看看 ArrayList 的情況,元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這就是“線程不安全”了。

那麼如何將其處理爲線程安全的?或者說對應的線程安全類有哪些呢?接下來就涉及到我們同步容器。

2、同步容器

同步容器分兩類,一種是Java提供好的類,另一類是Collections類中的相關同步方法。

(1)ArrayList的線程安全類:Vector,Stack

Vector實現了List接口,Vector實際上就是一個數組,和ArrayList非常的類似,但是內部的方法都是使用synchronized修飾過的方法。
Stack它的方法也是使用synchronized修飾了,繼承了Vector,實際上就是棧
使用舉例(Vector):

//定義
private static List<Integer> list = new Vector<>();
//多線程調用方法
private static void update(int i) {
   list.add(i);
}

源碼分析:使用了synchronized修飾

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

但是Vector也不是完全的線程安全的,比如:
錯誤[1]:刪除與獲取併發操作

public class VectorExample {

    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {

        while (true) {
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }
            Thread thread1 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.remove(i);
                    }
                }
            };
            Thread thread2 = new Thread() {
                public void run() {
                    for (int i = 0; i < vector.size(); i++) {
                        vector.get(i);
                    }
                }
            };
            thread1.start();
            thread2.start();
        }
    }
}

運行結果:報錯java.lang.ArrayIndexOutOfBoundsException: Array index out of range
原因分析:同時發生獲取與刪除的操作。當兩個線程在同一時間都判斷了vector的size,假設都判斷爲9,而下一刻線程1執行了remove操作,隨後線程2纔去get,所以就出現了錯誤。synchronized關鍵字可以保證同一時間只有一個線程執行該方法,但是多個線程同時分別執行remove、add、get操作的時候就無法控制了。

錯誤[2]:使用foreach\iterator遍歷Vector的時候進行增刪操作

public class VectorExample3 {

    // 報錯java.util.ConcurrentModificationException
    private static void test1(Vector<Integer> v1) { // foreach
        for(Integer i : v1) {
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }

    // 報錯java.util.ConcurrentModificationException
    private static void test2(Vector<Integer> v1) { // iterator
        Iterator<Integer> iterator = v1.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            if (i.equals(3)) {
                v1.remove(i);
            }
        }
    }

    // success
    private static void test3(Vector<Integer> v1) { // for
        for (int i = 0; i < v1.size(); i++) {
            if (v1.get(i).equals(3)) {
                v1.remove(i);
            }
        }
    }

    public static void main(String[] args) {
        Vector<Integer> vector = new Vector<>();
        vector.add(1);
        vector.add(2);
        vector.add(3);
        test1(vector);
    }
}

解決辦法:在使用iteratir進行增刪操作的時候,加上Lock或者synchronized同步措施或者併發容器

(2)HashMap的線程安全類:HashTable

使用舉例:

//定義
private static Map<Integer, Integer> map = new Hashtable<>();
//多線程調用方法
private static void update(int i) {
    map.put(i, i);
}

源碼分析:

  • 保證安全性:使用了synchronized修飾
  • 不允許空值(在代碼中特殊做了判斷)
  • HashMap和HashTable都使用哈希表來存儲鍵值對。在數據結構上是基本相同的,都創建了一個繼承自Map.Entry的私有的內部類Entry,每一個Entry對象表示存儲在哈希表中的一個鍵值對。

Entry對象唯一表示一個鍵值對,有四個屬性:
-K key 鍵對象
-V value 值對象
-int hash 鍵對象的hash值
-Entry entry 指向鏈表中下一個Entry對象,可爲null,表示當前Entry對象在鏈表尾部

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
(3)Collections類中的相關同步方法

Collections類中提供了一系列的線程安全方法用於處理ArrayList等線程不安全的Collection類
這裏寫圖片描述

使用方法:

//定義
private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());
//多線程調用方法
private static void update(int i) {
    list.add(i);
}

源碼分析:
內部操作的方法使用了synchronized修飾符

 static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        ...
        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }
        ...
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章