併發編程知識

1、線程基礎、線程之間的共享和協作

1.1、基礎概念

1.1.1、cpu核心數、線程數

​ cpu的核心數與線程數是1:1的關係,例如一個8核的cpu支持8個線程同時運行。但在intel引入超線程技術以後,關係就是1:2。在開發過程中並沒有感覺到線程的限制,那是因爲cpu時間片輪轉機制(RR調度)的算法的作用。

1.1.2、cpu時間片輪轉機制

​ cpu給每個進程分配一個“時間段”,這個時間段就叫做這個進程的“時間片”,這個時間片就是這個進程允許運行的時間,如果當這個進程的時間片段結束,操作系統就會把分配給這個進程的cpu剝奪,分配給另外一個進程。如果進程在時間片還沒結束的情況下阻塞了,或者說進程跑完了,cpu就會進行切換。cpu在兩個進程之間的切換稱爲“上下文切換”,上下文切換是需要時間的,大約需要花費5000~20000(5毫秒到20毫秒,這個花費的時間是由操作系統決定)個時鐘週期,儘管我們平時感覺不到。所以在開發過程中要注意上下文切換(兩個進程之間的切換)對我們程序性能的影響。

1.1.3、什麼是進行和線程

​ 進程:它是屬於程序調度/運行的資源分配的最小單位,一個進程的內部可能有多個線程,多個線程之間會共享這個進程的資源。進程與進程之間是相互獨立的 線程:它是cpu調度的最小單位,線程本身是不能獨立進行的,它必須依附某個進程,線程本身是不擁有系統資源的。

1.1.4、什麼是並行和併發

​ 並行是同一時刻可以處理多少件事,併發是在單位時間內可以處理多少件事情。

1.1.5、高併發編程的意義、好處和注意事項

​ 通過以上1.1~1.4的瞭解,我們可以知道高併發編程可以充分利用cpu的資源,例如一個8核的cpu跑一個單線的程序,那麼意味着在任意時刻,我有7個CPU核心是浪費掉的。另外可以充分地加快用戶的響應時間。同時使用併發編程可以使我們的代碼模塊化、異步化。

​ 注意事項/難點: 線程之間會共享進程的資源,既然說是共享資源,就有可能存在衝突。在高併發編程中如果控制不好,還有可能會造成線程的死鎖。每啓動一個線程,操作系統就需要爲這個線程分配一定的資源,線程數太多還可能會把內存消耗完畢,會導致系統死機。

1.2、啓動和終止線程

1.2.1、啓動的方法

​ a、類Thread

public class ThreadStartDemo extends Thread{
    @Override
    public void run() {
        System.out.println("繼承Thread方式啓動線程");
    }
    public static void main(String[] args) {
        new ThreadStartDemo().start();
    }
}

 

​ b、接口Runnable(沒有返回值)

public class ThreadStartDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("實現Runnable接口方式啓動線程");
    }
    public static void main(String[] args) {
        //要啓動實現Runnablede的線程的話還需要把runnable的實例傳到Thread裏
        new Thread(new  ThreadStartDemo()).start();
    }
}

 

​ c、接口Callable(允許有返回值)

public class ThreadStartDemo implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "實現Callable接口方式啓動線程";
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadStartDemo callableThread = new ThreadStartDemo();
        // 由於new Thread只接受Runnable類型的構造參數,所以要先把Callable包裝一下
        FutureTask<String> futureTask = new FutureTask<>(callableThread);
        new Thread(futureTask).start();
        // 獲取返回值,get方法是阻塞的
        System.out.println(futureTask.get());
    }
}

1.2.2、結束的方法

​ a、 方法執行完自動終止

​ b、拋出異常,又沒有捕獲異常

​ c、早期還提出過三個方法終止線程stop(), resume(), suspend() 。這三個方法都不建議用於終止線程

​ 原因: 一旦調用stop會強行終止線程,無法保證線程的資源正常釋放。

​ suspend()調用後線程是不會釋放資源的,很容易引起死鎖。 不推薦使用 suspend() 去掛起線程的原因,是因爲 suspend() 在導致線程暫停的同時,並不會去釋放任何鎖資源。其他線程都無法訪問被它佔用的鎖。直到對應的線程執行 resume() 方法後,被掛起的線程才能繼續,從而其它被阻塞在這個鎖的線程纔可以繼續執行。但是,如果 resume() 操作出現在 suspend() 之前執行,那麼線程將一直處於掛起狀態,同時一直佔用鎖,這就產生了死鎖。而且,對於被掛起的線程,它的線程狀態居然還是 Runnable。

​ 正確的終止線程的方法:interrupt(),isinterrupted()以及靜態方法interrupted().

三者的區別:

​ d、 interrupt():是屬於Thread類的方法,作用終止一個線程,但並不是強行關閉一個線程(java的線程是協作式的,不是強迫式的,調用一個線程的interrupt()方法並不會強制關閉一個線程,它就好比其他線程對要關閉的線程打了一聲招呼,告訴被關閉線程它要中斷了,但被關閉線程什麼時候關閉完全由它自身做主),線程調用該方法並不會立刻終止。既然調用interrupt()方法起不到終止線程的目的,那麼它爲什麼要這樣設計?這樣設計時爲了讓每個線程有充分的時間去做線程的清理工作。進行開發的時候要對線程的中斷請求進行處理,否則就不是一個設計良好的併發程序。總的來說,它的目的只是把線程中的“中斷標誌位”置爲true

​ e、isInterrupted(),判定當前線程是否處於中斷狀態。通過這個方法判斷中斷標誌位是否爲true。

​ f、static方法isInterrupted(), 也是判斷當前線程是否處於中斷狀態。當調用此方法時,它會把中斷標誌位改爲false。

1.3、線程再認識

​ 1、當線程調用了wait(),join(),sleep()方法時,方法會拋出InterruptedException,這個時候線程的中斷標誌會被複位成爲false,所以這個時候我們應該在catch裏面再調用一次interrupt(),再次中斷一次。

