java 鎖 及 線程

一、基礎(chǔ)感念

在了解鎖之前,有很多的基礎(chǔ)感念需要先理解以下,方便以后我們對各種情況的鎖的問題,有更好的認(rèn)識。

同步 和 異步

?? 同步就是多個(gè)任務(wù)一個(gè)一個(gè)執(zhí)行,即你在學(xué)習(xí)的時(shí)候不可能會打游戲,打游戲的時(shí)候不可能在學(xué)習(xí)。
?? 異步就是我洗衣服可以用洗衣機(jī)洗,邊洗邊打電話,而且打電話的同時(shí)還是能在做其他的事情,這個(gè)就是異步執(zhí)行,但異步執(zhí)行是一種,即做即完的

并發(fā) 和 并行

?? 并行 和 異步看似很像,但感念是完全不一樣的。如果上述說異步是一個(gè)人可以做很多事情,那并行可以說多個(gè)人做不同的事情,即多個(gè)CPU處理不同的指令才能叫做并行,一個(gè)CPU處理多線程并不能稱之為并行,而是并發(fā)。

臨界區(qū)

?? 臨界區(qū)用來表示一種公共資源或共享數(shù)據(jù),可以被多個(gè)線程使用。但是每一次只能有一個(gè)線程使用它,一旦臨界區(qū)資源被占用,其他線程要想使用這個(gè)資源,就必須等待。
?? 在并行程序中,臨界區(qū)資源是要被保護(hù)的對象,如果資源同時(shí)被兩個(gè)線程操作,則會得到破壞。

阻塞 和 非阻塞

?? 阻塞是當(dāng)臨界資源被搶占,其他線程則需要在外等待資源釋放,這種等待的過程稱為阻塞。
?? 非阻塞是不會受因?yàn)橘Y源被搶占,而不去做其他事情。

死鎖 饑餓 活鎖

?? 死鎖、饑餓、活鎖都屬于多線程活躍性問題。
?? 死鎖是一個(gè)嚴(yán)重的程序上設(shè)計(jì)出現(xiàn)的問題,當(dāng)一個(gè)資源被占用,因程序的意外問題,導(dǎo)致資源無法被釋放,則其他線程就一直等待造成的情況被稱為死鎖。
?? 饑餓是指某一個(gè)或者多個(gè)線程因?yàn)榉N種原因無法獲得所需的資源,導(dǎo)致一直無法執(zhí)行。比如他的優(yōu)先級可能太低,而高優(yōu)先級的線程不斷搶占它需要的資源,導(dǎo)致底優(yōu)先級線程無法工作。
?? 活鎖是多個(gè)線程之間互相謙讓而導(dǎo)致的,你讓我我讓你,或者說他們級別一樣導(dǎo)致。

并發(fā)的級別

?? 由于臨界區(qū)的存在,多線程之間的并發(fā)必須受到控制。根據(jù)控制并發(fā)的策略,我們可以把并發(fā)的級別進(jìn)行分類,大致可以分為阻塞、無饑餓、無障礙、無鎖、無等待幾種。

無饑餓

?? 饑餓的產(chǎn)生是因?yàn)榈變?yōu)先級在臨界區(qū)被高優(yōu)先級的線程插隊(duì)而導(dǎo)致一直無法獲取資源(也可以稱未非公平鎖),解決饑餓就是讓鎖變得公平,要想獲得資源,就必須乖乖排隊(duì),管你優(yōu)先級高低,先到先得。

無障礙

?? 無障礙是一種最弱的非阻塞調(diào)度。兩個(gè)線程如果是無障礙的執(zhí)行,那么他們不會因?yàn)榕R界區(qū)的問題導(dǎo)致一方被掛起。也就是說大家都可以大搖大擺的進(jìn)入臨界區(qū),那么如果一起修改共享數(shù)據(jù),把數(shù)據(jù)修改壞了怎么辦?對于無障礙的線程來說,一旦檢測到這種情況,它就會立即對自己所作的的修改進(jìn)行回滾,確保數(shù)據(jù)安全。但如果沒有數(shù)據(jù)競爭發(fā)生,那么線程就可以順利完成自己的工作,走出臨界區(qū)。
?? 如果說阻塞的控制方式是悲觀策略。也就是說,系統(tǒng)認(rèn)為兩個(gè)線程之間很有可能發(fā)生不幸的沖突,因此,以保護(hù)共享數(shù)據(jù)為第一優(yōu)先級。相對來說,非阻塞的調(diào)度就是一種樂觀的策略。它認(rèn)為多個(gè)線程之間很有可能不會發(fā)生沖突,或者說概率不大,因此大家都應(yīng)該無障礙的執(zhí)行,但是一旦檢測到?jīng)_突,就應(yīng)該回滾。
?? 從這個(gè)策略中可以看到,無障礙的多線程程序不一定能順暢的運(yùn)行。因?yàn)楫?dāng)臨界區(qū)中存在嚴(yán)重的沖突時(shí),所有的線程都可能不斷的回滾自己的操作,而沒有一個(gè)線程可以走出臨界區(qū),這種情況會影響系統(tǒng)的正常執(zhí)行。所以,我們可能會非常希望在這一堆線程中,至少可以有一個(gè)線程在有限的時(shí)間內(nèi)完成自己的操作,而退出臨界區(qū)。這樣至少可以保證系統(tǒng)不會再臨界區(qū)中無限的等待。
?? 一種可行的無障礙實(shí)現(xiàn)可以依賴一個(gè)“一致性標(biāo)記”來實(shí)現(xiàn)。線程在操作之前,先讀取并保存這個(gè)標(biāo)記,在操作完成后,再次讀取,檢查這個(gè)標(biāo)記是否被更改過,如果說兩者是一致的,則說明資源區(qū)沒有沖突。如果不一致,則說明資源可能在操作過程中與其他寫線程沖突,需要重試操作。而任何對資源有修改操作的線程,在修改數(shù)據(jù)前,都需要更新這個(gè)一致性標(biāo)記,表示數(shù)據(jù)不再安全。

