沒聽說過這些,就不要說你懂並發了,three。


引言

 

  很久沒有跟大家再聊聊並發了,今天LZ閑來無事,跟大家再聊聊並發。由於時間過去的有點久,因此LZ就不按照常理出牌了,只是把自己的理解記錄在此,如果各位猿友覺得有所收獲,就點個推薦或者留言激勵下LZ,如果覺得浪費了自己寶貴的時間,也可以發下牢騷。

  好了,廢話就不多說了,現在就開始咱們的並發之旅吧。

 

並發編程的簡單分類

  

  並發常見的編程場景,一句話概括就是,需要協調多個線程之間的協作,已保證程序按照自己原本的意願執行。那么究竟應該如何協調多個線程?

  這個問題比較寬泛,一般情況下,我們按照方式的緯度去簡單區分,有以下兩種方式:

  1,第一種是利用JVM的內部機制。

  2,第二種是利用JVM外部的機制,比如JDK或者一些類庫。

  第一種方式一般是通過synchronized關鍵字等方式去實現,第二種則一般是使用JDK當中的類去手動實現。兩種方式十分相似,他們的區別有點類似於C/C++和Java的垃圾搜集方式的區別,C/C++手動釋放內存的方式更加靈活和高效,而Java自動垃圾搜集的方式則更加安全和方便。

  並發一直被認為是編程當中的高級特性,也是很多大公司在面試的時候都比較在意的部分,因此掌握好並發的簡單技巧,還是能夠讓自己的技術沉淀有質的飛躍的。

  

詳解JVM內部機制——同步篇

 

  JVM有很多內部同步機制,這在有的時候是非常值得我們去使用和學習的,接下來咱們就一起看看,JVM到底提供了哪些內部的同步方式。

  1,static的強制同步機制

  static這個關鍵字相信大家都不陌生,不過它附帶的同步機制估計是很多猿友都不知道的。例如下面這個簡單的類。

public class Static { private static String someField1 = someMethod1(); private static String someField2; static { someField2 = someMethod2(); } }
View Code

  首先上面這一段代碼在編譯以后會變成下面這個樣子,這點各位可以使用反編譯工具去驗證。

public class Static { private static String someField1; private static String someField2; static { someField1 = someMethod1(); someField2 = someMethod2(); } }
View Code

  不過在JVM真正執行這段代碼的時候,其實它又變成了下面這個樣子。

public class Static { private static String someField1; private static String someField2; private static volatile boolean isCinitMethodInvoked = false; static { synchronized (Static.class) { if (!isCinitMethodInvoked) { someField1 = someMethod1(); someField2 = someMethod2(); isCinitMethodInvoked = true; } } } }
View Code

  也就是說在實際執行一個類的靜態初始化代碼塊時,虛擬機內部其實對其進行了同步,這就保證了無論多少個線程同時加載一個類,靜態塊中的代碼執行且只執行一次。這點在單例模式當中得到了有效的應用,各位猿友有興趣的可以去翻看LZ之前的單例模式博文。

  2,synchronized的同步機制

  synchronized是JVM提供的同步機制,它可以修飾方法或者代碼塊。此外,在修飾代碼塊的時候,synchronized可以指定鎖定的對象,比如常用的有this,類字面常量等。在使用synchronized的時候,通常情況下,我們會針對特定的屬性進行鎖定,有時也會專門建立一個加鎖對象。

  直接給方法加synchronized關鍵字,或者使用this,類字面常量作為鎖的方式比較常用,也比較簡單,這里就不再舉例了。我們來看看對某一屬性進行鎖定的方式,如下。

public class Synchronized { private List<String> someFields; public void add(String someText) { //some code
        synchronized (someFields) { someFields.add(someText); } //some code
 } public Object[] getSomeFields() { //some code
        synchronized (someFields) { return someFields.toArray(); } } }
