JUC详解
JUC
前言:
在Java中,線程部分是一個重點,本篇文章說的JUC也是關于線程的。JUC就是java.util .concurrent工具包的簡稱。這是一個處理線程的工具包,JDK 1.5開始出現的。下面一起來看看它怎么使用。
一、volatile關鍵字與內存可見性
1、內存可見性:
先來看看下面的一段代碼:
這段代碼很簡單,就是一個ThreadDemo類繼承Runnable創建一個線程。它有一個成員變量flag為false,然后重寫run方法,在run方法里面將flag改為true,同時還有一條輸出語句。然后就是main方法主線程去讀取flag。如果flag為true,就會break掉while循環,否則就是死循環。按道理,下面那個線程將flag改為true了,主線程讀取到的應該也是true,循環應該會結束。看看運行結果:
從圖中可以看到,該程序并沒有結束,也就是死循環。說明主線程讀取到的flag還是false,可是另一個線程明明將flag改為true了,而且打印出來了,這是什么原因呢?這就是內存可見性問題。
- 內存可見性問題:當多個線程操作共享數據時,彼此不可見。
看下圖理解上述代碼:
要解決這個問題,可以加鎖。如下:
加了鎖,就可以讓while循環每次都從主存中去讀取數據,這樣就能讀取到true了。但是一加鎖,每次只能有一個線程訪問,當一個線程持有鎖時,其他的就會阻塞,效率就非常低了。不想加鎖,又要解決內存可見性問題,那么就可以使用volatile關鍵字。
2、volatile關鍵字:
- 用法:
volatile關鍵字:當多個線程操作共享數據時,可以保證內存中的數據可見。用這個關鍵字修飾共享數據,就會及時的把線程緩存中的數據刷新到主存中去,也可以理解為,就是直接操作主存中的數據。所以在不使用鎖的情況下,可以使用volatile。如下:
這樣就可以解決內存可見性問題了。
- volatile和synchronized的區別:
volatile不具備互斥性(當一個線程持有鎖時,其他線程進不來,這就是互斥性)。
volatile不具備原子性。
二、原子性
1、理解原子性:
上面說到volatile不具備原子性,那么原子性到底是什么呢?先看如下代碼:
這段代碼就是在run方法里面讓i++,然后啟動十個線程去訪問。看看結果:
可以發現,出現了重復數據。明顯產生了多線程安全問題,或者說原子性問題。所謂原子性就是操作不可再細分,而i++操作分為讀改寫三步,如下:
所以i++明顯不是原子操作。上面10個線程進行i++時,內存圖解如下:
看到這里,好像和上面的內存可見性問題一樣。是不是加個volatile關鍵字就可以了呢?其實不是的,因為加了volatile,只是相當于所有線程都是在主存中操作數據而已,但是不具備互斥性。比如兩個線程同時讀取主存中的0,然后又同時自增,同時寫入主存,結果還是會出現重復數據。
2、原子變量:
JDK 1.5之后,Java提供了原子變量,在java.util.concurrent.atomic包下。原子變量具備如下特點:
- 有volatile保證內存可見性。
- 用CAS算法保證原子性。
3、CAS算法:
CAS算法是計算機硬件對并發操作共享數據的支持,CAS包含3個操作數:
- 內存值V
- 預估值A
- 更新值B
當且僅當V==A時,才會把B的值賦給V,即V = B,否則不做任何操作。就上面的i++問題,CAS算法是這樣處理的:首先V是主存中的值0,然后預估值A也是0,因為此時還沒有任何操作,這時V=B,所以進行自增,同時把主存中的值變為1。如果第二個線程讀取到主存中的還是0也沒關系,因為此時預估值已經變成1,V不等于A,所以不進行任何操作。
4、使用原子變量改進i++問題:
原子變量用法和包裝類差不多,如下:
只改這兩處即可。
三、鎖分段機制
JDK 1.5之后,在java.util.concurrent包中提供了多種并發容器類來改進同步容器類的性能。其中最主要的就是ConcurrentHashMap。
1、ConcurrentHashMap:
ConcurrentHashMap就是一個線程安全的hash表。我們知道HashMap是線程不安全的,Hash Table加了鎖,是線程安全的,因此它效率低。HashTable加鎖就是將整個hash表鎖起來,當有多個線程訪問時,同一時間只能有一個線程訪問,并行變成串行,因此效率低。所以JDK1.5后提供了ConcurrentHashMap,它采用了鎖分段機制。
如上圖所示,ConcurrentHashMap默認分成了16個segment,每個Segment都對應一個Hash表,且都有獨立的鎖。所以這樣就可以每個線程訪問一個Segment,就可以并行訪問了,從而提高了效率。這就是鎖分段。但是,java 8 又更新了,不再采用鎖分段機制,也采用CAS算法了。
2、用法:
java.util.concurrent包還提供了設計用于多線程上下文中的 Collection 實現: ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。當期望許多線程訪問一個給 定 collection 時,ConcurrentHashMap 通常優于同步的 HashMap, ConcurrentSkipListMap 通常優于同步的 TreeMap。當期望的讀數和遍歷遠遠 大于列表的更新數時,CopyOnWriteArrayList 優于同步的 ArrayList。下面看看部分用法:
10個線程并發訪問這個集合,讀取集合數據的同時再往集合中添加數據。運行這段代碼會報錯,并發修改異常。
將創建集合方式改成:
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();這樣就不會有并發修改異常了。因為這個是寫入并復制,每次生成新的,所以如果添加操作比較多的話,開銷非常大,適合迭代操作比較多的時候使用。
四、閉鎖
java.util.concurrent包中提供了多種并發容器類來改進同步容器的性能。ContDownLatch是一個同步輔助類,在完成某些運算時,只有其他所有線程的運算全部完成,當前運算才繼續執行,這就叫閉鎖。看下面代碼:
public class TestCountDownLatch {public static void main(String[] args){LatchDemo ld = new LatchDemo();long start = System.currentTimeMillis();for (int i = 0;i<10;i++){new Thread(ld).start();}long end = System.currentTimeMillis();System.out.println("耗費時間為:"+(end - start)+"秒");} }class LatchDemo implements Runnable{private CountDownLatch latch;public LatchDemo(){}@Overridepublic void run() {for (int i = 0;i<5000;i++){if (i % 2 == 0){//50000以內的偶數System.out.println(i);}}} }這段代碼就是10個線程同時去輸出5000以內的偶數,然后在主線程那里計算執行時間。其實這是計算不了那10個線程的執行時間的,因為主線程與這10個線程也是同時執行的,可能那10個線程才執行到一半,主線程就已經輸出“耗費時間為x秒”這句話了。所有要想計算這10個線程執行的時間,就得讓主線程先等待,等10個分線程都執行完了才能執行主線程。這就要用到閉鎖。看如何使用:
public class TestCountDownLatch {public static void main(String[] args) {final CountDownLatch latch = new CountDownLatch(10);//有多少個線程這個參數就是幾LatchDemo ld = new LatchDemo(latch);long start = System.currentTimeMillis();for (int i = 0; i < 10; i++) {new Thread(ld).start();}try {latch.await();//這10個線程執行完之前先等待} catch (InterruptedException e) {}long end = System.currentTimeMillis();System.out.println("耗費時間為:" + (end - start));} }class LatchDemo implements Runnable {private CountDownLatch latch;public LatchDemo(CountDownLatch latch) {this.latch = latch;}@Overridepublic void run() {synchronized (this) {try {for (int i = 0; i < 50000; i++) {if (i % 2 == 0) {//50000以內的偶數System.out.println(i);}}} finally {latch.countDown();//每執行完一個就遞減一個}}} }如上代碼,主要就是用latch.countDown()和latch.await()實現閉鎖,詳細請看上面注釋即可。
五、創建線程的方式 — 實現Callable接口
直接看代碼:
public class TestCallable {public static void main(String[] args){CallableDemo callableDemo = new CallableDemo();//執行callable方式,需要FutureTask實現類的支持,用來接收運算結果FutureTask<Integer> result = new FutureTask<>(callableDemo);new Thread(result).start();//接收線程運算結果try {Integer sum = result.get();//當上面的線程執行完后,才會打印結果。跟閉鎖一樣。所有futureTask也可以用于閉鎖System.out.println(sum);} catch (Exception e) {e.printStackTrace();}} }class CallableDemo implements Callable<Integer>{@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0;i<=100;i++){sum += i;}return sum;} }現在Callable接口和實現Runable接口的區別就是,Callable帶泛型,其call方法有返回值。使用的時候,需要用FutureTask來接收返回值。而且它也要等到線程執行完調用get方法才會執行,也可以用于閉鎖操作。
六、Lock同步鎖
在JDK1.5之前,解決多線程安全問題有兩種方式(sychronized隱式鎖):
-
同步代碼塊
-
同步方法
在JDK1.5之后,出現了更加靈活的方式(Lock顯式鎖): -
同步鎖
Lock需要通過lock()方法上鎖,通過unlock()方法釋放鎖。為了保證鎖能釋放,所有unlock方法一般放在finally中去執行。
再來看一下賣票案例:
public class TestLock {public static void main(String[] args) {Ticket td = new Ticket();new Thread(td, "窗口1").start();new Thread(td, "窗口2").start();new Thread(td, "窗口3").start();} }class Ticket implements Runnable {private int ticket = 100;@Overridepublic void run() {while (true) {if (ticket > 0) {try {Thread.sleep(200);} catch (Exception e) {}System.out.println(Thread.currentThread().getName() + "完成售票,余票為:" + (--ticket));}}} }多個線程同時操作共享數據ticket,所以會出現線程安全問題。會出現同一張票賣了好幾次或者票數為負數的情況。以前用同步代碼塊和同步方法解決,現在看看用同步鎖怎么解決。
class Ticket implements Runnable {private Lock lock = new ReentrantLock();//創建lock鎖private int ticket = 100;@Overridepublic void run() {while (true) {lock.lock();//上鎖try {if (ticket > 0) {try {Thread.sleep(200);} catch (Exception e) {}System.out.println(Thread.currentThread().getName() + "完成售票,余票為:" + (--ticket));}}finally {lock.unlock();//釋放鎖}}} }直接創建lock對象,然后用lock()方法上鎖,最后用unlock()方法釋放鎖即可。
七、等待喚醒機制
1、虛假喚醒問題:
生產消費模式是等待喚醒機制的一個經典案例,看下面的代碼:
這就是生產消費模式的案例,這里沒有使用等待喚醒機制,運行結果就是即使是缺貨狀態,它也會不斷的去消費,也會一直打印“缺貨”,即使是產品已滿狀態,也會不斷地進貨。用等待喚醒機制改進:
//店員 class Clerk{private int product = 0;//共享數據public synchronized void get(){ //進貨if(product >= 10){System.out.println("產品已滿");try {this.wait();//滿了就等待} catch (InterruptedException e) {e.printStackTrace();}}else {System.out.println(Thread.currentThread().getName()+":"+ (++product));this.notifyAll();//沒滿就可以進貨}}public synchronized void sell(){//賣貨if (product <= 0){System.out.println("缺貨");try {this.wait();//缺貨就等待} catch (InterruptedException e) {e.printStackTrace();}}else {System.out.println(Thread.currentThread().getName()+":"+ (--product));this.notifyAll();//不缺貨就可以賣}} }這樣就不會出現上述問題了。沒有的時候就生產,生產滿了就通知消費,消費完了再通知生產。但是這樣還是有點問題,將上述代碼做如下改動:
if(product >= 1){ //把原來的10改成1System.out.println("產品已滿");...... public void run() {try {Thread.sleep(200);//睡0.2秒} catch (InterruptedException e) {e.printStackTrace();}for (int i = 0;i<20;i++){clerk.sell();} }就做這兩處修改,再次運行,發現雖然結果沒問題,但是程序卻一直沒停下來。出現這種情況是因為有一個線程在等待,而另一個線程沒有執行機會了,喚醒不了這個等待的線程了,所以程序就無法結束。解決辦法就是把get和sell方法里面的else去掉,不要用else包起來。但是,即使這樣,如果再多加兩個線程,就會出現負數了。
new Thread(productor, "生產者C").start(); new Thread(consumer, "消費者D").start();運行結果:
一個消費者線程搶到執行權,發現product是0,就等待,這個時候,另一個消費者又搶到了執行權,product是0,還是等待,此時兩個消費者線程在同一處等待。然后當生產者生產了一個product后,就會喚醒兩個消費者,發現product是1,同時消費,結果就出現了0和-1。這就是虛假喚醒。解決辦法就是把if判斷改成while。如下:
只需要把if改成while,每次都再去判斷一下,就可以了。
2、用Lock鎖實現等待喚醒:
class Clerk {private int product = 0;//共享數據private Lock lock = new ReentrantLock();//創建鎖對象private Condition condition = lock.newCondition();//獲取condition實例public void get() { //進貨lock.lock();//上鎖try {while (product >= 1) {System.out.println("產品已滿");try {condition.await();//滿了就等待} catch (InterruptedException e) {}}System.out.println(Thread.currentThread().getName() + ":" + (++product));condition.signalAll();//沒滿就可以進貨}finally {lock.unlock();//釋放鎖}}public void sell() {//賣貨lock.lock();//上鎖try {while (product <= 0) {System.out.println("缺貨");try {condition.await();//缺貨就等待} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + ":" + (--product));condition.signalAll();//不缺貨就可以賣}finally {lock.unlock();//釋放鎖}} }使用lock同步鎖,就不需要sychronized關鍵字了,需要創建lock對象和condition實例。condition的await()方法、signal()方法和signalAll()方法分別與wait()方法、notify()方法和notifyAll()方法對應。
3、線程按序交替:
首先來看一道題:
分析:
線程本來是搶占式進行的,要按序交替,所以必須實現線程通信, 那就要用到等待喚醒。可以使用同步方法,也可以用同步鎖。編碼實現:
public class TestLoopPrint {public static void main(String[] args) {AlternationDemo ad = new AlternationDemo();new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {ad.loopA();}}}, "A").start();new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {ad.loopB();}}}, "B").start();new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10; i++) {ad.loopC();}}}, "C").start();} }class AlternationDemo {private int number = 1;//當前正在執行的線程的標記private Lock lock = new ReentrantLock();Condition condition1 = lock.newCondition();Condition condition2 = lock.newCondition();Condition condition3 = lock.newCondition();public void loopA() {lock.lock();try {if (number != 1) { //判斷condition1.await();}System.out.println(Thread.currentThread().getName());//打印number = 2;condition2.signal();} catch (Exception e) {} finally {lock.unlock();}}public void loopB() {lock.lock();try {if (number != 2) { //判斷condition2.await();}System.out.println(Thread.currentThread().getName());//打印number = 3;condition3.signal();} catch (Exception e) {} finally {lock.unlock();}}public void loopC() {lock.lock();try {if (number != 3) { //判斷condition3.await();}System.out.println(Thread.currentThread().getName());//打印number = 1;condition1.signal();} catch (Exception e) {} finally {lock.unlock();}} }以上編碼就滿足需求。創建三個線程,分別調用loopA、loopB和loopC方法,這三個線程使用condition進行通信。
八、ReadWriterLock讀寫鎖
我們在讀數據的時候,可以多個線程同時讀,不會出現問題,但是寫數據的時候,如果多個線程同時寫數據,那么到底是寫入哪個線程的數據呢?所以,如果有兩個線程,寫寫/讀寫需要互斥,讀讀不需要互斥。這個時候可以用讀寫鎖。看例子:
public class TestReadWriterLock {public static void main(String[] args){ReadWriterLockDemo rw = new ReadWriterLockDemo();new Thread(new Runnable() {//一個線程寫@Overridepublic void run() {rw.set((int)Math.random()*101);}},"write:").start();for (int i = 0;i<100;i++){//100個線程讀Runnable runnable = () -> rw.get();Thread thread = new Thread(runnable);thread.start();}} }class ReadWriterLockDemo{private int number = 0;private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//讀(可以多個線程同時操作)public void get(){readWriteLock.readLock().lock();//上鎖try {System.out.println(Thread.currentThread().getName()+":"+number);}finally {readWriteLock.readLock().unlock();//釋放鎖}}//寫(一次只能有一個線程操作)public void set(int number){readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName());this.number = number;}finally {readWriteLock.writeLock().unlock();}} }這個就是讀寫鎖的用法。上面的代碼實現了一個線程寫,一百個線程同時讀的操作。
九、線程池
我們使用線程時,需要new一個,用完了又要銷毀,這樣頻繁的創建銷毀也很耗資源,所以就提供了線程池。道理和連接池差不多,連接池是為了避免頻繁的創建和釋放連接,所以在連接池中就有一定數量的連接,要用時從連接池拿出,用完歸還給連接池。線程池也一樣。線程池中有一個線程隊列,里面保存著所有等待狀態的線程。下面來看一下用法:
public class TestThreadPool {public static void main(String[] args) {ThreadPoolDemo tp = new ThreadPoolDemo();//1.創建線程池ExecutorService pool = Executors.newFixedThreadPool(5);//2.為線程池中的線程分配任務pool.submit(tp);//3.關閉線程池pool.shutdown();} }class ThreadPoolDemo implements Runnable {private int i = 0;@Overridepublic void run() {while (i < 100) {System.out.println(Thread.currentThread().getName() + ":" + (i++));}} }線程池用法很簡單,分為三步。首先用工具類Executors創建線程池,然后給線程池分配任務,最后關閉線程池就行了。
總結:
以上為本文全部內容,涉及到了JUC的大部分內容。 本人也是初次接觸,如有錯誤,希望大佬指點一二!
總結
- 上一篇: CodeBlocks 20.03 配置
- 下一篇: python精要(80)-wxpytho