無鎖

?? 無鎖的并行都是無障礙的。在無鎖的情況下,所有的線程都能嘗試對臨界區(qū)進(jìn)行訪問,但不同的是,無鎖的并發(fā)保證必然有一個(gè)線程能夠在有限時(shí)間內(nèi)完成操作離開臨界區(qū)。
?? 在無鎖的調(diào)用中,一個(gè)典型的特點(diǎn)是可能會包含一個(gè)去窮循環(huán)。在這個(gè)循環(huán)中,線程會不斷嘗試修改共享變量。如果沒有沖突,修改成功,程序退出 ,否則繼續(xù)嘗試修改。但無論如何,無鎖的并行總能保證有一個(gè)線程可以勝出的,不至于全軍覆沒。至于臨界區(qū)中競爭失敗的線程,他們則必須不斷重試,直到自己勝利。如果運(yùn)氣不好,總是嘗試不成功,則會出類似饑餓的現(xiàn)象,線程會停止不前。

無等待

?? 無鎖只要求有一個(gè)線程可以在有限步內(nèi)完成操作,而無等待則在無鎖的基礎(chǔ)上更進(jìn)一步進(jìn)行擴(kuò)展。它要求所有的線程都必須在有限步內(nèi)完成,這樣就不會引起饑餓問題。如果再進(jìn)行優(yōu)化,還可以進(jìn)一步分解為有限無等待和線程數(shù)無關(guān)的無等待幾種,他們之前的區(qū)別只是對循環(huán)次數(shù)的限制不同。
?? 一種典型的無等待結(jié)構(gòu)就是RCU(read-copy-update)。它的基本思想是,對數(shù)據(jù)的讀可以不加控制。因此所有的讀線程都是無等待的,它們既不會被鎖定等待也不會引起任何沖突。但在寫數(shù)據(jù)的時(shí)候,先取得原始數(shù)據(jù)的副本,接著只修改副本數(shù)據(jù),修改完成后,在合適的時(shí)機(jī)回寫數(shù)據(jù)。

原子性

?? 是指一個(gè)操作是不可被中斷的,即使多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開始,就不會被其他線程干擾。

可見性

?? 指當(dāng)一個(gè)線程修改了共享變量的值,其他線程是否能夠立即知道這個(gè)修改。

有序性

?? 有序性的問題是因?yàn)樵诔绦驁?zhí)行時(shí),可能會進(jìn)行指令的重排,重排后的指令與原指令的順序未必一致。(這種情況會出現(xiàn)在并發(fā)程序設(shè)計(jì)中)

二、線程

狀態(tài)

NEW 新建;
RUNNABLE 可運(yùn)行狀態(tài);
BLOCKED 阻塞(遇到 synchronized,直到獲得鎖);
WAITING 無時(shí)間的等待( wait(),notify());
TIMED_WAITING 有時(shí)間的等待;
TERMINATED 結(jié)束。

suspend()暫停 resume()繼續(xù)

?? 字面意思,但 suspend() 不會釋放鎖,必須調(diào)用 resume()才能釋放鎖,但是如果意外的 resume() 比 suspend() 提前執(zhí)行,則其他線程永遠(yuǎn)等待,變?yōu)樗梨i。

stop() 強(qiáng)行終止線程

?? Thread.stop(); 強(qiáng)行終止線程,會導(dǎo)致數(shù)據(jù)不一致,破壞數(shù)據(jù)。

interrupt() isInterrupted() interrupted() 中斷線程

?? Thread.interrupt() 中斷線程,也就是設(shè)置中斷標(biāo)志位。Thread.isInterrupted() 判斷當(dāng)前線程是否被中斷。 Thread.interrupted() 也是用來判斷當(dāng)前線程的中斷狀態(tài)。如果在線程中使用了 Thread.sleep(),那么要中斷一個(gè)線程必須也在 Thread.sleep() 的catch 語句中 在執(zhí)行一次當(dāng)前線程的中斷。