public class ThreadStartDemo extends Thread {
    public ThreadStartDemo(String name) {
        super(name);
    }
    @Override
    public void run() {
         //如果現在這個while的條件不是“!isInterrupted()”而是“true”,
        //那麼即使main方法裏調用了test.interrupt()還是無法終止線程的,這就是java協作式。
        //通過實現Runnable接口創建的線程 -> !Thread.currentThread().isInterrupted()
        while(!isInterrupted()) {
            System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
            try {
                Thread.sleep(1000);
            } catch (Throwable e) {
                e.printStackTrace();
                //當線程調用了wait(),join(),sleep()方法時,方法會拋出InterruptedException,
                //這個時候線程的中斷標誌會被複位成爲false,所以這個時候我們應該在catch裏面再調用一次interrupt(),再次中斷一次。
                interrupt();
            }
        }
    }
    public static void main(String[] args) throws Throwable{
        ThreadStartDemo test = new ThreadStartDemo("MyThread");
        test.start();
        //讓main線程等待
        Thread.sleep(5000);
        test.interrupt();
    }
}

​ 2、開發過程中也可爲線程設置優先級,線程的優先級的範圍爲1~10,缺省值爲5,優先級較高的線程獲得分配的時間片就較高。調用Thread.setPriority()方法進行設置。這個優先級的設置在不同的操作系統中會有不一樣的結果,有些系統設置會忽略這個優先級的設定,有的操作系統可能全部給你設置爲5,所以在程序開發過程中, 不能指望這個操作。

​ 3、守護線程:守護線程的線程和主線程是同時銷燬的,主線程退出了,守護進程就一定會結束。設置守護進程是通過Thread.setDaemon(true)進行設置的,而且需要在調用start()方法之前設置。使用守護線程需要注意:守護進程裏非try..finally是不能保證一定執行finally的。

​ 4、 volatile是最輕量級的保證同步的方法,但它一般只使用於一個線程寫,多個線程讀這種場景。

​ 5、run()和start()的區別:調用run方法其實就像普通的類調用類中的方法一樣,run()方法由誰調用就歸宿與哪個線程。只有調用start()方法纔是真正的啓動線程。

​ 6、就緒狀態也成爲可運行狀態,調用了一個線程的start()方法,形成就處於可運行狀態,但這個時候並不是運行狀態,只有當cpu調度到該線程纔是運行狀態。

​ 7、yield()方法的作用是,當一個線程運行完了,就把cpu的時間讓出來。那麼它與sleep()方法的區別呢?調用sleep()方法,在sleep()的時間段內,cpu是不會再調度該線程,但是調用了yield()方法的下一時刻,cpu還是有可能會調度到該線程的 。

1.4、線程間的共享

1.4.1、synchronized

​ synchronized(內置鎖),要麼加載方法上面,要麼是用作同步塊的形式來使用,最大的作用是確保在多個線程在同一時刻只能有一個線程處於方法或同步塊之中,這樣它就保證了線程對變量訪問的可見性與排差性。 鎖的是對象,不是代碼塊,每個對象在內存的對象頭上有一個標誌位,標誌位上有1~2個字節標誌它爲一個鎖,synchronized的作用就是當所有的線程去搶這個對象的標誌位,誰把這個標誌位指向了自己,那就認爲這個線程搶到了這個鎖。

​ 對象鎖和類鎖:java的對象鎖和類鎖在鎖的概念上基本上和內置鎖是一致的,但是,兩個鎖實際是有很大的區別的,對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。但是有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定實例方法和靜態方法的區別的。

1.4.2、volatile

​ volatile關鍵字是與Java的內存模型有關的,因此在理解volatile關鍵詞之前,我們需要先了解一下與內存模型相關的概念和知識,然後分析了volatile關鍵字的實現原理,最後給出了幾個使用volatile關鍵字的場景。

​ 一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,會強制將修改的值立即寫入主存。(當一個線程修改了共享變量時,另一個線程可以讀取到這個修改後的值。 )每次要用該變量時,總是要在主內存中讀取(非工作內存)。volatile並不是線程安全的,只能保證變量的可見性,不能保證原子性。那麼就具備了兩層語義:

​ 1、保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

​ 2、禁止進行指令重排序。 所以volatile能在一定程度上保證有序性。

volatile關鍵字禁止指令重排序有兩層意思:

1)當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;

  2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

可見性

處理器爲了提高處理速度,不直接和內存進行通訊,而是將系統內存的數據獨到內部緩存後再進行操作,但操作完後不知什麼時候會寫到內存。
​
如果對聲明瞭volatile變量進行寫操作時,JVM會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫會到系統內存。 這一步確保瞭如果有其他線程對聲明瞭volatile變量進行修改,則立即更新主內存中數據。
​
但這時候其他處理器的緩存還是舊的,所以在多處理器環境下,爲了保證各個處理器緩存一致,每個處理會通過嗅探在總線上傳播的數據來檢查 自己的緩存是否過期,當處理器發現自己緩存行對應的內存地址被修改了,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作時,會強制重新從系統內存把數據讀到處理器緩存裏。 這一步確保了其他線程獲得的聲明瞭volatile變量都是從主內存中獲取最新的。

有序性

Lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成。

 

volatile的原理和實現機制

“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”
​
  lock前綴指令實際上相當於一個內存屏障(也稱內存柵欄),內存屏障會提供3個功能:
​
  1)它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
​
  2)它會強制將對緩存的修改操作立即寫入主存;
​
  3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。

應用場景:

1)對變量的寫操作不依賴於當前值
​
2)該變量沒有包含在具有其他變量的不變式中
​
下面列舉幾個Java中使用volatile的幾個場景。
​
①.狀態標記量
    
volatile boolean flag = false;
 //線程1
while(!flag){
    doSomething();
}
  //線程2
public void setFlag() {
    flag = true;
}
​
根據狀態標記,終止線程。
​
②.單例模式中的double check
class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
爲什麼要使用volatile 修飾instance?
​
主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:
​
1.給 instance 分配內存
​
2.調用 Singleton 的構造函數來初始化成員變量
​
3.將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)。
​
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。

 

備註:讀取變量的原始值、進行操作、寫入工作內存 。

1.4.3、ThreadLocal

​ ThreadLoacl(線程變量):可以確保每個線程只使用自己那一部分的東西。例如一個變量使用ThreadLocal包裝的話,那麼每個線程都是使用自己的那一份變量的拷貝。可以理解爲Map

​ ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。可能很多朋友都知道ThreadLocal爲變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。

1.5、線程間協作

1.5.1、等待和通知的標準格式

​ 輪詢無法保證及時性,資源的開銷也比較大,大部分時間都在做無用功。爲了解決這種情況,java裏提供了一種等待和通知機制,當線程調用wait方法會進入等待狀態,當調用notify或notifyAll(首選notifyAll,因爲notify通知的是等待隊列中的一個線程,有可能發生信號丟失的情況。)方法就會喚醒線程。wait、notify、notifyAll這三個方法是對象本身的方法,並不是線程的方法。