View Code

  這種方式一般要優於使用this或者類字面常量進行鎖定的方式,因為synchronized修飾的非靜態成員方法默認是使用的this進行鎖定,而synchronized修飾的靜態成員方法默認是使用的類字面常量進行的鎖定,因此如果直接在synchronized代碼塊中使用this或者類字面常量,可能會不經意的與synchronized方法產生互斥。通常情況下,使用屬性進行加鎖,能夠更加有效的提高並發度,從而在保證程序正確的前提下盡可能的提高性能。

  再來看一段比較特殊的代碼,如果猿友們經常看JDK源碼或者一些優秀的開源框架源碼的話,或許會見過這種方式。

public class Synchronized { private Object lock = new Object(); private List<String> someFields1; private List<String> someFields2; public void add(String someText) { //some code
        synchronized (lock) { someFields1.add(someText); someFields2.add(someText); } //some code
 } public Object[] getSomeFields() { //some code
        Object[] objects1 = null; Object[] objects2 = null; synchronized (lock) { objects1 = someFields1.toArray(); objects2 = someFields2.toArray(); } Object[] objects = new Object[someFields1.size() + someFields2.size()]; System.arraycopy(objects1, 0, objects, 0, objects1.length); System.arraycopy(objects2, 0, objects, objects1.length, objects2.length); return objects; } }
View Code

  lock是一個專門用於監控的對象,它沒有任何實際意義,只是為了與synchronized配合,完成對兩個屬性的統一鎖定。當然,一般情況下,也可以使用this代替lock,這其實沒有什么死的規定,完全可以按照實際情況而定。還有一種比較不推薦的方式,就是下面這種。

public class Synchronized { private List<String> someFields1; private List<String> someFields2; public void add(String someText) { //some code
        synchronized (someFields1) { synchronized (someFields2) { someFields1.add(someText); someFields2.add(someText); } } //some code
 } public Object[] getSomeFields() { //some code
        Object[] objects1 = null; Object[] objects2 = null; synchronized (someFields1) { synchronized (someFields2) { objects1 = someFields1.toArray(); objects2 = someFields2.toArray(); } } Object[] objects = new Object[someFields1.size() + someFields2.size()]; System.arraycopy(objects1, 0, objects, 0, objects1.length); System.arraycopy(objects2, 0, objects, objects1.length, objects2.length); return objects; } }
View Code

  這種加鎖方式比較挑戰人的細心程度,萬一哪個不小心把順序搞錯了,就可能造成死鎖。因此如果你非要使用這種方式,請做好被你的上司行刑的准備。

  

詳解JVM外部機制——同步篇

  

  與JVM內部的同步機制對應的,就是外部的同步機制,也可以叫做編程式的同步機制。接下來,咱們就看看一些常用的外部同步方法。

  ReentrantLock(可重入的鎖)

  ReentrantLock是JDK並發包中locks當中的一個類,專門用於彌補synchronized關鍵字的一些不足。接下來咱們就看一下synchronized關鍵字都有哪些不足,接着咱們再嘗試使用ReentrantLock去解決這些問題。

  1)synchronized關鍵字同步的時候,等待的線程將無法控制,只能死等。

  解決方式:ReentrantLock可以使用tryLock(timeout, unit)方法去控制等待獲得鎖的時間,也可以使用無參數的tryLock方法立即返回,這就避免了死鎖出現的可能性。

  2)synchronized關鍵字同步的時候,不保證公平性,因此會有線程插隊的現象。

  解決方式:ReentrantLock可以使用構造方法ReentrantLock(fair)來強制使用公平模式,這樣就可以保證線程獲得鎖的順序是按照等待的順序進行的,而synchronized進行同步的時候,是默認非公平模式的,但JVM可以很好的保證線程不被餓死。

  ReentrantLock有這樣一些優點,當然也有不足的地方。最主要不足的一點,就是ReentrantLock需要開發人員手動釋放鎖,並且必須在finally塊中釋放。

  下面給出兩個簡單的ReentrantLock例子,請各位猿友收看。

public class Lock {