Thread.sleep() 方法由于中斷而拋出異常,此時(shí),他會清除中斷標(biāo)志,如果不加處理,那么在下次執(zhí)行線程時(shí),就無法判斷這個(gè)中斷標(biāo)志,會繼續(xù)執(zhí)行線程,并不會達(dá)到中斷線程。
中斷是不會釋放鎖的。

    public static void main(String[] args) throws InterruptedException {
        String a = "1";
        Thread[] threads = new Thread[2];
        for(int i=0;i<2;i++){
            int b = i;
            threads[i] = new Thread(() -> {
                while (true) {
                    synchronized (a) {
                        try {
                            System.out.println("線程啟動 " + b);
                            if (Thread.currentThread().isInterrupted()) {
                                System.out.println("線程中斷" + b);
                                break;
                            }
                            Thread.sleep(5000);
                            System.out.println("執(zhí)行完畢 " + b);
                        } catch (InterruptedException e) {
                            System.out.println("老子被中斷了 " + b);
                            // 這里必須在中斷一次,否則
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            });
        }
        threads[0].start();
        Thread.sleep(2000);
        threads[0].interrupt();
        threads[1].start();
    }
輸出結(jié)果:
線程啟動 0
老子被中斷了 0
線程啟動 0
線程中斷0
線程啟動 1
執(zhí)行完畢 1
線程啟動 1
執(zhí)行完畢 1
線程啟動 1
wait() notify() notifyAll() 等待和喚醒

?? 如果一個(gè)線程調(diào)用了 object.wait(),那么它就會進(jìn)入object對象的等待隊(duì)列,這個(gè)等待隊(duì)列中可能會有多個(gè)線程,因?yàn)橄到y(tǒng)運(yùn)行多個(gè)線程同時(shí)等待某一個(gè)對象。當(dāng) object.notify() 被調(diào)用時(shí),它就會從這個(gè)等待隊(duì)列中,隨機(jī)選擇一個(gè)線程,并將其喚醒。需要大家注意的是這個(gè)選擇是不公平的,并不是先等待的線程會優(yōu)先被選擇,這個(gè)選擇完全是隨機(jī)的。object.notifyAll() 它和notify() 的功能基本一致,但不同的是,它會喚醒在這個(gè)等待隊(duì)列中所有等待的線程,而不是隨機(jī)選擇一個(gè)。
?? object.wait() 和 object.notify() 必須在對應(yīng)的 synchronized 語句中,需要首先獲得目標(biāo)對象的一個(gè)監(jiān)視器。

wait() 方法只會釋放當(dāng)前對象的鎖,不會釋放所有鎖。
notify()不會立刻立刻釋放sycronized(obj)中的obj鎖,必須要等notify()所在線程執(zhí)行完容synchronized(obj)塊中的所有代碼才會釋放這把鎖。

join() 等待線程結(jié)束,yield() 謙讓
    public volatile static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for(i=0;i<100000;i++);
        });
        thread.start();
        thread.join();
        System.out.println(i);
    }

?? join() 會一直阻塞線程直到目標(biāo)線程執(zhí)行完畢。如果不使用join() 等待 thread,那么得到的 i 很可能是0 或者一個(gè)非常小的數(shù)字。因?yàn)?thread 還沒開始執(zhí)行,i 的值就已經(jīng)被輸出了。
?? yield() 會使當(dāng)前線程讓出CPU。但讓出CPU并不代表當(dāng)前線程不執(zhí)行了。當(dāng)前線程讓出CPU后,會進(jìn)行CPU資源的爭奪,但是否能夠再次被分配,就不一定了。如果你覺得一個(gè)線程不那么重要,或者優(yōu)先級非常低,而且又害怕它會占用太多的CPU資源,那么可以在適當(dāng)?shù)臅r(shí)候調(diào)用 yield() ,給予其他重要線程更多的工作機(jī)會。

yield 不會釋放鎖,需執(zhí)行完畢

ThreadGroup 線程組
    public static void main(String[] args) throws InterruptedException {
        ThreadGroup threadGroup = new ThreadGroup("訂單組");
        Thread t1 = new Thread(threadGroup,() -> {
            String name = Thread.currentThread().getName();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("當(dāng)前線程名稱 :" + name);
        },"下單");
        Thread t2 = new Thread(threadGroup,() -> {
            String name = Thread.currentThread().getName();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("當(dāng)前線程名稱 :" + name);
        },"取消訂單");
        t1.start();
        t2.start();
        System.out.println(threadGroup.activeCount());
        threadGroup.list();
    }

結(jié)果:
2
java.lang.ThreadGroup[name=訂單組,maxpri=10]
    Thread[下單,5,訂單組]
    Thread[取消訂單,5,訂單組]
當(dāng)前線程名稱 :取消訂單
當(dāng)前線程名稱 :下單
setDaemon() 守護(hù)線程

?? 守護(hù)線程是一種特殊的線程,就和他的名字一樣,它是系統(tǒng)的守護(hù)者,在后臺默默的完成一些系統(tǒng)的任務(wù),比如垃圾回收線程、JIT線程就可以理解為守護(hù)線程。與之相對應(yīng)的是用戶線程,用戶線程可以認(rèn)為是系統(tǒng)的工作線程,它會完成這個(gè)程序應(yīng)該要完成的業(yè)務(wù)操作。如果用戶線程全部結(jié)束,這也意味著這個(gè)程序?qū)嶋H上無事可做。守護(hù)線程要守護(hù)的對象已經(jīng)不存在了,那么整個(gè)應(yīng)用程序就自然應(yīng)該結(jié)束。因此,當(dāng)一個(gè)java應(yīng)用內(nèi),只有守護(hù)線程時(shí),java虛擬機(jī)就會自然退出。

        t1.setDaemon(true);
        t2.setDaemon(true);
        t1.start();
        t2.start();