​ 在線程之間進行通信,往往有一個”等待和通知的標準範式”,如下: 調用wait的線程(等待方): ① 獲取對象的鎖 ② 在一個循環裏判定條件是否滿足,不滿足就調用wait方法 ③ 條件滿足就執行業務邏輯 通知方: ① 獲取對象的鎖 ② 改變條件

​ ③ 通知所有等待在對象的線

1.5.2、join方法

​ thread.Join把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行的線程。 比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。

t.join();      //調用join方法,等待線程t執行完畢

t.join(1000);  //等待 t 線程,等待時間是1000毫秒。

public class ThreadStartDemo{
​
    public static void main(String[] args) throws Throwable {
         System.out.println("MainThread run start.");
        //啓動一個子線程
         Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("threadA run start.");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("threadA run finished.");
            }
        });
         threadA.start();
         System.out.println("MainThread join before");
         try {
             threadA.join();    //調用join()
        } catch (Exception e) {
             e.printStackTrace();
        }
         System.out.println("MainThread run finished.");
    }
​
}

運行結果如下:

MainThread run start.
MainThread join before
threadA run start.
threadA run finished.
MainThread run finished.
  
對子線程threadA使用了join()方法之後,我們發現主線程會等待子線程執行完成之後才往後執行。
當子線程threadA執行完畢的時候,jvm會自動喚醒阻塞在threadA對象上的線程,在我們的例子中也就是主線程。至此,threadA線程對象被notifyall了,那麼主線程也就能繼續跑下去了。
可以看出,join()方法實現是通過wait()。當main線程調用threadA.join()時候,main線程會獲得線程對象threadA的鎖(wait)意味着拿到該對象的鎖,調用該對象的wait(等待時間),直到該對象喚醒main線程(也就是子線程threadA執行完畢退出的時候)
總結
首先join() 是一個synchronized方法, 裏面調用了wait(),這個過程的目的是讓持有這個同步鎖的線程進入等待,那麼誰持有了這個同步鎖呢?答案是主線程,因爲主線程調用了threadA.join()方法,相當於在threadA.join()代碼這塊寫了一個同步代碼塊,誰去執行了這段代碼呢,是主線程,所以主線程被wait()了。然後在子線程threadA執行完畢之後,JVM會調用lock.notify_all(thread);喚醒持有threadA這個對象鎖的線程,也就是主線程,會繼續執行。

1.5.3、調用yield、sleep、wait、notify等方法對鎖有何影響?

​ 1、調用yield()方法和sleep()方法以後,持有的鎖是不釋放的,所以一般調用這兩個方法的良好寫法是不要寫在synchronized代碼塊外面。

​ 2、調用wait()方法和notify()方法是會釋放鎖的,調用這兩個方法的前提是必須持有鎖,而且調用這兩個方法之後,還是會把這兩個方法所在的synchronized代碼塊中的代碼執行完成才釋放鎖(調用這兩個方法是不會釋放鎖的),所以良好的寫法是寫在synchronized代碼塊中的最後一行。

​ 3、wait()、notify()、notifyAll()方法是和對象綁定一起的,話句話來說,就是你在object1上調用了notifyAll方法,那麼通知的就是在object1上等待的線程,並不能通知到object2對象上的線程。

2、線程的併發工具類

2.1、Fork/Join

什麼是分而治之?
規模爲N的問題,N<閾值,直接解決,N>閾值,將N分解爲K個小規模子問題,子問題互相對立,與原問題形式相同,將子問題的解合併得到原問題的解
動態規範
工作密取:workStealing
Fork/Join使用的標準範式
RecursiveTask有返回值RecursiveAction沒有返回值   

2.2、CountDownLatch作用、應用場景和實戰

作用:是一組線程等待其他的線程完成工作以後在執行,加強版join

await用來等待,countDown負責計數器的減一

Latch 爲門閂的意思。如果翻譯成倒計數門閂,表示:把門鎖起來,不讓裏面的線程跑出來。因此這個類用來控制線程等待,可以讓某個線程等待直到倒計時結束,再開始執行。

發令槍

例子1:

public class CountDownLatchDemo {
​
    private int count  = 0;
    private final static CountDownLatch countDownLatch = new CountDownLatch(5);
    
    private void add() {
        System.out.println("線程:" + Thread.currentThread().getName());
        for(int i = 0; i < 10000;i++) {
            count ++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        CountDownLatchDemo test = new CountDownLatchDemo();
        for(int i  = 0 ; i < 5; i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                        countDownLatch.countDown();
                        test.add();
                }
            }).start();
        }
        countDownLatch.await();
        Thread.sleep(1000);
        System.out.println("----------" + test.count);
    }
}
​
運行結果:
線程:Thread-0
線程:Thread-3
線程:Thread-4
線程:Thread-1
線程:Thread-2
----------41041

 

2.3、CyclicBarrier作用、應用場景和實戰

​ 字面意思迴環柵欄,通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行。叫做迴環是因爲當所有等待線程都被釋放以後,CyclicBarrier 可以被重用。我們暫且把這個狀態就叫做 barrier,當調用 await() 方法之後,線程就處於 barrier 了。

​ 讓一組線程達到某個屏障,被阻塞,一直到組內最後一個線程達到屏障時,屏障開放,所有被阻塞的線程會繼續運行CyclicBarrier(int parties)。

CyclicBarrier(int parties, Runnable barrierAction),屏障開放,barrierAction定義的任務會執行

CountDownLatch和CyclicBarrier辨析

1、countdownlatch放行由第三者控制,CyclicBarrier放行由一組線程本身控制 2、countdownlatch放行條件》=線程數,CyclicBarrier放行條件=線程數

例子1: 用來掛起當前線程,直至所有線程都到達 barrier 狀態再同時執行後續任務;

public class CyclicBarrierDemo {
​
    public static void main(String[] args) {
        int num = 4;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
        for (int i = 0; i < num; i++) {
            new Writer(cyclicBarrier).start();
        }
    }
}
​
class Writer extends Thread {
​
    private CyclicBarrier cyclicBarrier;
​
    public Writer(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }
​
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + "正在寫入數據...");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 寫入數據完畢,等待其他線程寫入...");
            cyclicBarrier.await();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 線程寫入完畢,繼續執行其他任務");
    }
}
運行結果:
Thread-0正在寫入數據...
Thread-3正在寫入數據...
Thread-2正在寫入數據...
Thread-1正在寫入數據...
Thread-1 寫入數據完畢,等待其他線程寫入...
Thread-0 寫入數據完畢,等待其他線程寫入...
Thread-3 寫入數據完畢,等待其他線程寫入...
Thread-2 寫入數據完畢,等待其他線程寫入...
Thread-3 線程寫入完畢,繼續執行其他任務
Thread-1 線程寫入完畢,繼續執行其他任務
Thread-0 線程寫入完畢,繼續執行其他任務
Thread-2 線程寫入完畢,繼續執行其他任務

