現在屬于一個查漏補缺的階段,之前京東的面試中問到我,關于多線程操作集合時,集合不安全該如何解決?
當時就隻想到了(可能因為緊張,我承認比較菜 )使用使用實作安全的集合類和使用Collections。synchronizedxxx的集合安全類來解決,現在回想起來自己當時的确回答的不好,這兩種方式并不能保證“真正的線程安全”。查漏補缺,将掌握不熟練的知識一定要多練習,傳回回顧知識,熟能生巧。
而且最近越來越感受到,每當看一些曾經掌握的知識的源碼,就發現那些前輩真的很厲害,有一些設計很巧妙。
文章目錄
- List集合不安全
-
- 方式1:直接使用多線程安全的集合類
-
- 如何回答Vector是否是線程安全的集合類?
- 方式2:使用Collections工具類修飾的集合類
- 方式3:使用集合安全類
-
- CopyOnWriteArrayList是如何保證線程安全的?
- CopyOnWriteArrayList中可能出現的問題:
- CopyOnWriteArrayList内部時如何實作的(梳理版)?
- synchronizedArrayList的實作和CopyOnWriteArrayList有什麼不同?
- Set不安全
-
- 使用Collections工具包下的synchronizedSet
- 使用同步的Set集合
- Map不安全
-
- 使用Hashtable
- 使用Collections.synchronizedMap
- 使用同步Map
List集合不安全
目前企業的開發都會考慮到高并發的問題,部落客我在今年秋招時也被多次問到集合類在多線程中使用的問題。這部分還是很重要的。
我們熟悉的ArrayList、HashMap等等集合類很多都不是線程安全的。也就是說在多線程情況下是可能造成多線程問題,是以
有需求也必須
讓使用的集合類變安全。
通常能夠想到的使用多線程安全的集合類的方式有三種:
- 直接使用本身就是線程安全的集合類,比如使用Vector,HashTable等等。
- 使用集合類工具類Collections類下的工具類對集合類進行修飾。
- 使用JUC包下的多線程安全集合類,如CopyOnWriteArrayList等。
接下來,我将三種不同的多線程安全的使用集合類做代碼示範。
先示範一下普通的集合類會造成多線程安全問題
package jucTest2;
import java.util.ArrayList;
import java.util.UUID;
/**
* @author 雷雨
* @date 2020/12/4 17:02
* 集合類在多線程下操作的不安全性
*/
public class UnFireCollectionDemo {
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
new Thread(()->{
for (int i = 0; i < 10; i++) {
arrayList.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(arrayList);
}
},"A線程").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
arrayList.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(arrayList);
}
},"B線程").start();
}
}
出現了
java.util.ConcurrentModificationException
異常(同步修改異常),也就是說在多線程同時操作集合時出現了多線程操作的問題。
方式1:直接使用多線程安全的集合類
package jucTest2;
import java.util.UUID;
import java.util.Vector;
/**
* @author 雷雨
* @date 2020/12/4 16:55
* 直接使用多線程安全的集合類
*/
public class FireCollectionDemo1 {
public static void main(String[] args) {
Vector<String> vector = new Vector<>();
new Thread(()->{
for (int i = 1; i <= 10 ; i++) {
vector.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(vector);
}
},"線程A").start();
new Thread(()->{
for (int i = 1; i <= 10 ; i++) {
vector.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(vector);
}
},"線程B").start();
}
}
有觀察結果會發現:沒有出現異常。也就是說在集合Vector進行多線程操作時沒有發生多線程問題。
為什麼Vector是線程安全的?
簡單的講,Vector是線程安全的,因為Vector中的每個方法都使用了synchronized修飾,進而保證通路 vector 的任何方法都必須獲得對象的 intrinsic lock (或叫 monitor lock),也即,在vector内部,其所有方法不會被多線程所通路。
Vector一定不存在多線程安全問題嗎?
if (!vector.contains(element))
vector.add(element);
...
}
其實Vector也可能存在多線程安全安全問題,雖然在Vector的内部的方法都使用了synchronized修飾,保證了在Vector内部使用時,Vector是線程安全的,但是如果如上述代碼所示,是在外部環境中使用的,仍然存在鎖競争,對應上述代碼,雖然contains和add方法都是原子性的操作,但是在if條件判斷為真之後,關于contains的鎖釋放了,在多線程的環境中,其他線程有可能與add線程競争并擷取了鎖資源後修改了其狀态,而add線程在當時正在等待,隻有其他線程釋放鎖資源後,add線程拿到了鎖,add線程才執行(而在add方法執行時,它已經是基于一個錯誤的假設了)。
單個方法的synchronized了并不代表組合方法調用的原子性。
如何回答Vector是否是線程安全的集合類?
Vector 和 ArrayList 實作了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 關鍵修飾。但對于複合操作,Vector 仍然需要進行同步處理。
方式2:使用Collections工具類修飾的集合類
package jucTest2;
import java.util.*;
/**
* @author 雷雨
* @date 2020/12/4 17:28
* 直接使用多線程安全的集合類
*/
public class FireCollectionDemo2 {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<String>());
new Thread(()->{
for (int i = 1; i <= 10 ; i++) {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}
},"線程A").start();
new Thread(()->{
for (int i = 1; i <= 10 ; i++) {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}
},"線程B").start();
}
}
結果正常輸出,沒有發生多線程安全問題。
關于Collections.synchronizedList和Vector的差別:
- 在源碼中Vector中線程安全的實作是使用了synchronized鎖住了整個方法(也就是使用了同步方法的方式),而在Collections.synchronizedList中是使用synchronized鎖了目前的mutex對象,而mutex對象指向的是目前的執行個體。
- 那麼Vector鎖的對象是調用者,而Collections.synchronizedList鎖的是synchronizedList本身的執行個體對象。
方式3:使用集合安全類
比如使用CopyOnWriteArrayList
package jucTest2;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author 雷雨
* @date 2020年12月4日20:45:23
* 直接使用多線程安全的集合類CopyOnWriteArratList
*/
public class FireCollectionDemo3 {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
new Thread(()->{
for (int i = 1; i <= 10 ; i++) {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}
},"線程A").start();
new Thread(()->{
for (int i = 1; i <= 10 ; i++) {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}
},"線程B").start();
}
}
多次運作,發現沒有出現異常,該集合類是一個多線程安全的類。
CopyOnWriteArrayList是如何保證線程安全的?
CopyOnWriteArrayList直接翻譯就是寫的時候複制,也就是說在寫操作的時候是建立一個新的容器進行寫操作,寫完之後,再将原容器的引用指向新容器,整個過程加鎖,保證了寫的線程安全。
整個過程都使用Lock加鎖,是線程安全的
//添加元素操作
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//複制
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//将array引用指向新數組
setArray(newElements);
return true;
} finally {
//解鎖
lock.unlock();
}
}
//删除操作
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
而因為讀操作的時候不會對目前容器做任何處理,是以我們可以對容器進行并發的讀,而不需要加鎖,也就是讀寫分離。
public E get(int index) {
return get(getArray(), index);
}
CopyOnWriteArrayList中可能出現的問題:
CopyOnWriteArrayList雖然實作了讀寫分離,提高了效率,并且在需要寫操作的地方使用了ReentrantLock保證了線程的同步,但是仍然是存在問題的:
- 由于寫操作是通過複制原數組,會消耗記憶體,如果原數組的資料量較大,可能會導緻頻繁的minor GC。
- 不能用于
的場景,因為是讀寫分離的,而且在寫操作中采用的方式是通過複制寫的操作,那麼就會有耗時,可能會在寫入資料的過程中,有讀取的操作,那麼可能導緻讀取的資料還是舊的資料。實時性
能保證CopyOnWriteArrayList
,但是卻不能滿足實時性的要求。最終一緻性
從第二點也就說明了CopyOnWriteArrayList其實比較适用于
讀多寫少
的場景,但是還是慎用,不能保證每次寫入的操作的資料量,可能會導緻讀取到舊的資料的可能性。
小結:
CopyOnWriteArrayList的思想:
1、讀寫分離,讀和寫分開
2、最終一緻性
3、使用另外開辟空間的思路,來解決并發沖突
CopyOnWriteArrayList内部時如何實作的(梳理版)?
- 寫操作的實作
再梳理一遍源碼(以添加操作為例)
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加鎖
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//複制
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//将array引用指向新數組
setArray(newElements);
return true;
} finally {
//解鎖
lock.unlock();
}
}
首先CopyOnWriteArrayList的寫操作的實作:
- 首先在寫操作的内部建立了一個ReentrantLock同步鎖。
- 另外在寫操作,在加鎖的情況下還使用了複制寫的思想。複制一個新的資料,添加元素之後,将引用指向新數組。
為什麼要使用ReentrantLock?
是為了保證多線程的同步操作。對于多線程同步,采用加鎖,能夠解決多線程的問題。
為什麼要使用複制寫的操作?
因為關于集合的操作不僅有寫操作還有讀操作,如何不采用複制寫的思想,那麼就要對讀操作也要加鎖,不然就可能會造成線程安全問題(鎖競争機制)。但是CopyOnWriteArrayList為了保證讀寫分離(保證讀的操作的效率),是以沒有對讀操作加鎖。
如果沒有複制,寫時加鎖,讀取不加鎖,那麼就會造成并發讀寫問題,産生不可預期的錯誤,造成ConcurrentModificationException問題。(是因為為了保證并發讀寫的安全性,在集合中維護了一個ModConcurrent用來計數集合修改次數)如果在寫時,進行了讀取操作,ModConcurrent變化了,就會抛出ConcurrentModificationException。
- 可能會問,為什麼CopyOnWriteArrayList中采用寫操作,讀不加鎖?
如果寫操作加鎖,讀操作也使用ReentrantLock加鎖,那麼就退化為synchronized,讀性能大大減弱。
synchronizedArrayList的實作和CopyOnWriteArrayList有什麼不同?
synchronizedArrayList的實作是使用了synchronized關鍵字在方法的内部對操作進行加鎖(同步代碼塊)的方式實作線層同步,CopyOnWriteArrayList的内部使用的是ReentrantLock(同步鎖),CopyOnWriteArrayList底層儲存元素的數組使用了volatile保證而來線程間的可見性。
Set不安全
package jucTest2;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* @author 雷雨
* @date 2020/12/4 21:53
* Set 集合不安全
*/
public class UnFireCollectionSet {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
new Thread(()->{
for (int i = 0; i < 10; i++) {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}
},"A線程").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}
},"B線程").start();
}
}
結果可以看到發生了CurrentModifcationException異常(同步修改異常)。
使用Collections工具包下的synchronizedSet
package jucTest2;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
/**
* @author 雷雨
* @date 2020/12/5 8:47
* 使用集合安全的類 set
* Collections工具包下的集合安全類
*/
public class FireCollectionSet2 {
public static void main(String[] args) {
Set<String> set = Collections.synchronizedSet(new HashSet<String>());
new Thread(()->{
for (int i = 0; i < 10; i++) {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}
},"線程A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}
},"線程B").start();
}
}
使用同步的Set集合
使用CopyOnWriteSet
package jucTest2;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author 雷雨
* @date 2020/12/5 8:53
* 使用同步Set
*/
public class FireCollectionSet3 {
public static void main(String[] args) {
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<String>();
new Thread(()->{
for (int i = 0; i < 10; i++) {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}
},"線程A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
}
},"線程B").start();
}
}
CopyOnWriteSet的底層使用CopyOnWriteList來實作的,是以也能保證線程安全。
Map不安全
HashMap在多線程操作下不安全
package jucTest2;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author 雷雨
* @date 2020/12/5 9:03
* Map在多線程下操作不安全
*
*/
public class UnFireCollectionMap {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線層A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線程B").start();
}
}
使用Hashtable
使用HashTable
package jucTest2;
import java.util.Hashtable;
import java.util.UUID;
/**
* @author 雷雨
* @date 2020/12/5 9:10
* 使用本身就是多線程安全的類
*/
public class FireCollectionMap1 {
public static void main(String[] args) {
Hashtable<String,Integer> map = new Hashtable<>();
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線層A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線程B").start();
}
}
使用本身是集合安全的Hashtable能夠保證多線程操作的安全。
為什麼Hashtable是線程安全的?
看源碼分析一下
//寫操作上加鎖
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;
}
//讀操作加鎖
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
小結:Hashtable就是将讀寫方法都進行了加鎖,保證了讀寫的多線程安全性。
但是還是仍然存在多線程問題的,因為存在鎖資源競争。
使用Collections.synchronizedMap
package jucTest2;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* @author 雷雨
* @date 2020/12/5 9:17
* 使用Collections
*/
public class FireCollectionMap2 {
public static void main(String[] args) {
Map<Object, Object> map = Collections.synchronizedMap(new HashMap<>());
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線層A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線程B").start();
}
}
Collections.synchronizedMap是線程安全的類。
使用同步Map
使用ConcurrentHashMap
package jucTest2;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* @author 雷雨
* @date 2020/12/5 9:25
* 使用同步Map
*/
public class FireCollectionMap3 {
public static void main(String[] args) {
ConcurrentMap<String,Integer> map = new ConcurrentHashMap<>();
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線層A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
int temp = i;
map.put(UUID.randomUUID().toString().substring(0,5),temp);
System.out.println(map);
}
},"線程B").start();
}
}
ConcurrentHashMap源碼分析放在之後的部落格,因為細節比較多。