    private ReentrantLock nonfairLock = new ReentrantLock();

    private ReentrantLock fairLock = new ReentrantLock(true);

    private List<String> someFields;

    public void add(String someText) {
        // 等待獲得鎖,與synchronized類似
        nonfairLock.lock();
        try {
            someFields.add(someText);
        } finally {
            // finally中釋放鎖是無論如何都不能忘的
            nonfairLock.unlock();
        }
    }

    public void addTimeout(String someText) {
        // 嘗試獲取,如果10秒沒有獲取到則立即返回
        try {
            if (!fairLock.tryLock(10, TimeUnit.SECONDS)) {
                return;
            }
        } catch (InterruptedException e) {
            return;
        }
        try {
            someFields.add(someText);
        } finally {
            // finally中釋放鎖是無論如何都不能忘的
            fairLock.unlock();
        }
    }

}
View Code

   以上主要展示了ReentrantLock的基本用法和限時的等待,接下來咱們來看看當需要鎖定多個對象的時候,ReentrantLock是如何使用的。從以下代碼可以看出,用法與上面的synchronized中的方式非常相似。

package concurrent;

import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author zuoxiaolong
 *
 */
public class ReentrantLockTest {
    
    private ReentrantLock lock = new ReentrantLock();

    private List<String> someFields1;
    private List<String> someFields2;
    
    public void add(String someText) {
        //some code
        lock.lock();
        try {
            someFields1.add(someText);
            someFields2.add(someText);
        } finally {
            lock.unlock();
        }
        //some code
    }
    
    public Object[] getSomeFields() {
        //some code
        Object[] objects1 = null;
        Object[] objects2 = null;
        lock.lock();
        try {
            objects1 = someFields1.toArray();
            objects2 = someFields2.toArray();
        } finally {
            lock.unlock();
        }
        Object[] objects = new Object[someFields1.size() + someFields2.size()];
        System.arraycopy(objects1, 0, objects, 0, objects1.length);
        System.arraycopy(objects2, 0, objects, objects1.length, objects2.length);
        return objects;
    }
    
}
View Code

 

詳解JVM內部機制——條件等待篇

  

  剛才已經討論過JVM內部同步的機制,接下來咱們一起看一下JVM內部的條件等待機制。Java當中的類有一個共同的父類Object,而在Object中,有一個wait的本地方法,這是一個神奇的方法。

  它可以用來協調線程之間的協作,使用方式也比較簡單,看一下下面這個例子,你就基本入門了哦。

public class ObjectWait { private volatile static boolean lock; public static void main(String[] args) throws InterruptedException { final Object object = new Object(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.println("等待被通知!"); try { synchronized (object) { while (!lock) { object.wait(); } } } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("已被通知"); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println("馬上開始通知!"); synchronized (object) { object.notify(); lock = true; } System.out.println("已通知"); } }); thread1.start(); thread2.start(); Thread.sleep(100000); } }
View Code

  這是一個最基本的例子,我們使用一個線程在object對象上等待另外一個線程的通知,當另外一個線程通知了以后,等待的線程將會繼續運行。其實初次接觸這個東西,是不是感覺很有意思呢。

  wait一般情況下最常用的場景是構造一個花銷非常大的對象的時候,比如JDK動態代理在生成代理類的時候就使用了這種方式。JDK6在生成一個代理類之前,會先檢測一個是否正在生成中的標識,如果正在生成的話,JDK6就會在對象上等待,直到正在生成的代理類生成完畢,然后直接從緩存中獲取。

  這里需要提醒大家的一點是,wait,notify和notifyAll方法在使用前,必須獲取到當前對象的鎖,否則會告訴你非法的監控狀態異常。還有一點,則是如果有多個線程在wait等待,那么調用notify會隨機通知其中一個線程,而不會按照順序通知。換句話說,notify的通知機制是非公平的,notify並不保證先調用wait方法的線程優先被喚醒。notifyAll方法則不存在這個問題,它將通知所有處於wait等待的線程。

  