例子2:讓這些線程等待至一定的時間,如果還有線程沒有到達 barrier 狀態就直接讓到達 barrier 的線程執行後續任務。

public class CyclicBarrierDemoWithAwaitTime {
​
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
 
        for(int i=0;i<N;i++) {
            if(i<N-1)
                new Writer(barrier).start();
            else {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                new Writer(barrier).start();
            }
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
            try {
                Thread.sleep(3000);      //以睡眠來模擬寫入數據操作
                System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其他線程寫入完畢");
                try {
                    cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有線程寫入完畢,繼續處理其他任務...");
        }
    }
}
​
運行結果:
線程Thread-0正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-1寫入數據完畢,等待其他線程寫入完畢
線程Thread-2寫入數據完畢,等待其他線程寫入完畢
線程Thread-0寫入數據完畢,等待其他線程寫入完畢
線程Thread-3正在寫入數據...
java.util.concurrent.TimeoutException
Thread-1所有線程寫入完畢,繼續處理其他任務...
Thread-0所有線程寫入完畢,繼續處理其他任務...
Thread-2所有線程寫入完畢,繼續處理其他任務...
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
線程Thread-3寫入數據完畢,等待其他線程寫入完畢
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)Thread-3所有線程寫入完畢,繼續處理其他任務...
​
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at com.sirius.java.concurrent.gunshot.CyclicBarrierDemoWithAwaitTime$Writer.run(CyclicBarrierDemoWithAwaitTime.java:47)
​
上面的代碼在main方法的for循環中,故意讓最後一個線程啓動延遲,因爲在前面三個線程都達到barrier之後,等待了指定的時間發現第四個線程還沒有達到barrier,就拋出異常並繼續執行後面的任務。

例子3:想在所有線程寫入操作完之後,進行額外的其他操作 .

public class CyclicBarrierDemoWithExtraThing {
​
    public static void main(String[] args) {
        int num = 4;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(num, new Runnable() {
            @Override
            public void run() {
                System.out.println("當前線程:" + Thread.currentThread().getName() + ",所有線程執行完成,進行額外的操作");
            }
        });
        for (int i = 0; i < num; i++) {
            new Writer2(cyclicBarrier).start();
        }
    }
​
}
​
class Writer2 extends Thread {
    private CyclicBarrier cyclicBarrier;
​
    public Writer2(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }
​
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在寫入數據...");
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "寫入數據完畢,等待其他線程寫入...");
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "線程寫入完畢,繼續執行其他任務");
    }
}
​
運行結果:
Thread-0正在寫入數據...
Thread-2正在寫入數據...
Thread-3正在寫入數據...
Thread-1正在寫入數據...
Thread-0寫入數據完畢,等待其他線程寫入...
Thread-2寫入數據完畢,等待其他線程寫入...
Thread-1寫入數據完畢,等待其他線程寫入...
Thread-3寫入數據完畢,等待其他線程寫入...
當前線程:Thread-3,所有線程執行完成,進行額外的操作
Thread-3線程寫入完畢,繼續執行其他任務
Thread-0線程寫入完畢,繼續執行其他任務
Thread-1線程寫入完畢,繼續執行其他任務
Thread-2線程寫入完畢,繼續執行其他任務

例子4:檢測 CyclicBarrier 是否重用

public class CyclicBarrierDemoReusing {
​
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier = new CyclicBarrier(N);
​
        for (int i = 0; i < N; i++) {
            new Writer4(barrier).start();
        }
​
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
        System.out.println("CyclicBarrier重用");
        for (int i = 0; i < N; i++) {
            new Writer4(barrier).start();
        }
    }
​
}
​
class Writer4 extends Thread {
    private CyclicBarrier cyclicBarrier;
​
    public Writer4(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }
​
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 正在寫入數據...");
        try {
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "寫入數據完畢,等待其他線程寫入...");
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "  線程寫入完畢,繼續執行其他任務");
    }
}
​
運行結果:
Thread-0 正在寫入數據...
Thread-1 正在寫入數據...
Thread-3 正在寫入數據...
Thread-2 正在寫入數據...
Thread-0寫入數據完畢,等待其他線程寫入...
Thread-3寫入數據完畢,等待其他線程寫入...
Thread-2寫入數據完畢,等待其他線程寫入...
Thread-1寫入數據完畢,等待其他線程寫入...
Thread-1  線程寫入完畢,繼續執行其他任務
Thread-0  線程寫入完畢,繼續執行其他任務
Thread-3  線程寫入完畢,繼續執行其他任務
Thread-2  線程寫入完畢,繼續執行其他任務
CyclicBarrier重用
Thread-4 正在寫入數據...
Thread-6 正在寫入數據...
Thread-5 正在寫入數據...
Thread-7 正在寫入數據...
Thread-7寫入數據完畢,等待其他線程寫入...
Thread-5寫入數據完畢,等待其他線程寫入...
Thread-4寫入數據完畢,等待其他線程寫入...
Thread-6寫入數據完畢,等待其他線程寫入...
Thread-6  線程寫入完畢,繼續執行其他任務
Thread-5  線程寫入完畢,繼續執行其他任務
Thread-7  線程寫入完畢,繼續執行其他任務
Thread-4  線程寫入完畢,繼續執行其他任務

CountDownLatch一般用於某個線程A等待若干個其他線程執行完任務之後,它才執行; 而CyclicBarrier一般用於一組線程互相等待至某個狀態,然後這一組線程再同時執行; 另外,CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。

 

2.4、Semaphore作用、應用場景和實戰

控制同時訪問某個特定資源的線程數量,用在流量控制 .

Semaphore 翻譯成字面意思爲 信號量,廣義上說,信號量是對鎖的擴展,無論是內部鎖 synchronized 還是重入鎖 ReentrantLock,一次都只允許一個線程訪問一個資源,而信號量卻可以指定多個線程同時訪問一個資源,通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。

