這裡寫目錄标題
-
- ReentrantLock
- semaphore
- CountDownLatch
- 原子類
- 線程安全的集合
ReentrantLock
ReentrantLock 和 synchronized 一樣,也是一個可重入鎖。
- ReentrantLock 使用方法
import java.util.concurrent.locks.ReentrantLock;
/**
兩個線程操作一個數
*/
public class MyReentrantLock {
static int count;
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread() {
@Override
public void run() {
reentrantLock.lock();
for (int i = 0; i < 100000; i++) {
count++;
}
reentrantLock.unlock();
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
reentrantLock.lock();
for (int i = 0; i < 100000; i++) {
count++;
}
reentrantLock.unlock();
}
};
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
- 差別(參考 ReenTrantLock 與synchronized 差別)
- ReenTrantLock 是 JDK(java 源碼包) 實作的,而 synchronized 是關鍵字,通過 JVM 底層源碼(c++) 實作;
- ReenTrantLock 自己實作加鎖(lock)和解鎖(unlock),而 synchronized 是全套服務,加鎖解鎖自己實作;
- ReenTrantLock 中有 tryLock ,當線程阻塞後,可以手動實作等待時間,或者直接傳回(tryLock() 方法中可以設定等待時間,或者不設定,是以可以避免進入核心态的阻塞狀态,線程進入核心态就是不可控的,往往進入核心态意味着效率更低。是以設計鎖就是避免線程進入核心态)(為什麼有 synchronized 還要有 ReenTrantLock 的原因);
semaphore
semaphore(信号量),簡單點來了解,信号量就是一個計數器,表示可用資源的個數。
怎麼來了解這個 計數器 呢?
類似于停車場入口顯示的車位剩餘量,當一輛車離開停車場,那麼車位剩餘量+1,如果一輛車進入停車場,那麼車位剩餘量-1,如果車位剩餘量為 0 ,那麼車不能挺進去。
semaphore 也是這樣,涉及到兩個操作
P :申請一個可用資源(可用資源-1,也就是剩餘車位數量-1)
V :釋放一個可用資源(可用資源+1,也就是剩餘車位數量+1)
如果信号量為 0 ,也就是可用資源個數是 0,那麼就會阻塞,直到其他線程釋放資源。
注意:而 信号量 + 1 或者 - 1 操作都是 原子性 的。
基于 信号量 的了解,是以可以設計一個鎖
- 信号量隻有 0 和 1;
- 如果一個線程進行 P 操作,那麼信号量 -1,此時如果其他線程想使用此資源,需要阻塞等待;
- 如果線程釋放了資源,那麼信号量 +1,此時阻塞線程可以申請資源。
也可以實作一個 阻塞隊列
需要三個 semaphore,一個代表鎖,一個代表資源的個數,一個代表空位的個數;
CountDownLatch
類似于一個計數器,但是有不同,舉個例子。
比如下載下傳一個遊戲,需要多個線程下載下傳。那麼線程 1 下載下傳好了目前的檔案,而其他線程還沒下載下傳好,是以并不算完全下載下傳成功,等待所有線程都下載下傳成功後,才算真的下載下傳完成。
原子類
原子類能夠保證操作資料時 CPU 的指令是原子的。
- 示例
import java.util.concurrent.atomic.AtomicInteger;
public class MyAtomic {
static AtomicInteger atomicInteger = new AtomicInteger();
static int num = 0;
/**
* 執行 5 次自增對比結果
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 5; i++) {
doIncrement();
System.out.println("atomicInteger"+i + "= " + atomicInteger);
System.out.println("num"+i + "= " + num);
System.out.println("--------分割線---------");
atomicInteger = new AtomicInteger();
num = 0;
}
}
/**
* 建立兩個線程對一個數進行修改
* @throws InterruptedException
*/
public static void doIncrement() throws InterruptedException {
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
atomicInteger.incrementAndGet();
num++;
}
}
};
thread1.start();
Thread thread2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
atomicInteger.incrementAndGet();
num++;
}
}
};
thread2.start();
thread1.join();
thread2.join();
}
}
- 結果
atomicInteger1= 100001
num1= 63105
--------分割線---------
atomicInteger2= 100001
num2= 91873
--------分割線---------
atomicInteger3= 100001
num3= 91147
--------分割線---------
atomicInteger4= 100001
num4= 92904
--------分割線---------
atomicInteger5= 100001
num5= 88765
由結果得知,原子類 AtomicInteger 是線程安全的
線程安全的集合
平時刷題用的一些集合都是線程不安全的,例如
- ArrayList
- LinkedList
- HashSet
- HashMap
當然也有安全的集合,例如
5. Stack
6. Vector
7. HashTable(目前沒用到過)
針對線程不安全的集合,在 Java.util.concurrent 這個包中包含了許多線程安全的集合(标記了部分)
當然,也可以自己實作線程安全的集合,那就是通過加 synchronized 關鍵字。而 HashTable 就是使用了這個方法,保證線程安全的。
而為什麼有了 HashTable 還要有 concurrentHashMap ?(HashTable 和 concurrentHashMap 的差別)
直接加 synchronized 關鍵字必然會導緻效率很低,而 concurrentHashMap 是通過什麼方式保證線程安全呢?
- 每個連結清單/紅黑樹加鎖的方式來保證線程安全(這種分法是 jdk1.8之後的分法,而之前是幾個連結清單/紅黑樹加一個鎖),也就是說,如果線程之間修改的不是同一個資料,那麼就不會導緻線程不安全。
- 針對讀操作,不加鎖,雖然可能會導緻一個線程正在修改資料的時候,另外一個線程剛好讀這個資料,導緻線程不安全。當然可以用讀寫鎖來進行優化。
- 内部廣泛使用 CAS 操作來提高效率,例如擷取元素個數的時候,沒加鎖,直接 CAS;
- 針對擴容進行了優化,假如某一個線程觸發了擴容,那麼當其他線程進行操作的時候,就會一起進行擴容。而 HashTable 則是觸發擴容的線程單獨進行擴容。