?? 設(shè)置守護(hù)線程必須在start()之前設(shè)置。如果上述例子兩個(gè)都是守護(hù)線程,則不會等到線程里打印結(jié)果,程序直接結(jié)束。用戶線程的話,會等到線程以上兩個(gè)線程執(zhí)行完成,再主線程結(jié)束。

setPriority() 線程優(yōu)先級

?? java中,使用1-10表示線程優(yōu)先級,數(shù)字越大則越優(yōu)先。

三、volatile

? ? 當(dāng)你用 volatile 去申明一個(gè)變量時(shí),就等于告訴了虛擬機(jī),這個(gè)變量極有可能會被某些程序或者線程修改。為了確保這個(gè)變量被修改后,應(yīng)用程序范圍內(nèi)的所有線程都能夠"看到"這個(gè)改動,虛擬機(jī)就必須采用一些特殊的手段,保證這個(gè)變量的可見性、有序性、原子性。
?? volatile 對于保證操作的原子性是有非常大的幫助的。但是需要注意的是,volatile 并不能代替鎖,他也無法保證一些復(fù)合操作的原子性。比如 i++

四、synchronized

? ? synchronized 的作用是實(shí)現(xiàn)線程間的同步。它的工作是對同步的代碼加鎖,使得每一次只能有一個(gè)線程進(jìn)入同步塊,從而保證線程間的安全性。

  • 指定加鎖對象:對給定的對象加鎖,進(jìn)入同步代碼前要獲得給定對象的鎖,且要保證每個(gè)線程里的 synchronized(對象) 的對象參數(shù)是同一個(gè)實(shí)例。
  • 直接作用于實(shí)例方法:相當(dāng)于對當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖。若兩個(gè)線程不是同一個(gè)實(shí)例,則鎖失敗。
  • 直接作用于靜態(tài)方法:相當(dāng)于對當(dāng)前類加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類的鎖。
    public static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        String a = "aaa";
        String b = "aaa";
        Thread t1 = new Thread(() -> {
            for(int j=0;j<100000;j++){
                synchronized (a){
                    add();
                }
            }
        });
        Thread t2 = new Thread(() ->  {
            for(int j=0;j<100000;j++){
                synchronized (b){
                    add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    public static void add(){
        i++;
    }
結(jié)果:
200000
    public static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        String a = "aaa";
        String b = "bbb";
        Thread t1 = new Thread(() -> {
            for(int j=0;j<100000;j++){
                synchronized (a){
                    add();
                }
            }
        });
        Thread t2 = new Thread(() ->  {
            for(int j=0;j<100000;j++){
                synchronized (b){
                    add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    public static void add(){
        i++;
    }
結(jié)果:
112775
    public static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        String a = new String("aaa");
        String b = new String("aaa");
        Thread t1 = new Thread(() -> {
            for(int j=0;j<100000;j++){
                synchronized (a){
                    add();
                }
            }
        });
        Thread t2 = new Thread(() ->  {
            for(int j=0;j<100000;j++){
                synchronized (b){
                    add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
        System.out.println(a);
        System.out.println(b);
    }
    public static void add(){
        i++;
    }
結(jié)果:
106451
aaa
aaa

四、ReentrantLock 重入鎖

??當(dāng)線程請求一個(gè)由其它線程持有的對象鎖時(shí),該線程會阻塞,而當(dāng)線程請求由自己持有的對象鎖時(shí),如果該鎖是重入鎖,請求就會成功,否則阻塞。特別注意,若一個(gè)線程多次獲得鎖,那么在釋放所得時(shí)候,也必須釋放相同次數(shù)。
?? synchronized 也是重入鎖,當(dāng)一個(gè)類里的 A、B、C三個(gè)方法都被加上 synchronized 則A調(diào)用B,B調(diào)用C 會依次正確調(diào)用執(zhí)行,如果 synchronized 不是重入鎖,則這種調(diào)用方式會被 成為死鎖,因?yàn)?A B C 三個(gè)方法持有的是同一個(gè)實(shí)例。

reentrantLock.lockInterruptibly() 中斷處理

?? 在等待鎖的過程中,程序可以根據(jù)需要取消對鎖的申請。lockInterruptibly() 對中斷進(jìn)行響應(yīng)的鎖申請動作,即在等待鎖的過程中,可以響應(yīng)中斷。

reentrantLock.tryLock() 鎖申請等待限時(shí)

?? tryLock() 有兩種方法

  • reentrantLock.tryLock(5, TimeUnit.SECONDS); 如果鎖被其他線程占用則等待5秒,超過5秒沒有得到鎖,就會返回 false,成功得到鎖則返回 true。
  • reentrantLock.tryLock(); 如果鎖被其他線程占用則直接返回 false,得到鎖則直接返回 true
ReentrantLock(true) 公平鎖

?? 在大多情況下鎖都是非公平的。也就是說,線程1 和 線程2 同時(shí)請求了鎖A,那么當(dāng)鎖A可用時(shí),是線程1可以獲得鎖還是線程2可以獲得鎖呢?這是不一定的,系統(tǒng)只是會從這個(gè)鎖的等待隊(duì)列種隨機(jī)挑選一個(gè)。
?? 當(dāng) new ReentrantLock(true) 表示是公平的。但要實(shí)現(xiàn)一個(gè)公平鎖,必然要求系統(tǒng)維護(hù)一個(gè)有序隊(duì)列,因此公平鎖的實(shí)現(xiàn)成本比較高了,如果沒有特別的需要,也不需要使用公平鎖。

reentrantLock.lock(); 獲得鎖,如果鎖被占用則等待;
reentrantLock.tryLock(); 線程嘗試獲取鎖,如果獲取成功,則返回 true,如果獲取失敗(即鎖已被其他線程獲?。?,則返回 false
reentrantLock.tryLock(long timeout,TimeUnit unit); 線程如果在指定等待時(shí)間內(nèi)獲得了鎖,就返回true,否則返回 false
reentrantLock.unlock(); 釋放鎖
reentrantLock.isHeldByCurrentThread() 當(dāng)前線程是否持有該鎖
reentrantLock.lockInterruptibly() 獲得鎖,但有線響應(yīng)中斷
reentrantLock.getHoldCount(); 當(dāng)前線程調(diào)用 lock() 方法的次數(shù)
reentrantLock.getQueueLength(); 當(dāng)前正在等待獲取 Lock 鎖的線程的估計(jì)數(shù)
reentrantLock.getWaitQueueLength(Condition condition); 當(dāng)前正在等待狀態(tài)的線程的估計(jì)數(shù),需要傳入 Condition 對象
reentrantLock.hasWaiters(Condition condition); 查詢是否有線程正在等待與 Lock 鎖有關(guān)的 Condition 條件
reentrantLock.hasQueuedThread(Thread thread); 查詢指定的線程是否正在等待獲取 Lock 鎖
reentrantLock.hasQueuedThreads(); 查詢是否有線程正在等待獲取此鎖定
reentrantLock.isFair(); 判斷當(dāng)前 Lock 鎖是不是公平鎖
reentrantLock.hasQueuedThread(Thread thread); 查詢指定的線程是否正在等待獲取 Lock 鎖
reentrantLock.hasQueuedThread(Thread thread); 查詢指定的線程是否正在等待獲取 Lock 鎖
reentrantLock.hasQueuedThread(Thread thread); 查詢指定的線程是否正在等待獲取 Lock 鎖

Condition 條件
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            try{
                System.out.println("進(jìn)入測試");
                lock.lock();
                System.out.println("獲取鎖");
                condition.await();
                System.out.println("等待結(jié)束");
                Thread.sleep(5000);
                System.out.println("這是對我的一次測試");
            }catch(Exception e){
                lock.unlock();
            }
        }).start();
        Thread.sleep(3000);
        System.out.println("等待三秒結(jié)束");
        lock.lock();
        condition.signal();
        lock.unlock();
    }
結(jié)果:
進(jìn)入測試
獲取鎖
等待三秒結(jié)束
等待結(jié)束
這是對我的一次測試

?? 和Object 里waite() notify() 一樣,當(dāng)線程使用 condition.await()時(shí),要求線程持有相關(guān)的重入鎖,在 condition.await() 調(diào)用后,這個(gè)線程會釋放這把鎖。同理,在 condition.signal() 方法調(diào)用時(shí),也要求線程先獲得相關(guān)鎖,在 condition.signal() 方法調(diào)用后,系統(tǒng)會從當(dāng)前 Condition 對象的等待隊(duì)列中,喚醒一個(gè)線程,一旦線程喚醒,它會重新嘗試獲得與之綁定的重入鎖,一旦成功獲取,就可以繼續(xù)執(zhí)行。因此,在 condition.signal() 方法調(diào)用后,一般需要釋放相關(guān)的鎖,讓給被喚醒的線程,讓它繼續(xù)執(zhí)行。

五、信號量 Semaphore

?? 信號量為多線程寫作提供更為強(qiáng)大的控制方法。廣義上講,信號量是對鎖的擴(kuò)展。無論是內(nèi)部 synchronized 還是 ReentrantLock,一次都只允許一個(gè)線程訪問一個(gè)資源,而信號量卻可以指定多個(gè)線程,同時(shí)訪問摸一個(gè)資源。在構(gòu)造信號量對象時(shí),必須要指定信號量的準(zhǔn)入數(shù),即同時(shí)能申請多少個(gè)許可。

Semaphore semaphore = new Semaphore(3);
Semaphore semaphore1 = new Semaphore(3,true); // 第二個(gè)參數(shù)指定是否公平
  • acquire() 嘗試獲得一個(gè)準(zhǔn)入許可。若無法獲得,則線程會等待,直到有線程釋放一個(gè)許可或者當(dāng)前線程被中斷。
  • acquireUninterruptibly() 和 acquire() 類似,但不會響應(yīng)中斷。
  • tryAcquire() 嘗試獲得一個(gè)許可,成功返回true,失敗返回false。
  • release() 在線程訪問資源結(jié)束后,釋放一個(gè)許可,以使其他等待許可的線程可以進(jìn)行資源訪問。
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        for(int i=0;i<20;i++){
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    Thread.sleep(2000);
                    System.out.println("結(jié)束 -> "+ Thread.currentThread().getId());
                    semaphore.release();
                }catch (Exception e){

                }
            }).start();
        }
    }
結(jié)果:
結(jié)束 -> 13
結(jié)束 -> 14
結(jié)束 -> 12
結(jié)束 -> 15
...

六、ReetrantReadWriteLock 讀寫鎖

?? ReetrantReadWriteLock實(shí)現(xiàn)了ReadWriteLock接口,ReadWriteLock管理一組鎖,一個(gè)是只讀的鎖,一個(gè)是寫鎖。

  • ReetrantReadWriteLock 支持獲取鎖順序,非公平模式(默認(rèn)),公平模式
  • ReetrantReadWriteLock 支持可重入
  • ReetrantReadWriteLock 支持鎖降級,可以從寫鎖降級到讀鎖,但不能從讀鎖升級到寫鎖。

七、CountDownLatch 倒計(jì)時(shí)器

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i = 0;i<5;i++){
            int b = i;
            new Thread(() -> {
                System.out.println("i 已準(zhǔn)備 = "+ b);
                countDownLatch.countDown();
            }).start();
        }
        // 等待裝載完畢
        countDownLatch.await();
        System.out.println("結(jié)束");
    }
結(jié)果:
i 已準(zhǔn)備 = 0
i 已準(zhǔn)備 = 1
i 已準(zhǔn)備 = 2
i 已準(zhǔn)備 = 3
i 已準(zhǔn)備 = 4

?? 為什么沒有輸出 "結(jié)束",是因?yàn)槲覀兘o CountDownLatch 的任務(wù)為10個(gè),但是循環(huán)只有5個(gè)任務(wù),所以在 countDownLatch.await(); 會一直等待裝載夠才會繼續(xù)執(zhí)行,所以阻塞在那里。如果循環(huán)大小比 CountDownLatch 的任務(wù)大,則一旦裝載夠,則會立馬繼續(xù)執(zhí)行。countDownLatch.countDown() 告訴CountDownLatch實(shí)例,已近準(zhǔn)備好一個(gè)。

   public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i = 0;i<12;i++){
            int b = i;
            new Thread(() -> {
                countDownLatch.countDown();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("i 已準(zhǔn)備 = "+ b);
            }).start();
        }
        // 等待裝載完畢
        countDownLatch.await();
        System.out.println("結(jié)束");
    }