/*
*假若一個工廠有 5 臺機器,但是有 8 個工人,一臺機器同時只能被一個工人使用,只有使用完了,其他工人才能繼續使用。那麼我們就可以通過 Semaphore 來實現:
*/
public class SemaphoreDemo {
    public static void main(String[] args) {
        int N = 8; //8 個工人
        Semaphore semaphore = new Semaphore(5);
        for (int i = 0; i < N; i++)
            new Worker(i, semaphore).start();
        ;
    }
​
    static class Worker extends Thread {
        private int num;
        private Semaphore semaphore;
​
        public Worker(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }
        @Override
        public void run() {
            try {
                semaphore.acquire();// 用來獲取一個許可,若無許可能夠獲得,則會一直等待,直到獲得許可。
                 System.out.println("工人 "+this.num+" 佔用一個機器在生產...");
                Thread.sleep(2000);
                 System.out.println("工人 "+this.num+" 釋放出機器");
                semaphore.release();//用來釋放許可。注意,在釋放許可之前,必須先獲獲得許可。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
​
運行結果:
工人 0 佔用一個機器在生產...
工人 1 佔用一個機器在生產...
工人 3 佔用一個機器在生產...
工人 2 佔用一個機器在生產...
工人 4 佔用一個機器在生產...
工人 0 釋放出機器
工人 4 釋放出機器
工人 3 釋放出機器
工人 1 釋放出機器
工人 2 釋放出機器
工人 5 佔用一個機器在生產...
工人 6 佔用一個機器在生產...
工人 7 佔用一個機器在生產...
工人 7 釋放出機器
工人 5 釋放出機器
工人 6 釋放出機器

 

2.5、Exchange作用、應用場景和實戰

兩個線程間的數據交換

2.6、Callable、Future和FutureTask

​ 2.6.1、Callable與Runnable

先說一下java.lang.Runnable吧,它是一個接口,在它裏面只聲明瞭一個run()方法:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();//由於run()方法返回值爲void類型,所以在執行完任務之後無法返回任何結果。
}

Callable位於java.util.concurrent包下,它也是一個接口,在它裏面也只聲明瞭一個方法,只不過這個方法叫做call():

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
可以看到,這是一個泛型接口,call()函數返回的類型就是傳遞進來的V類型。
那麼怎麼使用Callable呢?一般情況下是配合ExecutorService來使用的,在ExecutorService接口中聲明瞭若干個submit方法的重載版本:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
​
第一個submit方法裏面的參數類型就是Callable。
​
暫時只需要知道Callable一般是和ExecutorService配合來使用的,具體的使用方法講在後面講述。
​
一般情況下我們使用第一個submit方法和第三個submit方法,第二個submit方法很少使用。

​ 2.6.2、Future

 Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。

  Future類位於java.util.concurrent包下,它是一個接口:

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  在Future接口中聲明瞭5個方法,下面依次解釋每個方法的作用:
​
    cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning爲true還是false,此方法肯定返回false,即如果取消已經完成的任務會返回false;如果任務正在執行,若mayInterruptIfRunning設置爲true,則返回true,若mayInterruptIfRunning設置爲false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning爲true還是false,肯定返回true。
    isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。
    isDone方法表示任務是否已經完成,若任務完成,則返回true;
    get()方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;
    get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。
​
  也就是說Future提供了三種功能:
​
  1)判斷任務是否完成;
​
  2)能夠中斷任務;
​
  3)能夠獲取任務執行結果。
​
  因爲Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了下面的FutureTask。

​ 2.6.3、FutureTask

我們先來看一下FutureTask的實現:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask類實現了RunnableFuture接口,我們看一下RunnableFuture接口的實現:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}
 可以看出RunnableFuture繼承了Runnable接口和Future接口,而FutureTask實現了RunnableFuture接口。所以它既可以作爲Runnable被線程執行,又可以作爲Future得到Callable的返回值。

 事實上,FutureTask是Future接口的一個唯一實現類。

​ 2.6.4、實例:

​ 例子1:使用Callable+Future獲取執行結果

public class CallableFutureDemo {
​
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<Integer> result = executor.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("子線程在進行計算");
                Thread.sleep(3000);
                int sum = 0;
                for (int i = 0; i < 100; i++)
                    sum += i;
                return sum;
            }
​
        });
        executor.shutdown();
​
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
​
        System.out.println("\"主線程在執行任務");
​
        try {
            System.out.println("task運行結果" + result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
​
        System.out.println("所有任務執行完畢");
    }
}
​

​ 例子2: Callable和FutureTask

public class CallableFutureTaskDemo {
​
    public static void main(String[] args) {
        //第一種方式
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
​
            @Override
            public Integer call() throws Exception {
                System.out.println("子線程在進行計算");
                Thread.sleep(3000);
                int sum = 0;
                for (int i = 0; i < 100; i++)
                    sum += i;
                return sum;
            }
        });
        //單線程
//      new Thread(futureTask).start();
        //線程池
        executor.submit(futureTask);
        executor.shutdown();
​
         //第二種方式,注意這種方式和第一種方式效果是類似的,只不過一個使用的是ExecutorService,一個使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();*/
​
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
​
        System.out.println("主線程在執行任務");
​
        try {
            System.out.println("task運行結果" + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
​
        System.out.println("所有任務執行完畢");
    }
}

​ 例子3:實現自己的FutureTask

public class WlFutureTask<V> implements Runnable, Future<V> {
​
    private Callable<V> callable;// 封裝業務邏輯
​
    V result;
    
    public WlFutureTask(Callable<V> callable) {
        this.callable = callable;
    }
​
    @Override
    public void run() {
        //執行業務邏輯
        try {
            result = callable.call();// http接口,返回值
            System.out.println("----------");
            synchronized (this) {
                this.notifyAll();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
​
    @Override
    public V get() throws InterruptedException, ExecutionException {
        if(result != null) {
            return result;
        }
        synchronized (this) {
            this.wait();//阻塞
        }
        return result;
    }
​
    @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
        return false;
    }
​
    @Override
    public boolean isCancelled() {
        return false;
    }
​
    @Override
    public boolean isDone() {
        return false;
    }
​
    @Override
    public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return null;
    }
​
}
​
測試:
​
public class WlFutureTaskTest {
​
    
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Thread.sleep(3000);
                return 1;
            }
        };
        
        WlFutureTask<Integer> futureTask = new WlFutureTask<Integer>(callable);
        
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
​

 

3、原子操作CAS

3.1、什麼是原子操作

​ 不可被中斷的一個或者一系列操作

3.2、實現原子操作的方式

​ Java可以通過鎖和循環CAS的方式實現原子操作

