特別感謝:慕課網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);}
}
...
}