JAVA多線程——(二)多線程編程
文章目錄
【一】ReentrantLock
雖然在性能上ReentrantLock和synchronized沒有什麼區別,但ReentrantLock相比synchronized而言功能更加豐富,使用起來更爲靈活,也更適合複雜的併發場景。
-
lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。
-
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待
-
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
-
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態
-
我們可以在創建ReentrantLock對象時,通過以下方式來設置鎖的公平性:
ReentrantLock lock = new ReentrantLock(true);
public class Main {
Lock lock = new ReentrantLock();
AtomicInteger integer = new AtomicInteger(1);
public void print(){
//使用ReentrantLock加鎖的時候,必須在finally中釋放鎖,不然可能造成死鎖
lock.lock();
try {
System.out.println(integer.get());
integer.getAndIncrement();
}finally {
lock.unlock();
}
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.print();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.print();
}
});
thread1.start();
thread2.start();
}
}
Lock詳解:https://blog.csdn.net/wenge1477/article/details/98476731
Lock和synchronized差別:https://blog.csdn.net/wenge1477/article/details/98533427
【二】ReadWriteLock
讀寫鎖是一種通用技術,在其他編程語言或者數據庫中都有對應實現。讀寫鎖一般遵守下面三條規則:
- 允許多個線程獲取讀鎖
- 只允許一個線程獲取寫鎖
- 如果某個線程獲取了寫鎖,其他線程不能再獲取讀鎖
由於規則1多個線程在只讀的情況下可以同時讀取數據獲取共享變量,所以讀寫鎖優於互斥鎖。
讀寫鎖適用於讀多寫少:
ReadWriteLock接口的兩個方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
例子:
public class Main {
ReadWriteLock lock = new ReentrantReadWriteLock();
AtomicInteger count = new AtomicInteger(1);
public void get(){
//獲取數據時,獲取讀鎖
Lock readLock = this.lock.readLock();
readLock.lock();
try {
System.out.println(count);
}finally {
readLock.unlock();
}
}
public void add(){
//寫的時候,獲取寫鎖
Lock writeLock = this.lock.writeLock();
writeLock.lock();
try {
count.getAndIncrement();
}finally {
writeLock.unlock();
}
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.get();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.add();
}
});
thread1.start();
thread2.start();
}
}
【三】Condition
在java Lock體系下依然會有同樣的方法實現等待/通知機制,而Condition與Lock配合完成等待通知機制。
- Condition能夠支持不響應中斷,而通過使用Object方式不支持;
- Condition能夠支持多個等待隊列(new 多個Condition對象),而Object方式只能支持一個;
- Condition能夠支持超時時間的設置,而Object不支持
await() :造成當前線程在接到信號或被中斷之前一直處於等待狀態。
await(long time, TimeUnit unit) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。
awaitNanos(long nanosTimeout) :造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。返回值表示剩餘時間,如果在nanosTimesout之前喚醒,那麼返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了。
awaitUninterruptibly() :造成當前線程在接到信號之前一直處於等待狀態。【注意:該方法對中斷不敏感】。
awaitUntil(Date deadline) :造成當前線程在接到信號、被中斷或到達指定最後期限之前一直處於等待狀態。如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false。
signal() :喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。
signal()All :喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖。
例子:
public class Main {
public Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public Stack<Integer> stack = new Stack();
public int i = 1;
public void producer(){
lock.lock();
try {
while (stack.isEmpty()){
stack.push(new Integer(i++));
condition.signalAll();
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consumer(){
lock.lock();
try {
while (!stack.isEmpty()){
System.out.println(stack.pop());
condition.signalAll();
condition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
main.producer();
});
Thread thread2 = new Thread(()->{
main.consumer();
});
thread1.start();
thread2.start();
}
}
【四】併發容器
- ConcurrentHashMap
主要爲了解決HashMap線程不安全和Hashtable效率不高的問題。
1、JDK7版本
分段鎖機制,簡而言之,ConcurrentHashMap在對象中保存了一個Segment數組,即將整個Hash表劃分爲多個分段;而每個Segment元素,即每個分段則類似於一個Hashtable
ConcurrentHashMap的數據結構:
ConcurrentHashMap類結構如上圖所示。由圖可知,在ConcurrentHashMap中,定義了一個Segment<K, V>[]數組來將Hash表實現分段存儲,從而實現分段加鎖;而麼一個Segment元素則與HashMap結構類似,其包含了一個HashEntry數組,用來存儲Key/Value對。Segment繼承了ReetrantLock,表示Segment是一個可重入鎖,因此ConcurrentHashMap通過可重入鎖對每個分段進行加鎖
2、JDK8版本
在JDK1.8中,而是選擇了與HashMap類似的數組+鏈表+紅黑樹的方式實現,而加鎖則採用CAS和synchronized實現
CAS(Compare And Swap,比較交換):CAS有三個操作數,內存值V、預期值A、要修改的新值B,當且僅當A和V相等時纔會將V修改爲B,否則什麼都不做。
JDK1.8的數據結構:
public class Main {
private ConcurrentHashMap<String,Character> map = new ConcurrentHashMap<>();
public void producer(String key,char value){
map.put(key,value);
}
public void consumer(String key){
Character value = map.get(key);
System.out.println(value);
}
public static void main(String args[]){
Main main = new Main();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.producer(String.valueOf(i), (char) ('a'+i));
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
main.consumer(String.valueOf(i));
}
});
thread1.start();
thread2.start();
}
}
- ConcurrentLinkedQueue
ConcurrentLinkedQueue是一個基於鏈表的無界非阻塞隊列,並且是線程安全的,它採用的是先進先出的規則,當我們增加一個元素時,它會添加到隊列的末尾,當我們取一個元素時,它會返回一個隊列頭部的元素。
offer():將指定的元素插入隊列的尾部
poll() :獲取並移除隊列的頭,如果隊列爲空則返回null
peek():獲取表頭元素但不移除隊列的頭,如果隊列爲空則返回null。
remove(Object obj):移除隊列已存在的元素,返回true,
如果元素不存在,返回false。
add(E e):將指定元素插入隊列末尾,成功返回true,
失敗返回false(此方法非線程安全的方法,不推薦使用)。
注意:
雖然ConcurrentLinkedQueue的性能很好,
但是在調用size()方法的時候,會遍歷一遍集合
對性能損害較大,執行很慢,因此應該儘量的減少使用這個方法,
如果判斷是否爲空,最好用isEmpty()方法。
ConcurrentLinkedQueue不允許插入null元素,會拋出空指針異常。
ConcurrentLinkedQueue是無界的,所以使用時,
一定要注意內存溢出的問題。即對併發不是很大中等的情況下使用,
不然佔用內存過多或者溢出,對程序的性能影響很大,甚至是致命的。
- CopyOnWriteArrayList
1、實現了List接口
2、內部持有一個ReentrantLock lock = new ReentrantLock();
3、底層是用volatile transient聲明的數組 array
4、讀寫分離,寫時複製出一個新的數組,完成插入、
修改或者移除操作後將新數組賦值給array
性能上:
Vector是增刪改查方法都加了synchronized,保證同步,但是每個方法執行的時候都要去獲得鎖,性能就會大大下降。
而CopyOnWriteArrayList 只是在增刪改上加鎖,但是讀不加鎖,在讀方面的性能就好於Vector,CopyOnWriteArrayList支持讀多寫少的併發情況
-
CopyOnWriteArraySet
-
BlockingQueue
【五】Atomic
-
線程安全的幾個問題:
1、原子性:提供了互斥訪問,同一時刻只能有一個線程對它進行操作
2、可見性:一個線程對主內存的修改可以及時的被其他線程觀察到
3、有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序 -
原子更新基本類型
AtomicBoolean: 原子更新布爾類型。
AtomicInteger: 原子更新整型。
AtomicLong: 原子更新長整型。
- 原子更新數組
AtomicIntegerArray: 原子更新整型數組裏的元素。
AtomicLongArray: 原子更新長整型數組裏的元素。
AtomicReferenceArray: 原子更新引用類型數組裏的元素。
- 原子更新引用類型
AtomicReference: 原子更新引用類型。
AtomicReferenceFieldUpdater: 原子更新引用類型的字段。
AtomicMarkableReferce: 原子更新帶有標記位的引用類型
- 原子更新字段類
AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
AtomicLongFieldUpdater: 原子更新長整型字段的更新器。
AtomicStampedFieldUpdater: 原子更新帶有版本號的引用類型。
AtomicReferenceFieldUpdater: 上面已經說過此處不在贅述。
【六】ExecutorService
-
一、線程池: 提供一個線程隊列,隊列中保存着所有等待狀態的線程。避免了創建與銷燬的額外開銷,提高了響應的速度。
-
二、線程池的體系結構:
java.util.concurrent.Executor 負責線程的使用和調度的根接口
|–ExecutorService 子接口: 線程池的主要接口
|–ThreadPoolExecutor 線程池的實現類
|–ScheduledExceutorService 子接口: 負責線程的調度
|–ScheduledThreadPoolExecutor : 繼承ThreadPoolExecutor,實現了ScheduledExecutorService -
三、工具類 : Executors
1、ExecutorService newFixedThreadPool() : 創建固定大小的線程池
2、ExecutorService newCachedThreadPool() : 緩存線程池,線程池的數量不固定,可以根據需求自動的更改數量。
3、ExecutorService newSingleThreadExecutor() : 創建單個線程池。 線程池中只有一個線程
4、ScheduledExecutorService newScheduledThreadPool() : 創建固定大小的線程,可以延遲或定時的執行任務
【七】CountDownLatch
CountDownLatch,英文翻譯爲倒計時鎖存器,是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。
1、確保某個計算在其需要的所有資源都被初始化之後才繼續執行;
2、確保某個服務在其依賴的所有其他服務都已經啓動之後才啓動;
3、等待直到某個操作所有參與者都準備就緒再繼續執行。
用法:
-
第一種:
某一線程在開始運行前等待n個線程執行完畢。
將 CountDownLatch 的計數器初始化爲n :new CountDownLatch(n),每當一個任務線程執行完畢,就將計數器減1 countdownlatch.countDown(),當計數器的值變爲0時,在CountDownLatch上 await() 的線程就會被喚醒。一個典型應用場景就是啓動一個服務時,主線程需要等待多個組件加載完畢,之後再繼續執行。 -
第二種:
實現多個線程開始執行任務的最大並行性。
注意是並行性,不是併發,強調的是多個線程在某一時刻同時開始執行。類似於賽跑,將多個線程放到起點,等待發令槍響,然後同時開跑。做法是初始化一個共享的 CountDownLatch 對象,將其計數器初始化爲 1 :new CountDownLatch(1),多個線程在開始執行任務前首先 coundownlatch.await(),當主線程調用 countDown() 時,計數器變爲0,多個線程同時被喚醒。
例子:計算多線程耗時
public class TestCountDownLatch {
public static void main(String[] args){
//CountDownLatch 爲唯一的、共享的資源
final CountDownLatch latch = new CountDownLatch(5);
LatchDemo latchDemo = new LatchDemo(latch);
long begin = System.currentTimeMillis();
for (int i = 0; i <5 ; i++) {
new Thread(latchDemo).start();
}
try {
//多線程運行結束前一直等待
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("耗費時間:"+(end-begin));
}
}
class LatchDemo implements Runnable{
private CountDownLatch latch;
public LatchDemo(CountDownLatch latch){
this.latch=latch;
}
public LatchDemo(){
super();
}
@Override
public void run() {
//當前對象唯一,使用當前對象加鎖,避免多線程問題
synchronized (this){
try {
for (int i = 0; i < 50000; i++) {
if (i%2==0){
System.out.println(i);
}
}
}finally {
//保證肯定執行
latch.countDown();
}
}
}
}
【八】CyclicBarrier
CyclicBarrier可以使一定數量的線程反覆地在柵欄位置處彙集。當線程到達柵欄位置時將調用await方法,這個方法將阻塞直到所有線程都到達柵欄位置。如果所有線程都到達柵欄位置,那麼柵欄將打開,此時所有的線程都將被釋放,而柵欄將被重置以便下次使用。
CyclicBarrier內部使用了ReentrantLock和Condition兩個類。
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程使用await()方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。
CyclicBarrier的另一個構造函數CyclicBarrier(int parties, Runnable barrierAction),用於線程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景。
public class CyclicBarrierTest {
// 自定義工作線程
private static class Worker extends Thread {
private CyclicBarrier cyclicBarrier;
public Worker(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
super.run();
try {
System.out.println(Thread.currentThread().getName() + "開始等待其他線程");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "開始執行");
// 工作線程開始處理,這裏用Thread.sleep()來模擬業務處理
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "執行完畢");
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for (int i = 0; i < threadCount; i++) {
System.out.println("創建工作線程" + i);
Worker worker = new Worker(cyclicBarrier);
worker.start();
}
}
}
【九】Volatile
請查看博客:https://blog.csdn.net/wenge1477/article/details/98476642
【十】ThreadLocal
請查看博客:https://blog.csdn.net/wenge1477/article/details/98476832