3.3、CAS( Compare And Swap ) 爲什麼要有CAS?

​ Compare And Swap就是比較並且交換的一個原子操作,由Cpu在指令級別上進行保證。

爲什麼要有CAS:因爲通過鎖實現原子操作時,其他線程必須等待已經獲得鎖的線程運行完以後才能獲得資源,這樣就會佔用系統的大量資源

3.4、CAS包含哪些參數?

​ CAS包含三個參數:1、變量所在內存地址V;2、變量對應的值A;3、我們將要修改的值B。如果說V上的變量的值是A的話,就用B重新賦值,如果不是A,那就什麼事也不做,操作的返回結果原值是多少。

循環CAS:在一個(死)循環【for(;;)】裏不斷進行CAS操作,直到成功爲止(自旋操作即死循環)。

3.5、CAS實現原子操作的三大問題

​ 1、 ABA問題:其他的線程把值改成了B,很快改成了A,原子操作的線程發現值是A就修改,這樣會有問題。解決ABA,引入版本號:1A-》2C-》3A

​ 2、 循環時間很長的話,cpu的負荷比較大

​ 3、 對一個變量進行操作可以,同時操作多個共享變量有點麻煩

3.6、CAS線程安全(面試點)

​ 通過硬件層面的阻塞實現原子操作的安全

3.7、原子更新基本類型類

​ AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference。

3.8、原子更新數組類

​ AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

3.9、原子更新引用類型提供的類

​ ·AtomicReference: 可以解決更新多個變量的問題

​ ·AtomicStampedReference:解決ABA問題 使用數字作爲版本 關心得是有幾個人改過

​ ·AtomicMarkableReference:解決ABA問題 使用Boolean作爲版本,關心的是有沒有修改過

3.10、原子更新字段類

​ Atomic包提供了以下3個類進行原子字段更新。

​ ·AtomicReferenceFieldUpdater:

​ ·AtomicIntegerFieldUpdater:

​ ·AtomicLongFieldUpdater:

​ 違反了面向對象的原則,一般不使用

4、顯示鎖和AQS

4.1、顯示鎖

4.1.1、Lock接口、核心方法和使用以及和synchronized的比較

lock顯示鎖是基於jdk層面的實現是接口,通過這個接口可以實現同步訪問。

不同於synchronized關鍵字是java內置特性,基於jvm實現的,阻塞式獲取鎖。

Lock接口核心方法:

public interface Lock {
    void lock(); //邏輯劃分-獲取鎖(如果獲取不到鎖,那麼將會進入阻塞狀態,與synchronized關鍵字一樣)
    void lockInterruptibly() throws InterruptedException;//邏輯劃分-獲取鎖(可中斷的鎖獲取操作在嘗試獲取鎖的過程中,如果不能夠獲取到,如果被中斷,那麼它將能夠感知到這個中斷,而不是一直阻塞下去.)
    boolean tryLock();//邏輯劃分-嘗試獲取鎖
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//邏輯劃分-嘗試獲取鎖
    void unlock();//邏輯劃分-釋放鎖
    Condition newCondition();//邏輯劃分-條件變量
}

小結lockInterruptibly:

如果鎖不可用(被其他線程持有),除非發生以下事件,否則將會等待
    該線程成功獲得鎖
    發生中斷
如果當前線程遇到下面的事件,則將拋出 InterruptedException,並清除當前線程的已中斷狀態。
    在進入此方法時已經設置了該線程的中斷狀態
    在獲取鎖時被中斷

unlock方法介紹:

unlock並沒有什麼特殊的,他替代了synchronized關鍵字隱式的解鎖操作
通常需要在finally中確保unlock操作會被執行,之前提到過,對於synchronized關鍵字解鎖是隱式的,也是必然的,即使出現錯誤,JVM也會保障能夠正確的解鎖
但是對於Lock接口提供的unlock操作,則必須自己確保能夠正確的解鎖  

tryLock方法介紹:

相對於synchronized,Lock接口另一大改進就是try lock
顧名思義,嘗試獲取鎖,既然是嘗試,那顯然並不會勢在必得
tryLock方法就是一次嘗試,如果鎖可用,則獲取鎖,並立即返回值 true。如果鎖不可用,則此方法將立即返回值 false
也就是說方法會立即返回,如果獲取到鎖返回true,否則返回false,不管如何都是立馬返回.
tryLock只是一次嘗試,如果你需要不斷地進行嘗試,那麼可以使用while替代if的條件判斷
儘管tryLock只是一次的測試,但是可以藉助於循環(有限或者無限)進行多次測試  

tryLock(long time, TimeUnit unit) 方法介紹:

在指定的超時時間內,如果能夠獲取到鎖,那麼將會返回true;
如果超過了指定的時間,但是卻不能獲取到鎖,那麼將會返回false;
另外很顯然,這個方法是可中斷的,也就是說如果嘗試過程中,出現了中斷,那麼他將會拋出InterruptedException
所以,對於這個方法,他會一直嘗試獲取鎖(也可以認爲是一定時長內的“阻塞”,當然可以被中斷),除非:
​
    該線程成功獲得鎖
    超過了超時時長
    該線程被中斷
​
可以認爲是lockInterruptibly的限時版本
如果沒有發生中斷,也認爲他就是“定時版本的lock()”
不管怎麼理解,只需要記住:他會在一定時長內嘗試進行鎖的獲取,也支持中斷

Condition介紹:

在隱式鎖的邏輯中,藉助於Java底層機制,每個對象都有一個相關聯的鎖與監視器
對於synchronized的隱式鎖邏輯就是藉助於鎖與監視器,從而進行線程的同步與通信協作
在顯式鎖中,Lock接口提供了synchronized的語意,對於監視器的概念,則藉助於Condition,但是很顯然,Condition也是與鎖關聯的
Lock接口提供了方法Condition newCondition();
Condition也是一個接口,他定義了相關的監視器方法
在顯式鎖中,可以定義多個Condition,也就是一個鎖,可以對應多個監視器,可以更加細粒度的進行同步協作的處理

鎖小結:

對於lock方法和unlock方法,就是類似於synchronized關鍵字的加鎖和解鎖,並沒有什麼特別的
其他幾個方法是Lock接口針對於鎖獲取的阻塞以及可中斷兩個方面進行了拓展
隱式鎖的阻塞以及不可中斷,導致一旦開始嘗試獲取,那麼則沒辦法喚醒,將會一直等待,除非獲得
​
    lockInterruptibly()是阻塞式的,如果獲取不到會一直等待,但是他是可中斷的,能夠通過阻塞打破這種等待
    tryLock()不會進行任何阻塞,只是嘗試獲取一下,能獲取到就獲取,獲取不到就false,拉倒
    tryLock(long time, TimeUnit unit),即是可中斷的,又是限時阻塞的,即使不中斷,也不會一直阻塞,即使處於阻塞中(超時時長還沒到),也可以隨時中斷
