本文主要說明的是Java開發中涉及到的多線程編程的資源共享問題,即同步問題。
目前Java中可以支持資源同步的方法有:
- volatile關鍵字
- synchronized關鍵字
但是上述兩種方法還是有些許區別的,以下舉例來探討。
首先我們來看如下代碼,
int n = 0;
// 最終結果不足5000
public void testOfVolatile() {
// n是一般的對象屬性,啓用多線程去增加它的大小,但是最終無法保證是累加的預期值。
// 因爲線程中不同步,並且CPU在執行的時候會使用緩存,指令執行數據是在緩存-內存中執行的,導致
// 多線程訪問的時候數據不是同步狀態。
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
n ++;
i ++;
}
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
return i;
}
});
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
n ++;
i ++;
}
i = 0;
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
}
};
Thread thread1 = new Thread(futureTask);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
// join當前線程
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final n: " + n);
}
如上述代碼所示,對象屬性n是一個普通的變量,在測試方法中,直接啓用了5個線程來進行對n的增加,每個線程都增加10000的增量。期望是能夠讓n從0變化到50000。但是實際結果卻不是這麼回事,實際結果只有40000多,不到50000。這是因爲該變量n,是一個普通變量,多線程訪問該變量的時候不會處理同步問題,即每個線程在訪問該變量的時候,不一定變量n的值是當前最新的狀態,也許已經被其他線程改掉,但是還沒有寫入CPU的緩存中,導致變量數據不同步。
volatile
這個關鍵字描述的功能即表示,當前變量在被不同線程訪問的時候是能夠保證最新狀態,即最新值的,但是在線程寫入的時候卻不會保證同步,所以不會造成線程阻塞,理論上它依舊不是線程安全的。
同樣我們使用violatile關鍵字來修飾一個變量,看下面的代碼:
volatile int m = 0;
// 最終結果不足50000
private void testOfVolatile2() {
// m是volatile修飾的對象屬性,啓用多線程去增加它的大小,但是最終無法保證是累加的預期值。
// 因爲volatile僅保持可見性,但是不保證同步性,所以它不是線程安全的,僅僅保證不會阻塞。
// volatile保證讀取的是最新的值,但是不保證寫的時候是同步狀態。
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Thread " + Thread.currentThread().getId() + " started, m: " + m);
int i = 0;
while(i < 10 * 1000) {
m ++;
i ++;
}
System.out.println("Thread " + Thread.currentThread().getId() + " finished, m: " + m);
return i;
}
});
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getId() + " started, m: " + m);
int i = 0;
while(i < 10 * 1000) {
m ++;
i ++;
}
i = 0;
System.out.println("Thread " + Thread.currentThread().getId() + " finished, m: " + m);
}
};
Thread thread1 = new Thread(futureTask);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
// join當前線程
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final m: " + m);
}
通過上述代碼,我們得到的結果依舊不是50000,只是在某些線程開始工作訪問m變量的時候,與先前一個結束的線程最終的m值相同,即表示volatile保證了可見性。但是無法在其寫入的時候與其他線程保持同步,即不具備原子性。如此依賴,volatile是輕量級同步,它是在準備訪問對象的時候不從CPU的緩存中讀取,而是直接從主存中讀取,而寫入的時候則也是直接寫入主存。
需要注意的是,volatile只能夠修飾變量,不能夠修飾方法。任何依賴於之前值的操作, 如i++, i = i *10使用volatile都不安全,
而諸如get/set, boolean這類可以使用volatile。
synchronized
synchronized關鍵字在實際應用中應該遇到很多,主要是通過對象鎖來控制不同線程對同一變量對象的訪問。我們先來看下如下代碼:
public synchronized void plusNSync() {
n ++;
}
// 最終結果剛好50000
private void testOfSynchronized() {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
plusNSync();
i ++;
}
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
return i;
}
});
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getId() + " started, n: " + n);
int i = 0;
while(i < 10 * 1000) {
plusNSync();
i ++;
}
i = 0;
System.out.println("Thread " + Thread.currentThread().getId() + " finished, n: " + n);
}
};
Thread thread1 = new Thread(futureTask);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
// join當前線程
try {
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final n: " + n);
}
在該段代碼中,我們繼續使用普通對象屬性n,該變量未作任何修飾,但是我們爲它增加了一個synchronized關鍵字修飾的方法。該方法使用當前的測試類對象鎖來完成線程訪問同步,而累加n的方法也僅僅在該方法中進行,每個線程都調用該方法plusNSync()。結果自然明瞭,最終的結果顯示50000。
這裏表示synchronized是重量級同步,不僅能夠修飾變量,還能夠修飾方法,它具備可見性及原子性。
但是需要注意:
- synchronized對象鎖是針對堆內存中的對象,而不是棧中的對象引用,因此,當對象引用改變之後,那麼當前執行環境下的同步鎖也就失效了。
- 不要使用String類型的對象作爲同步對象,因爲String在String池中的不確定性也會導致各種問題。
ThreadLocal
對於ThreadLocal的誤解主要有:
- ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路
- ThreadLocal的目的是爲了解決多線程訪問資源時的共享問題
還有很多文章在對比 ThreadLocal 與 synchronize 的異同。既然是作比較,那應該是認爲這兩者解決相同或類似的問題。上面的描述,問題在於,ThreadLocal 並不解決多線程 共享 變量的問題。既然變量不共享,那就更談不上同步的問題。
對於ThreadLocal的合理解釋,ThreadLoal 變量,它的基本原理是,同一個 ThreadLocal 所包含的對象(對ThreadLocal< String >而言即爲 String 類型變量),在不同的線程中有不同的副本,及維護各自的變量對象。那麼這裏幾點要注意:
- 因爲每個 Thread 內有自己的實例副本,且該副本只能由當前 Thread 使用。這是也是 ThreadLocal 命名的由來
- 既然每個 Thread 有自己的實例副本,且其它 Thread 不可訪問,那就不存在多線程間共享的問題
- 既無共享,何來同步問題,又何來解決同步問題一說?
既然如此,那麼ThreadLocal的合理應用場景又是如何?引用它的官方解釋如下:
“ThreadLocal 提供了線程本地的實例。它與普通變量的區別在於,每個使用該變量的線程都會初始化一個完全獨立的實例副本。ThreadLocal 變量通常被private static
修飾。當一個線程結束時,它所使用的所有 ThreadLocal 相對的實例副本都可被回收。”
總體來說,ThreadLocal 適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景。後文會通過實例詳細闡述該觀點。另外,該場景下,並非必須使用 ThreadLocal ,其它方式完全可以實現同樣的效果,只是 ThreadLocal 使得實現更簡潔。
我們來看如下代碼:
public class TestOfThreadLocal {
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 0
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 0
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 0
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 01
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 01
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 012
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 01
// Thread name: Thread-2, ThreadLocal hashCode: 1001240990, Instance hashCode: 297626270, Value: 0123
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 012
// innerClass thread Thread-2 finished.
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 012
// Thread name: Thread-1, ThreadLocal hashCode: 1001240990, Instance hashCode: 896159899, Value: 0123
// Thread name: Thread-0, ThreadLocal hashCode: 1001240990, Instance hashCode: 413036867, Value: 0123
// innerClass thread Thread-0 finished.
// innerClass thread Thread-1 finished.
// Whole finished.
// 如上述結果所示,在當前測試類中,創建了一個單獨的對象InnerClass,
// 所以在多線程操作innerClass的方法時,實際是同一個InnerClass對象。
// InnerClass對象內直接引用了靜態內部類Counter類的靜態變量couner,
// 類型爲ThreadLocal。所以在理論上來說,ThreadLocal是內存共享的。
// 但是ThreadLocal封裝的StringBuilder對象,在三個線程中卻包含了3個內容,
public static void main(String[] args) {
int thread = 3;
// start 3 threads
final CountDownLatch latch = new CountDownLatch(thread);
final InnerClass innerClass = new InnerClass();
for (int i = 1 ; i <= thread; i ++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 4; j ++) {
innerClass.add(String.valueOf(j));
innerClass.print();
}
System.out.println("innerClass thread " + Thread.currentThread().getName() + " finished.");
latch.countDown();
}
}).start();
}
try {
latch.await(); // 等待latch計數到0
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Whole finished.");
}
private static class InnerClass {
public void add(String string) {
StringBuilder stringBuilder = Counter.counter.get();
Counter.counter.set(stringBuilder.append(string));
}
public void print() {
System.out.printf("Thread name: %s, ThreadLocal hashCode: %s, Instance hashCode: %s, Value: %s\n",
Thread.currentThread().getName(),
Counter.counter.hashCode(),
Counter.counter.get().hashCode(),
Counter.counter.get().toString());
}
}
private static class Counter {
// counter是Counter靜態內部類的靜態變量,理論上來說進程中是唯一的
private static ThreadLocal<StringBuilder> counter = new ThreadLocal<StringBuilder>() {
@Override
protected StringBuilder initialValue() {
return new StringBuilder();
}
};
}
}
從上述結果可見,
- Counter類爲靜態內部類,其ThreadLocal變量爲靜態變量,理論上來說,進程內存中應該只有一份實例。
- Counter類在多線程中被調用的時候,ThreadLocal對象內包裹的StringBuilder卻產生了不同的對象副本,即被clone 了一樣。
- 每個StringBuilder的結果最終都是打印爲0123,即表示每個線程之間對於StringBuilder沒有產生任何影響。
Lock
除了Sychronized關鍵字使用對象鎖完成同步外,Java還提供了一種鎖,Lock。
Lock與synchronized使用上來說,有如下區別:
- Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現,synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將 unLock()放到finally{} 中;
- synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
- Lock可以讓等待鎖的線程響應中斷,線程可以中斷去幹別的事務,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
- 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
- Lock可以提高多個線程進行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。
舉個例子:
當有多個線程讀寫文件時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。但是採用synchronized關鍵字來實現同步的話,就會導致一個問題:如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發生衝突,通過Lock就可以辦到。另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。
下面我們就來探討一下java.util.concurrent.locks包中常用的類和接口。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
首先要說明的就是Lock,通過查看Lock的源碼可知,Lock是一個接口。
下面來逐個講述Lock接口中每個方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。unLock()方法是用來釋放鎖的。
在Lock中聲明瞭四個方法來獲取鎖,那麼這四個方法有何區別呢?
lock()
lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。
由於在前面講到如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:
Lock lock = ...;
lock.lock();
try{
//處理任務
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
tryLock()
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖,則直接做其他事情
}
lockInterruptibly()
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意,當一個線程獲取了鎖之後,是不會被interrupt()方法中斷的。因爲本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
ReentrantLock
ReentrantLock,意思是“可重入鎖”,關於可重入鎖的概念在下一節講述。ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。下面通過一些實例看具體看一下如何使用ReentrantLock。
lock
private ArrayList<String> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
// lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。
private static void testOfLock() {
final TestOfLock testOfLock = new TestOfLock();
Thread thread1 = new Thread() {
@Override
public void run() {
testOfLock.lockInsert(Thread.currentThread());
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
testOfLock.lockInsert(Thread.currentThread());
}
};
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final list: " + testOfLock.list.toArray());
}
public void lockInsert(Thread thread) {
lock.lock(); // 獲取鎖
try {
System.out.println("線程" + thread.getName() + "得到了鎖");
for (int i = 0; i < 5; i ++) {
list.add(String.valueOf(i));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("線程" + thread.getName() + "釋放了鎖");
}
}
tryLock()
private ArrayList<String> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
// tryLock()方法是有返回值的,
// 它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,
// 也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。而後的動作也就不執行了。
private static void testOfTryLock() {
final TestOfLock test = new TestOfLock();
Thread thread1 = new Thread() {
@Override
public void run() {
test.tryLockInsert(Thread.currentThread());
}
};
Thread thread2 = new Thread() {
@Override
public void run() {
test.tryLockInsert(Thread.currentThread());
}
};
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("final list: " + test.list.toArray());
}
public void tryLockInsert(Thread thread) {
if (lock.tryLock()) {
try {
System.out.println("線程" + thread.getName() + "得到了鎖");
for (int i = 0; i < 5; i ++) {
list.add(String.valueOf(i));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("線程" + thread.getName() + "釋放了鎖");
}
} else {
System.out.println("線程" + thread.getName() + "獲取鎖失敗");
}
}
lockInterruptibly()
private ArrayList<String> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
// 當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。
// 也就使說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,
// 而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
// 由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在
// try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException.
private static void testOfInterruptLock() {
TestOfLock test = new TestOfLock();
Thread thread1 = test.new MyThread(test); // 必須使用對象new方法來創建內部類
Thread thread2 = test.new MyThread(test);
thread1.start();
thread2.start();
// 讓當前線程等待2秒鐘後,直接中斷thread2
try {
Thread.sleep(2000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void interruptLockInsert(Thread thread) throws InterruptedException {
lock.lockInterruptibly(); //注意,如果需要正確中斷等待鎖的線程,必須將獲取鎖放在外面,然後將InterruptedException拋出
try {
System.out.println("線程" + thread.getName() + "得到了鎖");
long startTime = System.currentTimeMillis();
for (;;) { // 不停循環,儘量延長測試時間
if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
break;
}
// 插入數據
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("線程" + thread.getName()+"執行 finally");
lock.unlock();
System.out.println("線程" + thread.getName() + "釋放了鎖");
}
}
class MyThread extends Thread {
private TestOfLock mTest = null;
public MyThread(TestOfLock test) {
this.mTest = test;
}
@Override
public void run() {
try {
mTest.interruptLockInsert(Thread.currentThread());
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("線程 " + Thread.currentThread().getName() + " 被中斷");
}
}
}
運行之後,發現thread2能夠被正確中斷。這個就是跟synchronized的區別點。
參考文章: