同步容器是什麼:
JDK提供給了很多容器,其中有list,set,queue,map等。
這裡我們挑出List單講。
眾所周知,很多書上,我們看到Arraylist並不是執行緒安全的,Vector是執行緒安全的。
那就從原始碼上分析一下:
ArrayList中,add方法如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
複製程式碼
Vector中,add方法如下:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
複製程式碼
對比發現,Vector之所以是執行緒安全的,是因為Vector對所有的方法使用synchronized進行了修飾。
不安全的同步容器:
public class SynchornizedVector {
public static void main(String[] agrs){
Vector vector = new Vector();
for(int i =0 ; i<10; i++){
vector.add(i,i);
}
new Thread(){
@Override
public void run() {
//vector共有10個元素,index對應0-9
//第一步:執行緒1執行到j=8,暫停;
for(int j = 0; j < vector.size(); j++){
//第三部,執行緒1繼續執行,要獲取vector.get(8)的時候出錯,因為vector的元素已經被執行緒2清空
if(j == 8){
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(vector.get(j));
}
}
}.start();
new Thread(){
@Override
public void run() {
//第二步:執行緒2獲得時間片,立即執行,刪除掉vector中所有的元素
for(int i = 0; i < vector.size(); i++){
vector.remove(i);
}
}
}.start();
}
}
複製程式碼
需要對size()的地方進行同步互斥,才能確保容器是安全的,舉例如下:
第39行和第17行
public class SynchornizedVector {
public static void main(String[] agrs) {
Vector vector = new Vector();
for (int i = 0; i < 10; i++) {
vector.add(i, i);
}
new Thread() {
@Override
public void run() {
//vector共有10個元素,index對應0-9
//第一步:執行緒1執行到j=8,暫停;
synchronized (SynchornizedVector.class) {
for (int j = 0; j < vector.size(); j++) {
//第三部,執行緒1繼續執行,要獲取vector.get(8)的時候出錯,因為vector的元素已經被執行緒2清空
if (j == 8) {
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(vector.get(j));
}
}
}
}.start();
new Thread() {
@Override
public void run() {
//第二步:執行緒2獲得時間片,立即執行,刪除掉vector中所有的元素
synchronized (SynchornizedVector.class) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}
}.start();
}
}
複製程式碼
工程中大量使用的同步容器ConcurrentHashMap
眾所周知,hashMap是根據雜湊值分段儲存的,同步Map在同步的時候鎖住了所有的段(粗粒度的鎖)
而ConcurrentHashMap根據雜湊值鎖定了雜湊值對應的段,提高了併發效能(細粒度的鎖)
其資料結構如下:
根據圖中的資料結構:每次對key尋找到相應的位置需要兩次定位:1.定位到Segment。2.定位到元素所在Segment中的具體連結串列的頭部。
對讀操作不加鎖,對寫操作的鎖的粒度細化到每個Segment
支援的最大併發數就是Segment的數量
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
}
複製程式碼
count:Segment中元素的數量
modCount:對table的大小造成影響的操作的數量,比如put(),remove()
threshold:擴容閾值
table:陣列中每一個元素代表了一個連結串列的頭部
loadFactor:用於確定threshold
get過程
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
複製程式碼
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = 1;
while (cap < c)
cap <<= 1;
for (int i = 0; i < this.segments.length; ++i)
this.segments[i] = new Segment<K,V>(cap, loadFactor);
}
複製程式碼
initialCapcity:初始的容量
loadFactor:負載引數
concurrentLevel:Segment的數量,一旦設定不可改變,如果map容量不夠,需要擴容,則增加Segment陣列的大小,而不增加Segment的數量,這樣就不需要對Map做rehash,只要對Segment中的元素做rehash
整個ConcurrentHashMap的初始化方法還是非常簡單的,先是根據concurrentLevel來new出Segment,這裡Segment的數量是不大於concurrentLevel的最大的2的指數,就是說Segment的數量永遠是2的指數個,這樣的好處是方便採用移位操作來進行hash,加快hash的過程。接下來就是根據intialCapacity確定Segment的容量的大小,每一個Segment的容量大小也是2的指數,同樣使為了加快hash的過程。
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
複製程式碼
第三行的作用是:把key對應的segment找出來
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
複製程式碼
採用移位的方式操作,可以加快計算速度
確定了具體的segment之後,就要確定segment中具體的連結串列位置
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
複製程式碼
V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
複製程式碼
put過程:
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
複製程式碼
如果Segment中元素的數量超過了threshold就要進行rehash,如有key存在,則更新value值,否則新生成一個HashEntry加入到整個Segment的頭部
注意:
ConcurrentHashMap 的 get 的操作在大多數情況下都是不加鎖的,只有當找到的 HashEntry 的 value 是 null 時,才會再進行一次加鎖的讀操作,以保障讀操作的一致性。通常這種情況發生在你找到的 HashEntry 恰是另一個執行緒在做 put 操作時建立的,且 value 恰好沒有設定完成。這種情況不太容易發生。所以,對於 ConcurrentHashMap 來說,發生在同一個 Segment 的一個寫和多個讀操作是並不互斥的,所以 Segment 也就沒有繼承讀寫鎖了,而且這種設計要比讀寫鎖的併發能力更高