併發是伴隨着多核處理器的誕生而產生的,爲了充分利用硬件資源,誕生了多線程技術。但是多線程又存在資源競爭的問題,引發了同步和互斥,並帶來線程安全的問題。於是,從jdk1.5開始,引入了concurrent包來解決這些問題。
java.util.concurrent 包是專爲 Java併發編程而設計的包。
在Java中,當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替進行,在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是線程安全的。
一般來說,concurrent包基本上由有3個package組成 :
java.util.concurrent:提供大部分關於併發的接口和類,如BlockingQueue,Callable,ConcurrentHashMap,ExecutorService, Semaphore等 ;
java.util.concurrent.atomic:提供所有原子操作的類, 如AtomicInteger, AtomicLong等;
java.util.concurrent.locks:提供鎖相關的類, 如Lock, ReentrantLock, ReadWriteLock, Condition等。
concurrent包下的所有類可以分爲如下幾大類:
locks部分:顯式鎖(互斥鎖和速寫鎖)相關,如ReentrantLock,ReentrantReadWriteLock等;
atomic部分:原子變量類相關,是構建非阻塞算法的基礎,如AtomicInteger,AtomicBoolean,AtomicLong,AtomicReference等;
executor部分:線程池相關,如ExecutorService,Callable,Future等;
collections部分:併發容器相關,如BlockingQueue,Deque,ConcurrentMap等;
tools部分:同步工具相關,如CountDownLatch,CyclicBarrier,Semaphore,Executors,Exchanger等。
JUC的類圖結構如下所示:
concurrent包的優點有:
①功能豐富,諸如線程池(ThreadPoolExecutor),CountDownLatch等併發編程中需要的類已經有現成的實現,不需要自己去實現一套; 相比較而言,jdk1.4對多線程編程的主要支持幾乎只有Thread, Runnable,synchronized等。synchronized和JDK5之後的Lock均是悲觀鎖(悲觀鎖一般是一個人在使用的時候,另一個人不能用,所以性能極低,所能支持的併發量就不高)。
②concurrent包裏面的一些操作是基於硬件級別的CAS(compare and swap,比較再賦值),就是在cpu級別提供了原子操作,簡單的說就是可以提供無阻塞、無鎖定的算法; 而現代cpu大部分都是支持這種算法的。JUC(java.util.concurrent)是基於樂觀鎖的,既能保證數據不混亂,又能保證性能。
version-(版本管理)就是基於樂觀鎖機制-->拿着我們期望的結果,和現有結果進行比對,如果是相同的,就賦值,如果不是相同的,就重試。
CAS:有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。
CAS算法內部是通過JNI--native方法來實現: 由java底層的C語言或者C++實現。
一般情況下:同步容器是有使用價值的。有時候,我們的異步容器,比如ArrayList,在併發環境下,會有這些問題:①數據紊亂;②java.util.ConcurrentModificationException。這都是對於集合的讀寫狀態不一致造成的問題。
我們可以這樣構建同步容器:
Collections.synchronizedList(new ArrayList<>());
Collections.synchronizedMap()
Collections.synchronizedSet()
上面的方法可以實現同步容器,但是使用了悲觀鎖,因而效率不高。
使用JUC體系中提供的容器,如:ConcurrentHashMap,則有這樣的優勢:①不會出現同步問題,數據是正常的;②速度相對較快 ,就算沒有hashmap快,也比hashtable快的多。其實,ConcurrentHashMap內部也是有同步的,在這方面面和hashtable沒有區別。那麼,ConcurrentHashMap快在哪裏?主要是因爲,其內部劃分了很多segment區域,當不同的線程操作不同的segment的時候,其實還是一個異步操作;只有當不同線程操作同一個segment的時候,纔會發生同步操作,所以速度很快。一個ConcurrentHashMap內部最多能有16個segment。
我們接下來看一個非常有用的類CountDownLatch, 它是一個可以用來在一個進程中等待多個線程完成任務的類。在此給出一個應用場景:某個主線程接到一個任務,起了n個子線程去完成,但是主線程需要等待這n個子線程都完成任務以後纔開始執行某個操作。詳見代碼:
package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
/**
* 主線程會在啓動的子線程完全結束後再繼續執行
*/
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.demoCountDown();
}
public void demoCountDown() {
int count = 10;
final CountDownLatch l = new CountDownLatch(count);
for (int i = 0; i < count; ++i) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.currentThread().sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread -" + index + "- has finished...");
l.countDown();
}
}).start();
}
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("now all threads have finished");
}
}
執行結果如下所示:
thread -9- has finished...
thread -8- has finished...
thread -0- has finished...
thread -4- has finished...
thread -1- has finished...
thread -2- has finished...
thread -5- has finished...
thread -6- has finished...
thread -3- has finished...
thread -7- has finished...
now all threads have finished
接下來,我們再看下Atomic相關的類, 比如AtomicLong, AtomicInteger等。簡單來說,這些類都是線程安全的,支持無阻塞無鎖定的。
package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
/**
* 測試AtomicLong與long
*/
public class AtomicTest {
public static void main(String[] args) {
AtomicTest test = new AtomicTest();
test.testAtomic();
}
public void testAtomic() {
final int loopcount = 10000;
int threadcount = 10;
final NonSafeSeq seq1 = new NonSafeSeq();
final SafeSeq seq2 = new SafeSeq();
final CountDownLatch l = new CountDownLatch(threadcount);
for (int i = 0; i < threadcount; ++i) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < loopcount; ++j) {
seq1.inc();
seq2.inc();
}
System.out.println("finished : " + index);
l.countDown();
}
}).start();
}
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("both have finished....");
System.out.println("NonSafeSeq:" + seq1.get());
System.out.println("SafeSeq with atomic: " + seq2.get());
}
class NonSafeSeq {
private long count = 0;
public void inc() {
count++;
}
public long get() {
return count;
}
}
class SafeSeq {
private AtomicLong count = new AtomicLong(0);
public void inc() {
count.incrementAndGet();
}
public long get() {
return count.longValue();
}
}
}
上述代碼執行如下:
finished : 0
finished : 3
finished : 2
finished : 6
finished : 9
finished : 5
finished : 8
finished : 1
finished : 4
finished : 7
both have finished....
NonSafeSeq:98454
SafeSeq with atomic: 100000
其中,NonSafeSeq是作爲對比的類,直接放一個private long count不是線程安全的,而SafeSeq裏面放了一個AtomicLong,是線程安全的;可以直接調用incrementAndGet來增加。通過上述執行結果可以看到,10個線程,每個線程運行了10,000次,理論上應該有100,000次增加,使用了普通的long是非線程安全的,而使用了AtomicLong是線程安全的。需要注意的是,這個例子也說明,雖然long本身的單個設置是原子的,要麼成功要麼不成功,但是諸如count++這樣的操作就不是線程安全的,因爲這包括了讀取和寫入兩步操作。
在jdk 1.4時代,線程間的同步主要依賴於synchronized關鍵字,本質上該關鍵字是一個對象鎖,可以加在不同的instance上或者class上。
concurrent包提供了一個可以替代synchronized關鍵字的ReentrantLock,簡單的說,你可以new一個ReentrantLock, 然後通過lock.lock和lock.unlock來獲取鎖和釋放鎖;需要注意的是,必須將unlock放在finally塊裏面。reentrantlock的好處有 :
①更好的性能;
②提供同一個lock對象上不同condition的信號通知;
③還提供lockInterruptibly這樣支持響應中斷的加鎖過程,意思是說你試圖去加鎖,但是當前鎖被其他線程hold住,然後你這個線程可以被中斷。
package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
/**
* 測試ReentrantLock
*/
public class ReentrantLockTest {
public static void main(String[] args) {
ReentrantLockTest lockTest = new ReentrantLockTest();
lockTest.demoLock();
}
public void demoLock() {
final int loopcount = 10000;
int threadcount = 10;
final SafeSeqWithLock seq = new SafeSeqWithLock();
final CountDownLatch l = new CountDownLatch(threadcount);
for (int i = 0; i < threadcount; ++i) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < loopcount; ++j) {
seq.inc();
}
System.out.println("finished : " + index);
l.countDown();
}
}).start();
}
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("both have finished....");
System.out.println("SafeSeqWithLock:" + seq.get());
}
class SafeSeqWithLock {
private long count = 0;
private ReentrantLock lock = new ReentrantLock();
public void inc() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public long get() {
return count;
}
}
}
上述代碼執行如下:
finished : 5
finished : 3
finished : 1
finished : 8
finished : 0
finished : 6
finished : 4
finished : 2
finished : 7
finished : 9
both have finished....
SafeSeqWithLock:100000
上述代碼操作中,通過對inc操作加鎖,保證了線程安全。
concurrent包裏面還提供了一個非常有用的鎖,讀寫鎖ReadWriteLock。
A ReadWriteLock maintains a pair of associated locks, one for read-only operations and one for writing.
The read lock may be held simultaneously by multiple reader threads, so long as there are no writers.
The write lock is exclusive.
上述英文意思是說:讀鎖可以有很多個鎖同時上鎖,只要當前沒有寫鎖; 寫鎖是排他的,上了寫鎖,其他線程既不能上讀鎖,也不能上寫鎖;同樣,需要上寫鎖的前提是既沒有讀鎖,也沒有寫鎖。
package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 測試讀寫鎖
*/
public class RWLockTest {
public static void main(String[] args) {
RWLockTest lockTest = new RWLockTest();
lockTest.testRWLock_getw_onr();
}
public void testRWLock_getw_onr() {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
final Lock rlock = lock.readLock();
final Lock wlock = lock.writeLock();
final CountDownLatch l = new CountDownLatch(2);
// start r thread,開啓讀鎖
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " now to get rlock--獲取讀鎖");
rlock.lock();
try {
Thread.currentThread().sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " now to unlock rlock--釋放讀鎖");
rlock.unlock();
l.countDown();
}
}).start();
// start w thread,開啓寫鎖
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " now to get wlock--獲取寫鎖");
wlock.lock();
System.out.println(Thread.currentThread().getName() + " now to unlock wlock--釋放寫鎖");
wlock.unlock();
l.countDown();
}
}).start();
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished");
}
}
上述代碼執行如下:
Thread-0 now to get rlock--獲取讀鎖
Thread-1 now to get wlock--獲取寫鎖
Thread-0 now to unlock rlock--釋放讀鎖
Thread-1 now to unlock wlock--釋放寫鎖
main finished
ReadWriteLock的實現是ReentrantReadWriteLock,有趣的是,在一個線程中,讀鎖不能直接升級爲寫鎖,但是寫鎖可以降級爲讀鎖;這意思是說,如果你已經有了讀鎖,再去試圖獲得寫鎖,將會無法獲得, 一直堵住了;但是如果你有了寫鎖,再去試圖獲得讀鎖,就沒問題。
下面是一段寫鎖降級的代碼:
public void testRWLock_downgrade() {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock rlock = lock.readLock();
Lock wlock = lock.writeLock();
System.out.println("now to get wlock");
wlock.lock();
System.out.println("now to get rlock");
rlock.lock();
System.out.println("now to unlock wlock");
wlock.unlock();
System.out.println("now to unlock rlock");
rlock.unlock();
System.out.println("finished");
}
上述代碼在main函數中執行後,結果如下:
now to get wlock
now to get rlock
now to unlock wlock
now to unlock rlock
finished
我們再看一段讀鎖升級的代碼:
public void testRWLock_upgrade() {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock rlock = lock.readLock();
Lock wlock = lock.writeLock();
System.out.println("now to get rlock");
rlock.lock();
System.out.println("now to get wlock");
wlock.lock();
System.out.println("now to unlock wlock");
wlock.unlock();
System.out.println("now to unlock rlock");
rlock.unlock();
System.out.println("finished");
}
上述代碼執行中,已經有了讀鎖,再去試圖獲得寫鎖,將會無法獲得, 程序一直堵塞,進入死鎖狀態,顯示如下:
now to get rlock
now to get wlock
另外,CountDownLatch是一個同步的輔助類,允許一個或多個線程,等待其他一組線程完成操作,再繼續執行。
CyclicBarrier也是一個同步的輔助類,允許一組線程相互之間等待,達到一個共同點,再繼續執行。
CountDownLatch和CyclicBarrier都是Synchronization aid,即“同步輔助器”,既然都是輔助工具,在使用中有什麼區別,各自的使用場景如何?
CountDownLatch場景舉例:一年級期末考試要開始了,監考老師發下去試卷,然後坐在講臺旁邊玩着手機等待着學生答題,有的學生提前交了試卷,並約起打球了,等到最後一個學生交卷了,老師開始整理試卷,貼封條,下班,陪老婆孩子去了。
啓發:CountDownLatch很像一個倒計時鎖,倒計時結束,另一個線程纔開始執行。就如監考老師要結束監考工作,必須等待所有學生都交了試卷,監考工作才能進入結束環節。
CyclicBarrier場景舉例:公司組織戶外拓展活動,幫助團隊建設,其中最重要的一個項目就是要求全體員工(包括女同事,BOSS,一個都不能少)都能翻越一個高達四米,而且沒有任何抓點的高牆,才能繼續進行其他項目。
啓發:CyclicBarrier可以看成是個障礙,所有的線程必須到齊後才能一起通過這個障礙。
另外,concurrent包中線程池部分,請參考我的另一篇博文《ScheduledThreadExecutor定時任務線程池》。