結(jié)果:
結(jié)束
i 已準(zhǔn)備 = 0
i 已準(zhǔn)備 = 11
i 已準(zhǔn)備 = 6
i 已準(zhǔn)備 = 1
i 已準(zhǔn)備 = 8
i 已準(zhǔn)備 = 5
i 已準(zhǔn)備 = 9
i 已準(zhǔn)備 = 2
i 已準(zhǔn)備 = 3
i 已準(zhǔn)備 = 10
i 已準(zhǔn)備 = 4
i 已準(zhǔn)備 = 7

八、CyclicBarrier 循環(huán)柵欄

?? 這貨比 CountDownLatch 牛逼一點(diǎn)的就是,我集齊 7 棵龍珠,許了愿,還可以再等集齊 7 棵龍珠,再許愿。只要我集齊 1 顆就必須等 7棵全部集齊,否則一直等待。但召喚神龍也是會有上限的,什么時(shí)候才能徹底結(jié)束呢?就是你在 CyclicBarrier 構(gòu)造函數(shù)傳入 7,一旦集齊 7 棵那就結(jié)束了。

    public static void main(String[] args) throws InterruptedException {
        int parties = 7;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(parties);
        for(int i = 0;i<8;i++){
            int b = i;
            if(b%parties ==0) {
                Thread.sleep(2000);
            }
            new Thread(() -> {
                System.out.println("已集齊 "+ (b%parties +1));
                try {
                    cyclicBarrier.await();
                    if(b%parties ==0) {
                        System.out.println("召喚神龍");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
結(jié)論是:
已集齊 1
已集齊 2
已集齊 3
已集齊 7
已集齊 5
已集齊 6
已集齊 4
召喚神龍
已集齊 1

?? 不要在乎以上結(jié)果的順序,可以看到它已經(jīng)集齊了7棵龍珠,召喚了神龍,但是召喚完了之后又在去集齊,這樣就造成了等待,勢必要再次集齊召喚神龍,且召喚了之后不再去集齊了,才能結(jié)束進(jìn)程。

九、LockSupport 線程阻塞工具

?? LockSupport 是一個(gè)非常方便實(shí)用線程阻塞工具,它可以在線程內(nèi)任意位置讓線程阻塞。和 Thread.suspend() 相比,它彌補(bǔ)了由于 resume() 在前發(fā)生,導(dǎo)致線程無法繼續(xù)執(zhí)行的情況。和Object.wait() 相比,它不需要先獲得某個(gè)對象鎖,也不會拋出 中斷異常,中斷異??梢栽诰€程中獲取 Thread.currentThread().isInterrupted() 來得知。

    public static void main(String[] args) throws InterruptedException {
        String a = "1";
        Thread[] threads = new Thread[2];
        for(int i=0;i<2;i++){
            int b = i;
            threads[i] = new Thread(() -> {
                try {
                    System.out.println("線程啟動 " + b);
//                  提前使用解鎖
                    LockSupport.unpark(Thread.currentThread());
                    LockSupport.park();
                    Thread.sleep(3000);
                    System.out.println("執(zhí)行完畢 " + b);
                } catch (Exception e) {
                    System.out.println("老子被中斷了 " + b);
                    // 這里必須在中斷一次,否則
                    Thread.currentThread().interrupt();
                }
            });
        }
        threads[0].start();
//        LockSupport.unpark(threads[0]);
        threads[1].start();
    }
輸出結(jié)果:
線程啟動 0
執(zhí)行完畢 0
線程啟動 1
執(zhí)行完畢 1

十、無鎖

?? 對于并發(fā)控制而言,鎖是一種悲觀策略。它總是假設(shè)每一次的臨界區(qū)操作會產(chǎn)生沖突,因此,必須對每次操作都小心翼翼。如果有多個(gè)線程同時(shí)訪問臨界區(qū)資源,就寧可犧牲讓線程等待,所以說鎖會阻塞線程執(zhí)行。而無鎖是一種樂觀的策略,它會假設(shè)對資源的訪問沒有沖突的。既然沒有沖突,自然不需要等待,所以所有的線程都可以在不停頓的狀態(tài)下持續(xù)執(zhí)行。那遇到?jīng)_突怎么辦?無鎖的策略使用一種叫做比較交換的技術(shù)(CAS compare and Swap) 來鑒別線程沖突,一旦檢測到?jīng)_突產(chǎn)生,就重試當(dāng)前操作直到?jīng)]有沖突位置。
?? 與鎖相比,使用比較交換(CAS) 會使程序看起來更加復(fù)雜一些。但由于其非阻塞性,它對死鎖問題天生免疫,并且,線程間的相互影響也遠(yuǎn)遠(yuǎn)比基于鎖的方式要小。更為重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統(tǒng)開銷,也沒有線程間頻繁調(diào)度帶來的開銷,因此,它要比基于鎖的方式擁有更優(yōu)越的性能。
?? CAS 的算法過程是這樣的:它包含三個(gè)參數(shù)CAS(V,E,N)。V表示要更新的變量,E表示預(yù)期值,N表示新值。僅當(dāng)V值等于E值時(shí),才會將V的值設(shè)為N,如果V值和E值不同,則說明已經(jīng)有其他線程做了更新,則當(dāng)前線程什么都不做。最后,CAS返回當(dāng)前V的真實(shí)值。CAS操作時(shí)抱著樂觀的態(tài)度進(jìn)行,他總是認(rèn)為自己可以完成操作。當(dāng)多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí),只有一個(gè)會勝出,并且成功更新,其余均會失敗。失敗的線程不會被掛起,僅是被告知失敗,并且允許再次嘗試,當(dāng)然也允許失敗的線程放棄操作?;谶@樣的原理,CAS操作即使沒有鎖,也可以發(fā)現(xiàn)其他線程對當(dāng)前線程的干擾,并進(jìn)行恰當(dāng)?shù)奶幚怼?/p>

AtomicInteger 無鎖的系統(tǒng)安全整數(shù)
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        for(int i=0;i<100;i++){
            new Thread(() -> {
                for(int j=0;j<100;j++){
                    if(atomicInteger.incrementAndGet() == 100){
                        System.out.println("臥槽");
                    }
                }
            }).start();
        }
        Thread.sleep(4000);
        System.out.println(atomicInteger.get());
    }
輸出結(jié)果:
臥槽
10000
  • get() 取得當(dāng)前值
  • set(int newValue) 設(shè)置當(dāng)前值
  • getAndSet(int newValue) 設(shè)置新值,并返回舊值
  • compareAndSet(int expect, int update) 如果當(dāng)前值為expect(期望),則設(shè)置為update(新)
  • getAndIncrement() 當(dāng)前值+1,返回舊值
  • getAndDecrement() 當(dāng)前值-1,返回舊值
  • getAndAdd(int delta) 當(dāng)前值+delta,返回舊值
  • addAndGet(int delta) 當(dāng)前值+delta,返回新值
  • incrementAndGet() 當(dāng)前值+1,返回新值
  • decrementAndGet() 當(dāng)前值-1,返回新值
AtomicReference 無鎖對象引用 和 AtomicStampedReference 帶有時(shí)間戳的對象引用

?? AtomicReference 和 AtomicInteger 非常類似,不同之處就在于 AtomicInteger 是對整數(shù)的封裝,而 AtomicReference 則對應(yīng)普通的對象引用。也就是它可以保證你在修改對象引用時(shí)的線程安全性。

@Data
@Accessors(chain = true)
class Account{
    private Integer amount = 0;
}

public static void main(String[] args) throws InterruptedException {
    Account account = new Account();
    account.setAmount(10);
    AtomicReference<Account> accountAtomicReference = new AtomicReference<Account>();
    accountAtomicReference.set(account);
    // 模擬充值
    for(int i=0;i<3;i++){
        new Thread(() -> {
            Account clientAccount = accountAtomicReference.get();
            System.out.println("充值前查詢越還有 "+ clientAccount.getAmount());
            if(clientAccount.getAmount() < 20){
                if(accountAtomicReference.compareAndSet(clientAccount,clientAccount.setAmount(clientAccount.getAmount() + 20) )){
                    System.out.println("余額小于20元,充值成功,余額:"+ clientAccount.getAmount());
                }
            }
        }).start();
    }
}
輸出結(jié)果:
充值前查詢越還有 10
充值前查詢越還有 10
充值前查詢越還有 10
余額小于20元,充值成功,余額:30

?? 以上列子可以看到,多線程間操作同一個(gè)實(shí)例對象,只會有一個(gè)成功。但這種模式存在一個(gè) ABA 問題,就是,在線程操作前,這個(gè)值很可能被其他線程用去做其他的,導(dǎo)致值被使用后又換回來,當(dāng)前線程一查看值沒問題繼續(xù)使用,造成數(shù)據(jù)被借用,我們還傻傻的不知道,這也是安全性問題。但這種情況就需要看我們的業(yè)務(wù)是否需要解決。
?? 解決辦法呢就是使用 AtomicStampedReference 帶有時(shí)間戳的對象引用,與其說時(shí)間戳,更像是一個(gè)修改標(biāo)記,每次消費(fèi)的時(shí)候,或者充值的時(shí)候我都給修改標(biāo)記+1,一旦和我的原始標(biāo)記不一樣,我就不讓其繼續(xù)充值,只讓其消費(fèi)。

public static void main(String[] args) throws InterruptedException {
    Account account = new Account();
    account.setAmount(10);
    AtomicStampedReference<Account> accountAtomicReference = new AtomicStampedReference<Account>(account,0);
    // 模擬充值
    for(int i=0;i<3;i++){
        int stamp = accountAtomicReference.getStamp();
        new Thread(() -> {
            while (true) {
                Account clientAccount = accountAtomicReference.getReference();
                System.out.println("充值前查詢余額還有 " + clientAccount.getAmount());
                if (clientAccount.getAmount() < 20) {
                    if (accountAtomicReference.compareAndSet(clientAccount, clientAccount.setAmount(clientAccount.getAmount() + 20),stamp,stamp+1)) {
                        System.out.println("余額小于20元,充值成功,余額:" + clientAccount.getAmount());
                    }
                }else {
                    System.out.println("當(dāng)前用戶 充值過不能再充值");
                    break;
                }
            }
        }).start();
    }
    // 模擬消費(fèi)
    for(int i=0;i<3;i++){
        new Thread(() -> {
            while (true) {
                int stamp = accountAtomicReference.getStamp();
                Account clientAccount = accountAtomicReference.getReference();
                System.out.println("消費(fèi)前查詢余額還有 " + clientAccount.getAmount());
                if (clientAccount.getAmount() >= 10) {
                    if (accountAtomicReference.compareAndSet(clientAccount, clientAccount.setAmount(clientAccount.getAmount() - 10),stamp,stamp+1)) {
                        System.out.println("成功消費(fèi)10元,余額還有:" + clientAccount.getAmount());
                    }
                }else{
                    System.out.println("余額不夠");
                    break;
                }
            }
        }).start();
    }
}
輸出結(jié)果:
充值前查詢余額還有 10
余額小于20元,充值成功,余額:30
充值前查詢余額還有 30
當(dāng)前用戶 充值過不能再充值
消費(fèi)前查詢余額還有 30
成功消費(fèi)10元,余額還有:20
消費(fèi)前查詢余額還有 20
成功消費(fèi)10元,余額還有:10
消費(fèi)前查詢余額還有 10
成功消費(fèi)10元,余額還有:0
消費(fèi)前查詢余額還有 0
余額不夠
消費(fèi)前查詢余額還有 0
余額不夠
充值前查詢余額還有 0
充值前查詢余額還有 20
當(dāng)前用戶 充值過不能再充值
消費(fèi)前查詢余額還有 20
成功消費(fèi)10元,余額還有:10
消費(fèi)前查詢余額還有 10
成功消費(fèi)10元,余額還有:0
消費(fèi)前查詢余額還有 0
余額不夠
充值前查詢余額還有 10
充值前查詢余額還有 20
當(dāng)前用戶 充值過不能再充值

?? 如果說在充值的時(shí)候加一個(gè)條件,讓其只能充值1次,如果我們用 AtomicReference 是完全做不到的,因?yàn)樗粫涗洠枰覀冏约喝ヌ砑右粋€(gè)全局變量去維護(hù),但使用 AtomicStampedReference 就可以做到,因?yàn)樗旧砭途S護(hù)了一個(gè)標(biāo)記,而且還幫我們解決了 ABA 問題,如果說值被其他線程冒用,標(biāo)記就會+1,使得和當(dāng)前線程的標(biāo)記不一樣,則保留值退出。

?? 出了AtomicInteger 和 AtomicReference 還有 AtomicReferenceArray AtomicIntegerArray等,具體的API都是差不多的。

本文大部分內(nèi)容均來自 《Java高并發(fā)程序設(shè)計(jì)》--葛一鳴,郭超

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容