​
對於lockInterruptibly()方法以及tryLock(long time, TimeUnit unit),都支持中斷,但是需要注意:
在某些實現中可能無法中斷鎖獲取,即使可能,該操作的開銷也很大  
​

總結:

Lock接口提供了相對於synchronized關鍵字,而更爲靈活的一種同步手段
它的核心與本質仍舊是爲了線程的同步與協作通信
所以它的核心仍舊是鎖與監視器,也就是Lock接口與Condition接口
但是靈活是有代價的,所以並不需要在所有的地方都嘗試使用顯式鎖,如果場景滿足需要,synchronized仍舊是一種很好的解決方案(也是應該被優先考慮的一種方式)
與synchronized再次對比下
    synchronized是JVM底層實現的,Lock是JDK接口層面的
    synchronized是隱式的,Lock是顯式的,需要手動加鎖與解鎖
    synchronized烏無論如何都會釋放,即使出現錯誤,Lock需要自己保障正確釋放
    synchronized是阻塞式的獲取鎖,Lock可以阻塞獲取,可中斷,還可以嘗試獲取,還可以設置超時等待獲取
    synchronized無法判斷鎖的狀態,Lock可以進行判斷
    synchronized可重入,不可中斷,非公平,Lock可重入,可中斷、可配置公平性(公平和非公平都可以)
    如果競爭不激烈,兩者的性能是差不多的,可是synchronized的性能還在不斷的優化,當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized   
    等   

4.1.2、ReentrantLock可重入鎖與鎖的公平和非公平

重入鎖:

重入鎖指的是當前線成功獲取鎖後,如果再次訪問該臨界區,則不會對自己產生互斥行爲。Java中對ReentrantLock和synchronized都是可重入鎖,synchronized由jvm實現可重入即使,ReentrantLock都可重入性基於AQS實現。同時,ReentrantLock還提供公平鎖和非公平鎖兩種模式。

ReentrantLock重入鎖:

重入鎖的基本原理是判斷上次獲取鎖的線程是否爲當前線程,如果是則可再次進入臨界區,如果不是,則阻塞。
由於ReentrantLock是基於AQS實現的,底層通過操作同步狀態來獲取鎖,下面看一下非公平鎖的實現邏輯:
        final boolean nonfairTryAcquire(int acquires) {
             //獲取當前線程
            final Thread current = Thread.currentThread();
             //通過AQS獲取同步狀態
            int c = getState();
            //同步狀態爲0,說明臨界區處於無鎖狀態,
            if (c == 0) {
             //修改同步狀態,即加鎖
                if (compareAndSetState(0, acquires)) {
                    //將當前線程設置爲鎖的owner
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果臨界區處於鎖定狀態,且上次獲取鎖的線程爲當前線程
            else if (current == getExclusiveOwnerThread()) {
             //則遞增同步狀態
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
重入鎖的最主要邏輯就鎖判斷上次獲取鎖的線程是否爲當前線程。

非公平鎖:

非公平鎖是指當鎖狀態爲可用時,不管在當前鎖上是否有其他線程在等待,新近線程都有機會搶佔鎖。
上述代碼即爲非公平鎖和核心實現,可以看到只要同步狀態爲0,任何調用lock的線程都有可能獲取到鎖,而不是按照鎖請求的FIFO原則來進行的。

公平鎖:

公平鎖是指當多個線程嘗試獲取鎖時,成功獲取鎖的順序與請求獲取鎖的順序相同。
  protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                ////此處爲公平鎖的核心,即判斷同步隊列中當前節點是否有前驅節點
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
從上面的代碼中可以看出,公平鎖與非公平鎖的區別僅在於是否判斷當前節點是否存在前驅節點!hasQueuedPredecessors() &&,由AQS可知,如果當前線程獲取鎖失敗就會被加入到AQS同步隊列中,那麼,如果同步隊列中的節點存在前驅節點,也就表明存在線程比當前節點線程更早的獲取鎖,故只有等待前面的線程釋放鎖後才能獲取鎖。

 

4.1.3、ReentrantReadWriteLock使用場景

一個基於AQS到讀寫鎖實現ReentrantReadWriteLock,該讀寫鎖到實現原理是:將同步變量state按照高16位和低16位進行拆分,高16位表示讀鎖,低16位表示寫鎖。

 

   protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
上述代碼的處理流程已經非常清晰:
​
    1、獲取同步狀態,並從中分離出低16爲的寫鎖狀態
    2、如果同步狀態不爲0,說明存在讀鎖或寫鎖
    3、如果存在讀鎖(c !=0 && w == 0),則不能獲取寫鎖(保證寫對讀的可見性)
    4、如果當前線程不是上次獲取寫鎖的線程,則不能獲取寫鎖(寫鎖爲獨佔鎖)
    5、如果以上判斷均通過,則在低16爲寫鎖同步狀態上利用CAS進行修改(增加寫鎖同步狀態,實現可重入)
    6、將當前線程設置爲寫鎖的獲取線程
​
寫鎖的釋放過程與獨佔鎖基本相同:
        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
       在釋放的過程中,不斷減少讀鎖同步狀態,只爲同步狀態爲0時,寫鎖完全釋放。
讀鎖的獲取與釋放
​
讀鎖是一個共享鎖,獲取讀鎖的步驟如下:
    1、獲取當前同步狀態
    2、計算高16爲讀鎖狀態+1後的值
    3、如果大於能夠獲取到的讀鎖的最大值,則拋出異常
    4、如果存在寫鎖並且當前線程不是寫鎖的獲取者,則獲取讀鎖失敗
    5、如果上述判斷都通過,則利用CAS重新設置讀鎖的同步狀態
​
讀鎖的獲取步驟與寫鎖類似,即不斷的釋放寫鎖狀態,直到爲0時,表示沒有線程獲取讀鎖。

 

4.1.4、Condition用法

https://blog.csdn.net/a_runner/article/details/80640675

此前,我們知道用synchronized與wait()和notify()/notifyAll()方法結合可以實現等待/通知模式。但是,在使用notify()/notifyAll()方法進行通知時,被通知的線程卻是由JVM隨機選擇的。爲了擺脫這種窘境,Java在1.5引入了ReentrantLock和Condition類結合使用來達到有選擇性的進行線程通知,在調度線程上更加靈活。下面是個測試方法:

4.2、LockSupport工具進階

ockSupport是JDK中比較底層的類,用來創建鎖和其他同步工具類的基本線程阻塞原語。
​
     Java鎖和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通過調用LockSupport.park()和LockSupport.unpark()實現線程的阻塞和喚醒的。LockSupport很類似於二元信號量(只有1個許可證可供使用),如果這個許可還沒有被佔用,當前線程獲取許可並繼續執行;如果許可已經被佔用,當前線程阻塞,等待獲取許可。
​
     LockSupport中的park() 和 unpark() 的作用分別是阻塞線程和解除阻塞線程,而且park()和unpark()不會遇到“Thread.suspend 和 Thread.resume所可能引發的死鎖”問題。因爲park() 和 unpark()有許可的存在;調用 park() 的線程和另一個試圖將其 unpark() 的線程之間的競爭將保持活性。
​
LockSupport函數列表
​
// 返回提供給最近一次尚未解除阻塞的 park 方法調用的 blocker 對象,如果該調用不受阻塞,則返回 null。
static Object getBlocker(Thread t)
// 爲了線程調度,禁用當前線程,除非許可可用。
static void park()
// 爲了線程調度,在許可可用之前禁用當前線程。
static void park(Object blocker)
// 爲了線程調度禁用當前線程,最多等待指定的等待時間,除非許可可用。
static void parkNanos(long nanos)
// 爲了線程調度,在許可可用前禁用當前線程,並最多等待指定的等待時間。
static void parkNanos(Object blocker, long nanos)
// 爲了線程調度,在指定的時限前禁用當前線程,除非許可可用。
static void parkUntil(long deadline)
// 爲了線程調度,在指定的時限前禁用當前線程,除非許可可用。
static void parkUntil(Object blocker, long deadline)
// 如果給定線程的許可尚不可用,則使其可用。
static void unpark(Thread thread)
​
說明:LockSupport是通過調用Unsafe函數中的接口實現阻塞和解除阻塞的。
​
park和wait的區別
​
在調用對象的Wait之前當前線程必須先獲得該對象的監視器(Synchronized),被喚醒之後需要重新獲取到監視器才能繼續執行。
​
而LockSupport並不需要獲取對象的監視器。LockSupport機制是每次unpark給線程1個"許可"——最多隻能是1,而park則相反,如果當前線程有許可,那麼park方法會消耗1個並返回,否則會阻塞線程直到線程重新獲得許可,在線程啓動之前調用 park/unpark方法沒有任何效果。
​
因爲它們本身的實現機制不一樣,所以它們之間沒有交集,也就是說LockSupport阻塞的線程,notify/notifyAll沒法喚醒.
​
總結下 LockSupport的park/unpark和Object的wait/notify:
​
    面向的對象不同;
    跟Object的wait/notify不同LockSupport的park/unpark不需要獲取對象的監視器;
    實現的機制不同,因此兩者沒有交集。
​
雖然兩者用法不同,但是有一點, LockSupport 的park和Object的wait一樣也能響應中斷.
        
public class MyLock implements Lock{
​
    AtomicReference<Thread>  owner = new AtomicReference<Thread>();
    public LinkedBlockingDeque<Thread> waiter = new LinkedBlockingDeque<>();
    
    
    @Override
    public void lock() {
        while(!owner.compareAndSet(null, Thread.currentThread())) {
            waiter.add(Thread.currentThread());
            LockSupport.park();
            waiter.remove(Thread.currentThread());
        }
    }
​
    @Override
    public void unlock() {
        if(owner.compareAndSet(Thread.currentThread(),null)){
            Object[] objects = waiter.toArray();
            for(Object object :objects){
                Thread next = (Thread)object;
                LockSupport.unpark(next);
            }
        }
    }
    
    
    
    @Override
    public void lockInterruptibly() throws InterruptedException {
        
    }
​
    @Override
    public boolean tryLock() {
        return false;
    }
​
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }
​
    
​
    @Override
    public Condition newCondition() {
        return null;
    }
​
}
測試:
public class MyLockTest {
​
    MyLock myLock = new MyLock();
    private int i = 0;
​
    public void incr() {
        // �ȽϺ��滻
        myLock.lock();
        i++;
        myLock.unlock();
​
    }
​
    public static void main(String[] args) {
        MyLockTest demo = new MyLockTest();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    demo.incr();
                }
            }).start();
        }
​
        try {
            Thread.sleep(2000);
            System.out.println("i=" + demo.i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

4.3、AbstractQueuedSynchronizer實現及源碼分析

AQS是AbustactQueuedSynchronizer的簡稱,它是一個Java提高的底層同步工具類,用一個int類型的變量表示同步狀態,並提供了一系列的CAS操作來管理這個同步狀態。AQS的主要作用是爲Java中的併發同步組件提供統一的底層支持,例如ReentrantLock,CountdowLatch就是基於AQS實現的,用法是通過繼承AQS實現其模版方法,然後將子類作爲同步組件的內部類。

5、併發容器

5.1、ConcurrentHashMap

5.2、其他併發容器

5.3、阻塞隊列

6、線程池

7、併發安全

--------------------------------------------------------------------------------------

1、線程基礎、線程之間的共享與協作
https://blog.csdn.net/aimashi620/article/details/82017700
2、LockSupport工具進階
https://www.cnblogs.com/moonandstar08/p/5132012.html
3、原子操作CAS
https://www.cnblogs.com/wangzhuxing/p/5207019.html
4、顯示鎖和AQS
https://www.cnblogs.com/waterystone/p/4920797.html
https://blog.csdn.net/zhangdong2012/article/details/79983404
5、AbstractQueuedSynchronizer實現及源碼分析
http://www.cnblogs.com/micrari/p/6937995.html
6、併發容器和併發工具類
https://www.cnblogs.com/love-yang/p/9798271.html
http://www.cnblogs.com/leeSmall/p/8439263.html
http://www.importnew.com/21889.html
7、線程池
https://www.cnblogs.com/dolphin0520/p/3932921.html
隊列介紹https://www.cnblogs.com/coprince/p/6349401.html
創建方式介紹https://blog.csdn.net/HepBen/article/details/80088719
8、併發安全
https://blog.csdn.net/weixin_42447959/article/details/83758933
https://www.cnblogs.com/timlearn/p/4012501.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章