0 前言
為何要使用Java執行緒同步? Java允許多執行緒併發控制,當多個執行緒同時操作一個可共享的資源變數時,將會導致資料不準確,相互之間產生衝突,因此加入同步鎖以避免在該執行緒沒有完成操作之前,被其他執行緒的呼叫,從而保證了該變數的唯一性和準確性。
但其併發程式設計的根本,就是使執行緒間進行正確的通訊。其中兩個比較重要的關鍵點,如下:
- 執行緒通訊:重點關注執行緒同步的幾種方式;
- 正確通訊:重點關注是否有執行緒安全問題;
Java中提供了很多執行緒同步操作,比如:synchronized關鍵字、wait/notifyAll、ReentrantLock、Condition、一些併發包下的工具類、Semaphore,ThreadLocal、AbstractQueuedSynchronizer等。本文主要說明一下這幾種同步方式的使用及優劣。
1 ReentrantLock可重入鎖
自JDK5開始,新增了Lock介面以及它的一個實現類ReentrantLock。ReentrantLock可重入鎖是J.U.C包內建的一個鎖物件,可以用來實現同步,基本使用方法如下:
public class ReentrantLockTest {
private ReentrantLock lock = new ReentrantLock();
public void execute() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " do something synchronize");
try {
Thread.sleep(5000l);
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " interrupted");
Thread.currentThread().interrupt();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLockTest.execute();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLockTest.execute();
}
});
thread1.start();
thread2.start();
}
}
複製程式碼
上面例子表示 同一時間段只能有1個執行緒執行execute方法,輸出如下:
Thread-0 do something synchronize
// 隔了5秒鐘 輸入下面
Thread-1 do something synchronize
複製程式碼
可重入鎖中可重入表示的意義在於 對於同一個執行緒,可以繼續呼叫加鎖的方法,而不會被掛起。可重入鎖內部維護一個計數器,對於同一個執行緒呼叫lock方法,計數器+1,呼叫unlock方法,計數器-1。
舉個例子再次說明一下可重入的意思:在一個加鎖方法execute中呼叫另外一個加鎖方法anotherLock並不會被掛起,可以直接呼叫(呼叫execute方法時計數器+1,然後內部又呼叫了anotherLock方法,計數器+1,變成了2):
public void execute() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " do something synchronize");
try {
anotherLock();
Thread.sleep(5000l);
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " interrupted");
Thread.currentThread().interrupt();
}
} finally {
lock.unlock();
}
}
public void anotherLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " invoke anotherLock");
} finally {
lock.unlock();
}
}
複製程式碼
輸出:
Thread-0 do something synchronize
Thread-0 invoke anotherLock
// 隔了5秒鐘 輸入下面
Thread-1 do something synchronize
Thread-1 invoke anotherLock
複製程式碼
2 synchronized
synchronized跟ReentrantLock一樣,也支援可重入鎖。但是它是 一個關鍵字,是一種語法級別的同步方式,稱為內建鎖:
public class SynchronizedKeyWordTest {
public synchronized void execute() {
System.out.println(Thread.currentThread().getName() + " do something synchronize");
try {
anotherLock();
Thread.sleep(5000l);
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " interrupted");
Thread.currentThread().interrupt();
}
}
public synchronized void anotherLock() {
System.out.println(Thread.currentThread().getName() + " invoke anotherLock");
}
public static void main(String[] args) {
SynchronizedKeyWordTest reentrantLockTest = new SynchronizedKeyWordTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLockTest.execute();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLockTest.execute();
}
});
thread1.start();
thread2.start();
}
}
複製程式碼
輸出結果跟ReentrantLock一樣,這個例子說明內建鎖可以作用在方法上。synchronized關鍵字也可以修飾靜態方法,此時如果呼叫該靜態方法,將會鎖住整個類。
同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized程式碼塊同步關鍵程式碼即可。
synchronized跟ReentrantLock相比,有幾點侷限性:
- 加鎖的時候不能設定超時。ReentrantLock有提供tryLock方法,可以設定超時時間,如果超過了這個時間並且沒有獲取到鎖,就會放棄,而synchronized卻沒有這種功能;
- ReentrantLock可以使用多個Condition,而synchronized卻只能有1個
- 不能中斷一個試圖獲得鎖的執行緒;
- ReentrantLock可以選擇公平鎖和非公平鎖;
- ReentrantLock可以獲得正在等待執行緒的個數,計數器等;
所以,Lock的操作與synchronized相比,靈活性更高,而且Lock提供多種方式獲取鎖,有Lock、ReadWriteLock介面,以及實現這兩個介面的ReentrantLock類、ReentrantReadWriteLock類。
關於Lock物件和synchronized關鍵字選擇的考量:
- 最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助使用者處理所有與鎖相關的程式碼。
- 如果synchronized關鍵字能滿足使用者的需求,就用synchronized,因為它能簡化程式碼。
- 如果需要更高階的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally程式碼釋放鎖。
在效能考量上來說,如果競爭資源不激烈,兩者的效能是差不多的,而當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的效能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。
3 Condition條件物件
Condition條件物件的意義在於 對於一個已經獲取Lock鎖的執行緒,如果還需要等待其他條件才能繼續執行的情況下,才會使用Condition條件物件。
Condition可以替代傳統的執行緒間通訊,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。
為什麼方法名不直接叫wait()/notify()/nofityAll()?因為Object的這幾個方法是final的,不可重寫!
public class ConditionTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " run");
System.out.println(Thread.currentThread().getName() + " wait for condition");
try {
condition.await();
System.out.println(Thread.currentThread().getName() + " continue");
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " interrupted");
Thread.currentThread().interrupt();
}
} finally {
lock.unlock();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " run");
System.out.println(Thread.currentThread().getName() + " sleep 5 secs");
try {
Thread.sleep(5000l);
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " interrupted");
Thread.currentThread().interrupt();
}
condition.signalAll();
} finally {
lock.unlock();
}
}
});
thread1.start();
thread2.start();
}
}
複製程式碼
這個例子中thread1執行到condition.await()時,當前執行緒會被掛起,直到thread2呼叫了condition.signalAll()方法之後,thread1才會重新被啟用執行。
這裡需要注意的是thread1呼叫Condition的await方法之後,thread1執行緒釋放鎖,然後馬上加入到Condition的等待佇列,由於thread1釋放了鎖,thread2獲得鎖並執行,thread2執行signalAll方法之後,Condition中的等待佇列thread1被取出並加入到AQS中,接下來thread2執行完畢之後釋放鎖,由於thread1已經在AQS的等待佇列中,所以thread1被喚醒,繼續執行。
傳統執行緒的通訊方式,Condition都可以實現。Condition的強大之處在於它可以為多個執行緒間建立不同的Condition。
注意,Condition是被繫結到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。
4 wait¬ify/notifyAll方式
Java執行緒的狀態轉換圖與相關方法,如下:
在圖中,紅框標識的部分方法,可以認為已過時,不再使用。上圖中的方法能夠參與到執行緒同步中的方法,如下:
-
wait、notify、notifyAll方法:執行緒中通訊可以使用的方法。執行緒中呼叫了wait方法,則進入阻塞狀態,只有等另一個執行緒呼叫與wait同一個物件的notify方法。這裡有個特殊的地方,呼叫wait或者notify,前提是需要獲取鎖,也就是說,需要在同步塊中做以上操作。
wait/notifyAll方式跟ReentrantLock/Condition方式的原理是一樣的。
Java中每個物件都擁有一個內建鎖,在內建鎖中呼叫wait,notify方法相當於呼叫鎖的Condition條件物件的await和signalAll方法。
public class WaitNotifyAllTest { public synchronized void doWait() { System.out.println(Thread.currentThread().getName() + " run"); System.out.println(Thread.currentThread().getName() + " wait for condition"); try { this.wait(); System.out.println(Thread.currentThread().getName() + " continue"); } catch (InterruptedException e) { System.err.println(Thread.currentThread().getName() + " interrupted"); Thread.currentThread().interrupt(); } } public synchronized void doNotify() { try { System.out.println(Thread.currentThread().getName() + " run"); System.out.println(Thread.currentThread().getName() + " sleep 5 secs"); Thread.sleep(5000l); this.notifyAll(); } catch (InterruptedException e) { System.err.println(Thread.currentThread().getName() + " interrupted"); Thread.currentThread().interrupt(); } } public static void main(String[] args) { WaitNotifyAllTest waitNotifyAllTest = new WaitNotifyAllTest(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { waitNotifyAllTest.doWait(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { waitNotifyAllTest.doNotify(); } }); thread1.start(); thread2.start(); } } 複製程式碼
這裡需要注意的是 呼叫wait/notifyAll方法的時候一定要獲得當前執行緒的鎖,否則會發生IllegalMonitorStateException異常。
-
join方法:該方法主要作用是在該執行緒中的run方法結束後,才往下執行。
package com.thread.simple; public class ThreadJoin { public static void main(String[] args) { Thread thread= new Thread(new Runnable() { @Override public void run() { System.err.println("執行緒"+Thread.currentThread().getId()+" 列印資訊"); } }); thread.start(); try { thread.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.err.println("主執行緒列印資訊"); } } 複製程式碼
-
yield方法:執行緒本身的排程方法,使用時執行緒可以在run方法執行完畢時,呼叫該方法,告知執行緒已可以出讓CPU資源。
public class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("低階", 1).start(); new MyThread("中級", 5).start(); new MyThread("高階", 10).start(); } } class MyThread extends Thread { public MyThread(String name, int pro) { super(name);// 設定執行緒的名稱 this.setPriority(pro);// 設定優先順序 } @Override public void run() { for (int i = 0; i < 30; i++) { System.out.println(this.getName() + "執行緒第" + i + "次執行!"); if (i % 5 == 0) Thread.yield(); } } } 複製程式碼
-
sleep方法:通過sleep(millis)使執行緒進入休眠一段時間,該方法在指定的時間內無法被喚醒,同時也不會釋放物件鎖;
/** * 可以明顯看到列印的數字在時間上有些許的間隔 */ public class Test1 { public static void main(String[] args) throws InterruptedException { for(int i=0;i<100;i++){ System.out.println("main"+i); Thread.sleep(100); } } } 複製程式碼
sleep方法告訴作業系統 至少在指定時間內不需為執行緒排程器為該執行緒分配執行時間片,並不釋放鎖(如果當前已經持有鎖)。實際上,呼叫sleep方法時並不要求持有任何鎖。
所以,sleep方法並不需要持有任何形式的鎖,也就不需要包裹在synchronized中。
5 ThreadLocal
ThreadLocal是一種把變數放到執行緒本地的方式來實現執行緒同步的。比如:SimpleDateFormat不是一個執行緒安全的類,可以使用ThreadLocal實現同步,如下:
public class ThreadLocalTest {
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Date date = new Date();
System.out.println(dateFormatThreadLocal.get().format(date));
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Date date = new Date();
System.out.println(dateFormatThreadLocal.get().format(date));
}
});
thread1.start();
thread2.start();
}
}
複製程式碼
為何SimpleDateFormat不是執行緒安全的類?具體請參考:
- https://blog.csdn.net/zdp072/article/details/41044059
- https://blog.csdn.net/zq602316498/article/details/40263083
ThreadLocal與同步機制的對比選擇:
- ThreadLocal與同步機制都是 為了解決多執行緒中相同變數的訪問衝突問題。
- 前者採用以 "空間換時間" 的方法,後者採用以 "時間換空間" 的方式。
6 volatile修飾變數
volatile關鍵字為域變數的訪問提供了一種免鎖機制,使用volatile修飾域相當於告訴虛擬機器該域可能會被其他執行緒更新,因此每次使用該域就要重新計算,而不是使用暫存器中的值,volatile不會提供任何原子操作,它也不能用來修飾final型別的變數。
//只給出要修改的程式碼,其餘程式碼與上同
public class Bank {
//需要同步的變數加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//這裡不再需要synchronized
public void save(int money) {
account += money;
}
}
複製程式碼
多執行緒中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。用final域,有鎖保護的域和volatile域可以避免非同步的問題。
7 Semaphore訊號量
Semaphore訊號量被用於控制特定資源在同一個時間被訪問的個數。類似連線池的概念,保證資源可以被合理的使用。可以使用構造器初始化資源個數:
public class SemaphoreTest {
private static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
for(int i = 0; i < 5; i ++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " " + new Date());
Thread.sleep(5000l);
semaphore.release();
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " interrupted");
}
}
}).start();
}
}
}
複製程式碼
輸出:
Thread-1 Mon Apr 18 18:03:46 CST 2016
Thread-0 Mon Apr 18 18:03:46 CST 2016
Thread-3 Mon Apr 18 18:03:51 CST 2016
Thread-2 Mon Apr 18 18:03:51 CST 2016
Thread-4 Mon Apr 18 18:03:56 CST 2016
複製程式碼
8 併發包下的工具類
8.1 CountDownLatch
CountDownLatch是一個計數器,它的構造方法中需要設定一個數值,用來設定計數的次數。每次呼叫countDown()方法之後,這個計數器都會減去1,CountDownLatch會一直阻塞著呼叫await()方法的執行緒,直到計數器的值變為0。
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(5);
for(int i = 0; i < 5; i ++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " " + new Date() + " run");
try {
Thread.sleep(5000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("all thread over");
}
}
複製程式碼
輸出:
Thread-2 Mon Apr 18 18:18:30 CST 2016 run
Thread-3 Mon Apr 18 18:18:30 CST 2016 run
Thread-4 Mon Apr 18 18:18:30 CST 2016 run
Thread-0 Mon Apr 18 18:18:30 CST 2016 run
Thread-1 Mon Apr 18 18:18:30 CST 2016 run
all thread over
複製程式碼
8.2 CyclicBarrier
CyclicBarrier阻塞呼叫的執行緒,直到條件滿足時,阻塞的執行緒同時被開啟。
呼叫await()方法的時候,這個執行緒就會被阻塞,當呼叫await()的執行緒數量到達屏障數的時候,主執行緒就會取消所有被阻塞執行緒的狀態。
在CyclicBarrier的構造方法中,還可以設定一個barrierAction。在所有的屏障都到達之後,會啟動一個執行緒來執行這裡面的程式碼。
public class CyclicBarrierTest {
public static void main(String[] args) {
Random random = new Random();
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for(int i = 0; i < 5; i ++) {
new Thread(new Runnable() {
@Override
public void run() {
int secs = random.nextInt(5);
System.out.println(Thread.currentThread().getName() + " " + new Date() + " run, sleep " + secs + " secs");
try {
Thread.sleep(secs * 1000);
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + new Date() + " runs over");
}
}).start();
}
}
}
複製程式碼
相比CountDownLatch,CyclicBarrier是可以被迴圈使用的,而且遇到執行緒中斷等情況時,還可以利用reset()方法,重置計數器,從這些方面來說,CyclicBarrier會比CountDownLatch更加靈活一些。
9 使用原子變數實現執行緒同步
有時需要使用執行緒同步的根本原因在於 對普通變數的操作不是原子的。那麼什麼是原子操作呢?
原子操作就是指將讀取變數值、修改變數值、儲存變數值看成一個整體來操作 即-這幾種行為要麼同時完成,要麼都不完成。
在java.util.concurrent.atomic包中提供了建立原子型別變數的工具類,使用該類可以簡化執行緒同步。比如:其中AtomicInteger以原子方式更新int的值:
class Bank {
private AtomicInteger account = new AtomicInteger(100);
public AtomicInteger getAccount() {
return account;
}
public void save(int money) {
account.addAndGet(money);
}
}
複製程式碼
10 AbstractQueuedSynchronizer
AQS是很多同步工具類的基礎,比如:ReentrantLock裡的公平鎖和非公平鎖,Semaphore裡的公平鎖和非公平鎖,CountDownLatch裡的鎖等他們的底層都是使用AbstractQueuedSynchronizer完成的。
基於AbstractQueuedSynchronizer自定義實現一個獨佔鎖:
public class MySynchronizer extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setState(0);
setExclusiveOwnerThread(null);
return true;
}
public void lock() {
acquire(1);
}
public void unlock() {
release(1);
}
public static void main(String[] args) {
MySynchronizer mySynchronizer = new MySynchronizer();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
mySynchronizer.lock();
try {
System.out.println(Thread.currentThread().getName() + " run");
System.out.println(Thread.currentThread().getName() + " will sleep 5 secs");
try {
Thread.sleep(5000l);
System.out.println(Thread.currentThread().getName() + " continue");
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " interrupted");
Thread.currentThread().interrupt();
}
} finally {
mySynchronizer.unlock();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
mySynchronizer.lock();
try {
System.out.println(Thread.currentThread().getName() + " run");
} finally {
mySynchronizer.unlock();
}
}
});
thread1.start();
thread2.start();
}
}
複製程式碼
11 使用阻塞佇列實現執行緒同步
前面幾種同步方式都是基於底層實現的執行緒同步,但是在實際開發當中,應當儘量遠離底層結構。本節主要是使用LinkedBlockingQueue來實現執行緒的同步。
LinkedBlockingQueue是一個基於連結串列的佇列,先進先出的順序(FIFO),範圍任意的blocking queue。
package com.xhj.thread;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 用阻塞佇列實現執行緒同步 LinkedBlockingQueue的使用
*/
public class BlockingSynchronizedThread {
/**
* 定義一個阻塞佇列用來儲存生產出來的商品
*/
private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
/**
* 定義生產商品個數
*/
private static final int size = 10;
/**
* 定義啟動執行緒的標誌,為0時,啟動生產商品的執行緒;為1時,啟動消費商品的執行緒
*/
private int flag = 0;
private class LinkBlockThread implements Runnable {
@Override
public void run() {
int new_flag = flag++;
System.out.println("啟動執行緒 " + new_flag);
if (new_flag == 0) {
for (int i = 0; i < size; i++) {
int b = new Random().nextInt(255);
System.out.println("生產商品:" + b + "號");
try {
queue.put(b);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("倉庫中還有商品:" + queue.size() + "個");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} else {
for (int i = 0; i < size / 2; i++) {
try {
int n = queue.take();
System.out.println("消費者買去了" + n + "號商品");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("倉庫中還有商品:" + queue.size() + "個");
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
}
}
}
}
public static void main(String[] args) {
BlockingSynchronizedThread bst = new BlockingSynchronizedThread();
LinkBlockThread lbt = bst.new LinkBlockThread();
Thread thread1 = new Thread(lbt);
Thread thread2 = new Thread(lbt);
thread1.start();
thread2.start();
}
}
複製程式碼