詳解JVM外部機制——條件等待篇

  

  上面咱們已經看過JVM自帶的條件控制機制,是使用的本地方法wait實現的。那么在JDK的類庫中,也有這樣的一個類Condition,來彌補wait方法本身的不足。與之前一樣,說到這里,咱們就來談談wait到底有哪些不足。

  1)wait方法當使用帶參數的方法wait(timeout)或者wait(timeout,nanos)時,無法反饋究竟是被喚醒還是到達了等待時間,大部分時候,我們會使用循環(就像上面的例子一樣)來檢測是否達到了條件。

  解決方式:Condition可以使用返回值標識是否達到了超時時間。

  2)由於wait,notify,notifyAll方法都需要獲得當前對象的鎖,因此當出現多個條件等待時,則需要依次獲得多個對象的鎖,這是非常惡心麻煩且繁瑣的事情。

  解決方式:Condition之需要獲得Lock的鎖即可,一個Lock可以擁有多個條件。

  第一個問題比較好理解,只是Condition的await方法多了一個返回參數boolean去標識究竟是被喚醒還是超時。但是第二個問題比較繁瑣一些,因此這里給出一個簡單的示例,如下。

package concurrent; /** * @author zuoxiaolong * */
public class ObjectWait { public static void main(String[] args) throws InterruptedException { final Object object1 = new Object(); final Object object2 = new Object(); Thread thread1 = new Thread(new Runnable() { public void run() { try { System.out.println("等待object1被通知!"); synchronized (object1) { object1.wait(); } System.out.println("object1已被通知,馬上開始通知object2!"); synchronized (object2) { object2.notify(); } System.out.println("通知object2完畢!"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread thread2 = new Thread(new Runnable() { public void run() { try { System.out.println("馬上開始通知object1!"); synchronized (object1) { object1.notify(); } System.out.println("通知object1完畢,等待object2被通知!"); synchronized (object2) { object2.wait(); } System.out.println("object2已被通知!"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); thread1.start(); Thread.sleep(1000); thread2.start(); } }
View Code

  這是一個多條件的示例。基本邏輯是,線程1先等待線程2通知,然后線程2再等待線程1通知。請記住,這是兩個不同的條件。可以看到,如果使用wait的話,必須兩次獲得兩個鎖,一不小心可能還會出現死鎖。接下來,咱們看看Condition實現一樣的功能是怎么實現的。

package concurrent; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * @author zuoxiaolong * */
public class ConditionTest { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { final Condition condition1 = lock.newCondition(); final Condition condition2 = lock.newCondition(); Thread thread1 = new Thread(new Runnable() { public void run() { lock.lock(); try { System.out.println("等待condition1被通知!"); condition1.await(); System.out.println("condition1已被通知,馬上開始通知condition2!"); condition2.signal(); System.out.println("通知condition2完畢!"); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }); Thread thread2 = new Thread(new Runnable() { public void run() { lock.lock(); try { System.out.println("馬上開始通知condition1!"); condition1.signal(); System.out.println("通知condition1完畢,等待condition2被通知!"); condition2.await(); System.out.println("condition2已被通知!"); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }); thread1.start(); Thread.sleep(1000); thread2.start(); } }
View Code

  可以看到,我們只需要獲取lock一次就可以了,在內部咱們可以使用兩個或多個條件而不再需要多次獲得鎖。這種方式會更加直觀,大大增加程序的可讀性。

  

詳解JVM外部機制——線程協作篇

  

  JDK當中除了以上的ReentrantLock和Condition之外,還有很多幫助猿友們協調線程的工具類。接下來咱們就一一混個臉熟。

  1,CountDownLatch

  這個類是為了幫助猿友們方便的實現一個這樣的場景,就是某一個線程需要等待其它若干個線程完成某件事以后才能繼續進行。比如下面的這個程序。

package concurrent; import java.util.concurrent.CountDownLatch; /** * @author zuoxiaolong * */
public class CountDownLatchTest { public static void main(String[] args) throws InterruptedException { final CountDownLatch countDownLatch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { final int number = i + 1; Runnable runnable = new Runnable() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) {} System.out.println("執行任務[" + number + "]"); countDownLatch.countDown(); System.out.println("完成任務[" + number + "]"); } }; Thread thread = new Thread(runnable); thread.start(); } System.out.println("主線程開始等待..."); countDownLatch.await(); System.out.println("主線程執行完畢..."); } }
View Code

  這個程序的主線程會等待CountDownLatch進行10次countDown方法的調用才會繼續執行。我們可以從打印的結果上看出來,盡管有的時候完成任務的打印會出現在主線程執行完畢之后,但這只是因為countDown已經執行完畢,主線程的打印語句先一步執行而已。

  2,CyclicBarrier

  這個類是為了幫助猿友們方便的實現多個線程一起啟動的場景,就像賽跑一樣,只要大家都准備好了,那就開始一起沖。比如下面這個程序,所有的線程都准備好了,才會一起開始執行。

package concurrent; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; /** * @author zuoxiaolong * */
public class CyclicBarrierTest { public static void main(String[] args) { final CyclicBarrier cyclicBarrier = new CyclicBarrier(10); for (int i = 0; i < 10; i++) { final int number = i + 1; Runnable runnable = new Runnable() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) {} System.out.println("等待執行任務[" + number + "]"); try { cyclicBarrier.await(); } catch (InterruptedException e) { } catch (BrokenBarrierException e) { } System.out.println("開始執行任務[" + number + "]"); } }; Thread thread = new Thread(runnable); thread.start(); } } }
View Code

  3,Semaphore

  這個類是為了幫助猿友們方便的實現控制數量的場景,可以是線程數量或者任務數量等等。來看看下面這段簡單的代碼。

package concurrent; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; /** * @author zuoxiaolong * */
public class SemaphoreTest { public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(10); final AtomicInteger number = new AtomicInteger(); for (int i = 0; i < 100; i++) { Runnable runnable = new Runnable() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) {} try { semaphore.acquire(); number.incrementAndGet(); } catch (InterruptedException e) {} } }; Thread thread = new Thread(runnable); thread.start(); } Thread.sleep(10000); System.out.println("共" + number.get() + "個線程獲得到信號"); System.exit(0); } }
View Code

  從結果上可以看出,LZ設定了總數為10,卻開了100個線程,但是最終只有10個線程獲取到了信號量,如果這10個線程不主動調用release方法的話,那么其余90個線程將一起掛死。

  4,Exchanger

  這個類是為了幫助猿友們方便的實現兩個線程交換數據的場景,使用起來非常簡單,看看下面這段代碼。

package concurrent; import java.util.concurrent.Exchanger; /** * @author zuoxiaolong * */
public class ExchangerTest { public static void main(String[] args) throws InterruptedException { final Exchanger<String> exchanger = new Exchanger<String>(); Thread thread1 = new Thread(new Runnable() { public void run() { try { System.out.println("線程1等待接受"); String content = exchanger.exchange("thread1"); System.out.println("線程1收到的為:" + content); } catch (InterruptedException e) {} } }); Thread thread2 = new Thread(new Runnable() { public void run() { try { System.out.println("線程2等待接受並沉睡3秒"); Thread.sleep(3000); String content = exchanger.exchange("thread2"); System.out.println("線程2收到的為:" + content); } catch (InterruptedException e) {} } }); thread1.start(); thread2.start(); } }
View Code

  兩個線程在只有一個線程調用exchange方法的時候調用方會被掛起,當都調用完畢時,雙方會交換數據。在任何一方沒調用exchange之前,線程都會處於掛起狀態。

  

小結

  

  今天LZ和各位一起見識了一下Java並發編程的一些基礎,掌握以上信息對於並發編程還是非常有必要的。希望各位猿友能夠有所收獲,咱們下次見!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM