鎖是用來控制多個線程訪問共享資源的方式,通常來講,一個鎖可以防止多個線程同時訪問共享資源(可是有些鎖能夠容許多個線程併發的訪問共享資源,好比讀寫鎖)。在Lock接口出現以前,Java程序是靠synchronized關鍵字實現鎖功能的,而JavaSE5以後,併發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字相似的同步功能,只是在使用時須要顯式地獲取和釋放鎖。雖然它缺乏了(經過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,可是卻擁有了鎖獲取與釋放的可操做性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具有的同步特性。html
重入鎖ReentrantLock,顧名思義,就是支持重進入的鎖,它表示該鎖可以支持一個線程對資源的重複加鎖。除此以外,該鎖的還支持獲取鎖時的公平和非公平性選擇。ReentrantLock是java.unti.concurrent包下的一個類,它的通常使用結構以下所示:java
public void lockMethod() {
ReentrantLock myLock = new ReentrantLock();
myLock.lock();
try{
// 受保護的代碼段
//critical section
} finally {
// 能夠保證發生異常 鎖能夠獲得釋放 避免死鎖的發生
myLock.unlock();
}
}複製代碼
在Java的ReentrantLock構造函數中提供了兩種鎖:建立公平鎖和非公平鎖(默認)。代碼以下:node
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}複製代碼
在公平的鎖上,線程按照他們發出請求的順序獲取鎖,但在非公平鎖上,則容許‘插隊’:當一個線程請求非公平鎖時,若是在發出請求的同時該鎖變成可用狀態,那麼這個線程會跳過隊列中全部的等待線程而得到鎖。算法
非公平鎖性能高於公平鎖性能的緣由:
在恢復一個被掛起的線程與該線程真正運行之間存在着嚴重的延遲。編程
以前提到鎖基本都是排他鎖,這些鎖在同一時刻只容許一個線程進行訪問,而讀寫鎖在同一時刻能夠容許多個讀線程訪問,可是在寫線程訪問時,全部的讀線程和其餘寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,經過分離讀鎖和寫鎖,使得併發性相比通常的排他鎖有了很大提高。數組
通常狀況下,讀寫鎖的性能都會比排它鎖好,由於大多數場景讀是多於寫的。在讀多於寫
的狀況下,讀寫鎖可以提供比排它鎖更好的併發性和吞吐量。Java併發包提供讀寫鎖的實現是ReentrantReadWriteLock緩存
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
// 獲取一個key對應的value
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
// 設置key對應的value,並返回舊的value
public static final Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
// 清空全部的內容
public static final void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}複製代碼
Cache組合一個非線程安全的HashMap做爲緩存的實現,同時使用讀寫鎖的
讀鎖和寫鎖來保證Cache是線程安全的。在讀操做get(String key)方法中,須要獲取讀鎖,這使
得併發訪問該方法時不會被阻塞。寫操做put(String key,Object value)方法和clear()方法,在更新
HashMap時必須提早獲取寫鎖,當獲取寫鎖後,其餘線程對於讀鎖和寫鎖的獲取均被阻塞,而
只有寫鎖被釋放以後,其餘讀寫操做才能繼續。安全
Condition是在java 1.5中才出現的,它用來替代傳統的Object的wait()、notify()實現線程間的協做,相比使用Object的wait()、notify(),使用Condition的await()、signal()這種方式實現線程間協做更加安全和高效。bash
調用Condition的await()和signal()方法,都必須在lock保護以內,就是說必須在lock.lock()和lock.unlock之間纔可使用多線程
public class ConTest {
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
public static void main(String[] args) {
// TODO Auto-generated method stub
ConTest test = new ConTest();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
consumer.start();
producer.start();
}
class Consumer extends Thread{
@Override
public void run() {
consume();
}
private void consume() {
try {
lock.lock();
System.out.println("我在等一個新信號"+this.currentThread().getName());
condition.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally{
System.out.println("拿到一個信號"+this.currentThread().getName());
lock.unlock();
}
}
}
class Producer extends Thread{
@Override
public void run() {
produce();
}
private void produce() {
try {
lock.lock();
System.out.println("我拿到鎖"+this.currentThread().getName());
condition.signalAll();
System.out.println("我發出了一個信號:"+this.currentThread().getName());
} finally{
lock.unlock();
}
}
}
}複製代碼
執行結果:
我在等一個新信號Thread-1
我拿到鎖Thread-0
我發出了一個信號:Thread-0
拿到一個信號Thread-1複製代碼
CopyOnWrite容器即寫時複製的容器。通俗的理解是當咱們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,而後新的容器裏添加元素,添加完元素以後,再將原容器的引用指向新的容器。這樣作的好處是咱們能夠對CopyOnWrite容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素。因此CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不一樣的容器
在使用CopyOnWriteArrayList以前,咱們先閱讀其源碼瞭解下它是如何實現的。如下代碼是向ArrayList裏添加元素,能夠發如今添加的時候是須要加鎖的,不然多線程寫的時候會Copy出N個副本出來。
public boolean add(T 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;
// 把原數組引用指向新數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}複製代碼
讀的時候不須要加鎖,若是讀的時候有多個線程正在向ArrayList添加數據,讀仍是會讀到舊的數據,由於寫的時候不會鎖住舊的ArrayList。
public E get(int index) {
return get(getArray(), index);
}複製代碼
ConcurrentHashMap是線程安全且高效的HashMap。ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap裏扮演鎖的角色,HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組,Segment的結構和HashMap相似,是一種數組和鏈表結構,一個Segment裏包含一個HashEntry數組,每一個HashEntry是一個鏈表結構的元素, 每一個Segment守護者一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先得到它對應的Segment鎖。
在多線程環境下,使用HashMap進行put操做會引發死循環,致使CPU利用率接近100%,因此在併發狀況下不能使用HashMap
HashTable容器使用synchronized來保證線程安全,但在線程競爭激烈的狀況下HashTable的效率很是低下。由於當一個線程訪問HashTable的同步方法,其餘線程也訪問HashTable的同步方法時,會進入阻塞或輪詢狀態。如線程1使用put進行元素添加,線程2不但不能使用put方法添加元素,也不能使用get方法來獲取元素,因此競爭越激烈效率越低
HashTable容器在競爭激烈的併發環境下表現出效率低下的緣由是全部訪問HashTable的線程都必須競爭同一把鎖,假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不一樣數據段的數據時,線程間就不會存在鎖競爭,從而能夠有效提升併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將數據分紅一段一段地存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問
在定位元素的代碼裏咱們能夠發現,定位HashEntry和定位Segment的散列算法雖然同樣,都與數組的長度減去1再相「與」,可是相「與」的值不同,定位Segment使用的是元素的hashcode經過再散列後獲得的值的高位,而定位HashEntry直接使用的是再散列後的值。其目的是避免兩次散列後的值同樣,雖然元素在Segment裏散列開了,可是卻沒有在HashEntry裏散列開
hash >>> segmentShift) & segmentMask // 定位Segment所使用的hash算法
int index = hash & (tab.length - 1); // 定位HashEntry所使用的hash算法複製代碼
Segment的get操做實現很是簡單和高效。先通過一次再散列,而後使用這個散列值經過散
列運算定位到Segment,再經過散列算法定位到元素,代碼以下
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}複製代碼
get操做的高效之處在於整個get過程不須要加鎖,除非讀到的值是空纔會加鎖重讀。咱們
知道HashTable容器的get方法是須要加鎖的,那麼ConcurrentHashMap的get操做是如何作到不加鎖的呢?緣由是它的get方法裏將要使用的共享變量都定義成volatile類型,如用於統計當前Segement大小的count字段和用於存儲值的HashEntry的value。定義成volatile的變量,可以在線程之間保持可見性,可以被多線程同時讀,而且保證不會讀到過時的值
因爲put方法裏須要對共享變量進行寫入操做,因此爲了線程安全,在操做共享變量時必須加鎖。put方法首先定位到Segment,而後在Segment裏進行插入操做。插入操做須要經歷兩個步驟,第一步判斷是否須要對Segment裏的HashEntry數組進行擴容,第二步定位添加元素的位置,而後將其放在HashEntry數組裏
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}複製代碼
Segment的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}複製代碼
ConcurrentHashMap的作法是先嚐試2次經過不鎖住Segment的方式來統計各個Segment大小,若是統計的過程當中,容器的count發生了變化,則再採用加鎖的方式來統計全部Segment的大小。
那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢?使用modCount變量,在put、remove和clean方法裏操做元素前都會將變量modCount進行加1,那麼在統計size先後比較modCount是否發生變化,從而得知容器的大小是否發生變化
阻塞隊列(BlockingQueue)是一個支持兩個附加操做的隊列。這兩個附加的操做支持阻塞的插入和移除方法。
插入和移除操做的4中處理方式
方法/處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(e,time,unit) |
檢查方法 | element() | peek() | 不可用 | 不可用 |
JDK 7提供了7個阻塞隊列,以下。
ArrayBlockingQueue是一個用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。默認狀況下不保證線程公平的訪問隊列
LinkedBlockingQueue是一個用鏈表實現的有界阻塞隊列。此隊列的默認和最大長度爲Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。
PriorityBlockingQueue是一個支持優先級的無界阻塞隊列。默認狀況下元素採起天然順序升序排列。也能夠自定義類實現compareTo()方法來指定元素排序規則,或者初始化PriorityBlockingQueue時,指定構造參數Comparator來對元素進行排序。須要注意的是不能保證同優先級元素的順序
DelayQueue是一個支持延時獲取元素的無界阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現Delayed接口,在建立元素時能夠指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素
SynchronousQueue是一個不存儲元素的阻塞隊列。每個put操做必須等待一個take操做,不然不能繼續添加元素
LinkedTransferQueue是一個由鏈表結構組成的無界阻塞TransferQueue隊列。相對於其餘阻塞隊列,LinkedTransferQueue多了tryTransfer和transfer方法。
LinkedBlockingDeque是一個由鏈表結構組成的雙向阻塞隊列。所謂雙向隊列指的是能夠從隊列的兩端插入和移出元素。雙向隊列由於多了一個操做隊列的入口,在多線程同時入隊時,也就減小了一半的競爭。相比其餘的阻塞隊列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法