線程并發(fā)通解

??前言:本文內(nèi)容較長,可以帶上瓜子,爆米花細(xì)細(xì)閱讀,感興趣的朋友可以收藏關(guān)注。

??在學(xué)習(xí)JAVA的過程中往往都不可避免的會(huì)學(xué)到多線程,同時(shí)往上的內(nèi)容大多也很零散,縱使有很多寫的不錯(cuò)的文章,但是畢竟有種孤軍奮戰(zhàn)的感覺,學(xué)完就忘完,這次就帶大家解決這些問題,讓大家由以一個(gè)更廣闊和更實(shí)用的角度進(jìn)行分析,保障大家學(xué)到用到,不會(huì) 線程從學(xué)完到放棄,力求以簡單的比喻,淺顯的代碼演示給大家對(duì)線程并發(fā)最直觀的感受,同時(shí)感謝前人的鋪墊,互勉。

??為了給大家一個(gè)對(duì)將要學(xué)習(xí)內(nèi)容的直觀感受,先來一張大綱圖,本文就圍繞這些知識(shí)展開。

大綱圖

線程并發(fā)流程圖

??可以看到內(nèi)容還是比較多的,那么我們就一條條往下梳理。

??在介紹多線程之前我們需要先了解一下什么是線程
??線程是我們程序運(yùn)行的最小單元,我們可以用它訪問它所在進(jìn)程的全部資源,當(dāng)存在多個(gè)線程訪問同一進(jìn)程的資源時(shí),出現(xiàn)了多線程,當(dāng)多個(gè)線程同時(shí)訪問同一資源時(shí),出現(xiàn)了線程并發(fā),由此產(chǎn)生線程同步的概念。
??在這里需要簡單的提一下線程的基本api。

??創(chuàng)建線程

??1:繼承Thread類

    public class MyThread extends Thread {
        @Override
        public void run() {
            super.run();
        }
    }
    MyThread myThread = new MyThread();
    myThread.start();

??新建我們的類,繼承Thread,實(shí)現(xiàn)run方法,最后實(shí)例化,調(diào)用start即可。

??2:實(shí)現(xiàn)Runnable類

    public Runnable runnable = new Runnable() {
        @Override
        public void run() {
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();

??和上面的差不多,只是把Runnable當(dāng)參數(shù)傳給Thread的構(gòu)造器即可。

??3:通過Callable和Future創(chuàng)建線程

    public Callable runnable = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "我是執(zhí)行結(jié)果";
        }
    };
   FutureTask futrue = new FutureTask(runnable);
   Thread thread = new Thread(futrue);
   thread.start();

??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化我們的FutureTask,再實(shí)例化我們的線程,運(yùn)行即可。

  • cancel(boolean mayInterruptIfRunning)
    取消當(dāng)前任務(wù)的執(zhí)行。

??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化如果執(zhí)行cancel時(shí)任務(wù)已完成或者之前已進(jìn)行取消操作或者因?yàn)槟承┰虿荒苓M(jìn)行取消操作,那么將返回false
如果執(zhí)行cancel時(shí)線程還未執(zhí)行該任務(wù),那么該任務(wù)將不會(huì)被執(zhí)行,返回ture
??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化如果執(zhí)行cancel時(shí)線程正在處理該任務(wù),且mayInterruptIfRunning為false,那么任務(wù)會(huì)繼續(xù)執(zhí)行到執(zhí)行完成,此時(shí)返回ture
??需要?jiǎng)?chuàng)建一個(gè)類繼承Callable,并且可以控制泛型類型,然后實(shí)例化如果執(zhí)行cancel時(shí)線程正在處理該任務(wù),且mayInterruptIfRunning為ture,那么會(huì)中斷該線程,此時(shí)返回ture

  • isCancelled()
    獲取任務(wù)是否被取消。如果任務(wù)在取消前正常完成,那么返回ture

  • isDone()
    獲取任務(wù)是否已完成如果任務(wù)已完成,返回true。如果任務(wù)時(shí)因中斷,異常等原因被終止,也返回true

  • get()
    獲取任務(wù)執(zhí)行結(jié)果,get()方法會(huì)一直阻塞直到任務(wù)完成。如果任務(wù)被中斷,將拋出InterruptedException

  • get(long timeout, TimeUnit unit)
    在規(guī)定時(shí)間內(nèi)獲取任務(wù)執(zhí)行結(jié)果,如果沒有在規(guī)定時(shí)間內(nèi)完成任務(wù)則拋出TimeoutException。
    ??FutureTask的出現(xiàn)讓我們把任務(wù)和線程給分開了,提供了代碼更高的可控性。

??線程生命周期
  • 新建 :從新建一個(gè)線程對(duì)象到程序start() 這個(gè)線程之間的狀態(tài),都是新建狀態(tài);
  • 就緒 :線程對(duì)象調(diào)用start()方法后,就處于就緒狀態(tài),等到JVM里的線程調(diào)度器的調(diào)度;
  • 運(yùn)行 :就緒狀態(tài)下的線程在獲取CPU資源后就可以執(zhí)行run(),此時(shí)的線程便處于運(yùn)行狀態(tài),運(yùn)行狀態(tài)的線程可變?yōu)榫途w、阻塞及死亡三種狀態(tài)。
  • 等待/阻塞/睡眠 :在一個(gè)線程執(zhí)行了sleep(睡眠)、suspend(掛起)等方法后會(huì)失去所占有的資源,從而進(jìn)入阻塞狀態(tài),在睡眠結(jié)束后可重新進(jìn)入就緒狀態(tài)。
  • 終止 :run()方法完成后或發(fā)生其他終止條件時(shí)就會(huì)切換到終止?fàn)顟B(tài)。
??線程操作 sleep/yield/join

??sleep
??當(dāng)我們需要一個(gè)線程延遲執(zhí)行的時(shí)候就可以調(diào)用這個(gè)方法,當(dāng)然也會(huì)拋出異常,我們catch一下即可。

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();

??單位是毫秒,我們可以乘以1000再操作,當(dāng)我們調(diào)用Sleep后,線程會(huì)進(jìn)入阻塞狀態(tài),給其他線程執(zhí)行的機(jī)會(huì),但是不會(huì)釋放當(dāng)前所持的對(duì)象鎖,即不會(huì)釋放同步資源鎖,當(dāng)sleep()休眠時(shí)間滿后,該線程不一定會(huì)立即執(zhí)行,這是因?yàn)槠渌€程可能正在運(yùn)行而起沒有被調(diào)度為放棄執(zhí)行,除非此線程具有更高的優(yōu)先級(jí)。

??yield
??該方法和sleep方法類似,也是Thread類提供的一個(gè)靜態(tài)方法,可以讓正在執(zhí)行的線程暫停,但是不會(huì)進(jìn)入阻塞狀態(tài),而是直接進(jìn)入就緒狀態(tài)。相當(dāng)于只是將當(dāng)前線程暫停一下,然后重新進(jìn)入就緒的線程池中,讓線程調(diào)度器重新調(diào)度一次。也會(huì)出現(xiàn)某個(gè)線程調(diào)用yield方法后暫停,但之后調(diào)度器又將其調(diào)度出來重新進(jìn)入到運(yùn)行狀態(tài)。

??join
??當(dāng)B線程執(zhí)行到了A線程的.join()方法時(shí),B線程就會(huì)等待,等A線程都執(zhí)行完畢,B線程才會(huì)執(zhí)行。
??所以join可以用來臨時(shí)加入線程執(zhí)行。

        final Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d("ThreadInfo", "執(zhí)行了A線程");
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    threadA.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d("ThreadInfo", "執(zhí)行了B線程");
            }
        });
        threadB.start();
        threadA.start();

??查看下日志:

    ThreadInfo: 執(zhí)行了A線程
    ThreadInfo: 執(zhí)行了B線程

??我們同時(shí)執(zhí)行了A,B鮮橙吧,但是由于我們?cè)贐線程中添加了threadA.join();,所以直到一秒后A執(zhí)行結(jié)束,才輪到B執(zhí)行,可以想象為排隊(duì)買票被人強(qiáng)插隊(duì)的情況。

??線程運(yùn)行優(yōu)先級(jí)
    Thread t = new Thread();
    t.priority(100);  //設(shè)置優(yōu)先級(jí)

??級(jí)別越高,獲取執(zhí)行的幾率越高,當(dāng)然并不一定是,只是增大幾率,傳入?yún)?shù)是int,可自行判斷。

一 : Java內(nèi)存模型(JMM)

??Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣底層細(xì)節(jié)。此處的變量與Java編程時(shí)所說的變量不一樣,指包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量與方法參數(shù),后者是線程私有的,不會(huì)被共享。

??來,我們看一張線程從內(nèi)存中存取變量的圖。


線程工作圖(圖1-1)

??從這里我們看到當(dāng)線程工作的狀態(tài),我們需要知道是,一個(gè)變量的產(chǎn)生一定是從主內(nèi)存中誕生的,當(dāng)我們創(chuàng)建一個(gè)線程后,會(huì)從主內(nèi)存中復(fù)制這個(gè)線程需要用到的數(shù)據(jù)副本。
??如上圖,當(dāng)我們線程A讀取了一個(gè)參數(shù)時(shí)并修改時(shí),會(huì)先把修改的數(shù)據(jù)放到本地內(nèi)存A中,然后同步到主內(nèi)存中
??然后通知我們的線程B刷新數(shù)據(jù),那么當(dāng)我們的線程B就會(huì)將本地內(nèi)存B中的數(shù)據(jù)從主內(nèi)存中重新刷新,以便獲取到最新的值。
??而我們看到JMM就在本地內(nèi)存和主內(nèi)存之間起作用,控制數(shù)據(jù)的存取規(guī)則。
??可能有的同學(xué)會(huì)好奇,為什么要有個(gè)本地內(nèi)存A,B這個(gè)東西,多麻煩啊,直接對(duì)主內(nèi)存進(jìn)行操作多好,省了多少事。
??我們看一下面這個(gè)圖


圖1-2

??由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算能力之間有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(cache)來作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再從緩存同步回內(nèi)存之中沒這樣處理器就無需等待緩慢的內(nèi)存讀寫了。
??基于高速緩存的存儲(chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是引入了一個(gè)新的問題:緩存一致性(Cache Coherence)。在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而他們又共享同一主存,如上圖所示:多個(gè)處理器運(yùn)算任務(wù)都涉及同一塊主存,需要一種協(xié)議可以保障數(shù)據(jù)的一致性,這類協(xié)議有MSI、MESI、MOSI及Dragon Protocol等。

??好了,不扯遠(yuǎn)了,通過處理器操作內(nèi)存的這個(gè)過程我們可以類比到我們的線程操作內(nèi)存的過程,總體是為了提高運(yùn)行效率。

??那再回到我們上面的JMM,這里我們還需要提出幾個(gè)概念,方便講解JMM。

  • 重排序

??在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分三種類型:
??1.編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。線程A線程B本地內(nèi)存Ax=1本地內(nèi)存Bx=1主內(nèi)存x=1步驟1步驟2線程之間通信:線程A向B發(fā)送消息
??2.指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
??3.內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
??從java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷上面三種重排序:上述的1屬于編譯器重排序,2和3屬于處理器重排序。
??來個(gè)圖理解一下:


圖1-3
  • 內(nèi)存可見性

??要說這個(gè)問題的話我們可以回到上面看下圖一,就知道了,線程A,B各自從主內(nèi)存中復(fù)制出了同一個(gè)變量a,但是線程A,B中的副本的操作卻是不互通的,比如線程A改了a的值,然后準(zhǔn)備往主內(nèi)存中刷新,但是此時(shí)B線程提前了從主內(nèi)存中獲取了a的值,那么獲取a的值就是舊數(shù)據(jù),因?yàn)榫€程A修改的值還來得及刷進(jìn)去。
??那么線程A中的操作對(duì)線程B是不是就是不透明的,也就是不可見,要是B能夠知道A改了之后再去主內(nèi)存取一次值,那A的操作對(duì)B就是可見的。

  • 順序一致性模型

??首先這是一個(gè)理想中的模型,即不存在,但是卻為我們?cè)O(shè)置內(nèi)存模型提供了參考依據(jù)。
??比如,看下面的代碼:

int a = 3;
int b = 4;
int c = 5;

??很簡單的賦值,當(dāng)我們寫下這串代碼的時(shí)候,我們默認(rèn)執(zhí)行就是a - > b - > c。但事實(shí)上編譯器不這么認(rèn)為,再編譯器看來a,b,c三個(gè)數(shù)據(jù)的賦值之間是沒有任何關(guān)系的,什么意思呢,就是a有沒有被賦值為3完全不影響b能不能被賦值為4,那么同理c也是。那秉承著高效率的原則,處理器完全可以同時(shí)執(zhí)行這三個(gè)變量的賦值工作,是吧,不然現(xiàn)在處理器這么牛不用放著干嘛呢。
??那再看下下面的代碼:

int a = 3;
int b = 4;
int c = a * b;

??我們看到c的值是由a和b的乘積得到的,但是a和b的執(zhí)行順序卻是可以不分前后,但是c的執(zhí)行順序一定要在a和b之后。c對(duì)于a,b的關(guān)系我們可以稱之為數(shù)據(jù)依賴性,就是沒有a和b的賦值,c是無法產(chǎn)生的。
??順序一致性模型就是我們編譯器和處理器參考處理數(shù)據(jù)的依據(jù)。
??說下順序一致性模型的特點(diǎn):

1 : 一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行。
2 : 所有線程(不管程序是否同步)都只能看到一個(gè)單一的操作執(zhí)行順序。在順序一致性內(nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見。

??第一點(diǎn)我們上面也說了,我們通過一張圖看下第二點(diǎn)


圖1-4

??可以看到在同一時(shí)間,只能由一個(gè)線程對(duì)主內(nèi)存進(jìn)行操作。
??假如現(xiàn)在有線程A,B分別執(zhí)行了A1-A2-A3和B1-B2-B3的操作,并且這倆個(gè)線程被執(zhí)行了同步操作,那么我們可以看到的執(zhí)行順序可能是下面這樣:


圖1-5

??可以看到不僅整個(gè)執(zhí)行順序看起來是有序的,單個(gè)線程里面執(zhí)行順序也是有序的。
??假如A,B線程沒有被執(zhí)行同步操作,可以看到可能如下:


圖1-6

??可以看到雖然整體的執(zhí)行是無序的,但就A,B單個(gè)來說,執(zhí)行仍然是有序的,從而為內(nèi)存可見性提供了強(qiáng)力的保證,因?yàn)樵陧樞驁?zhí)行模型下這倆種情況的執(zhí)行順序?qū)λ械木€程來說都是可見的。

??那么實(shí)際中我們的JMM就參考順序一致性模型進(jìn)行設(shè)置

??JMM在同步程序下執(zhí)行

??參考下面的代碼:

class SynchronizedExample {

     int a = 0;
     boolean flag = false;

     public synchronized void reader() {
          flag = true;
     }

     public synchronized void writer() 
     {
          if (flag) {
               int i = a;
          }
      }

}

??上面示例代碼中,假設(shè)A線程執(zhí)行writer()方法后,B線程執(zhí)行reader()方法。這是一個(gè)正確同步的多線程程序。根據(jù)JMM規(guī)范,該程序的執(zhí)行結(jié)果將與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。下面是該程序在兩個(gè)內(nèi)存模型中的執(zhí)行時(shí)序?qū)Ρ葓D:


圖1-7

??參考右邊的順序模型,雖然JMM的執(zhí)行我們發(fā)現(xiàn)臨界區(qū)中的順序被重排了,但是整體的執(zhí)行順序是有序的,這是因?yàn)榕R界區(qū)中的順序不會(huì)影響到外面的執(zhí)行,所以執(zhí)行順序是可以調(diào)整的。
??雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器的互斥執(zhí)行的特性,這里的線程B根本無法“觀察”到線程A在臨界區(qū)內(nèi)的重排序。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結(jié)果。
??從這里我們可以看到JMM在具體實(shí)現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下,盡可能的為編譯器和處理器的優(yōu)化打開方便之門。

??JMM在非同步程序下執(zhí)行

??對(duì)于未同步或未正確同步的多線程程序,JMM只提供最小安全性:線程執(zhí)行時(shí)讀取到的值,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(0,null,false),JMM保證線程讀操作讀取到的值不會(huì)無中生有(out of thin air)的冒出來。為了實(shí)現(xiàn)最小安全性,JVM在堆上分配對(duì)象時(shí),首先會(huì)清零內(nèi)存空間,然后才會(huì)在上面分配對(duì)象(JVM內(nèi)部會(huì)同步這兩個(gè)操作)。因此,在已清零的內(nèi)存空間(pre-zeroed memory)分配對(duì)象時(shí),域的默認(rèn)初始化已經(jīng)完成了。
??JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致。因?yàn)槿绻胍WC執(zhí)行結(jié)果一致,JMM需要禁止大量的處理器和編譯器的優(yōu)化,這對(duì)程序的執(zhí)行性能會(huì)產(chǎn)生很大的影響。而且未同步程序在順序一致性模型中執(zhí)行時(shí),整體是無序的,其執(zhí)行結(jié)果往往無法預(yù)知。保證未同步程序在這兩個(gè)模型中的執(zhí)行結(jié)果一致沒什么意義。未同步程序在JMM中的執(zhí)行時(shí),整體上是無序的,其執(zhí)行結(jié)果無法預(yù)知。未同步程序在兩個(gè)模型中的執(zhí)行特性有下面幾個(gè)差異:
??1.順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行,而JMM不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序),這一點(diǎn)前面圖1-7已經(jīng)闡述,這里就不再贅述。
??2.順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序,而JMM不保證所有線程能看到一致的操作執(zhí)行順序。這一點(diǎn)前面圖1-4,1-5,1-6部分已經(jīng)闡述,這里就不再贅述。
??3.JMM不保證對(duì)64位的long型和double型變量的讀/寫操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫操作都具有原子性。
??第3個(gè)差異與處理器總線的工作機(jī)制密切相關(guān)。在計(jì)算機(jī)中,數(shù)據(jù)通過總線在處理器和內(nèi)存之間傳遞。每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(wù)。

??工作流程參考下圖:


圖1-8

??需要注意的是在一個(gè)處理器執(zhí)行總線事務(wù)期間,總線會(huì)禁止其它所有的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫。

??總線的這些工作機(jī)制可以把所有處理器對(duì)內(nèi)存的訪問以串行化的方式來執(zhí)行;在任意時(shí)間點(diǎn),最多只能有一個(gè)處理器能訪問內(nèi)存。這個(gè)特性確保了單個(gè)總線事務(wù)之中的內(nèi)存讀/寫操作具有原子性。
??在一些32位的處理器上,如果要求對(duì)64位數(shù)據(jù)的寫操作具有原子性,會(huì)有比較大的開銷。為了照顧這種處理器,java語言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long型變量和double型變量的寫具有原子性。當(dāng)JVM在這種處理器上運(yùn)行時(shí),會(huì)把一個(gè)64位long/ double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行。這兩個(gè)32位的寫操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對(duì)這個(gè)64位變量的寫將不具有原子性。

??當(dāng)單個(gè)內(nèi)存操作不具有原子性,將可能會(huì)產(chǎn)生意想不到后果。請(qǐng)看下面示意圖:


圖1-9

??如上圖所示,假設(shè)處理器A寫一個(gè)long型變量,同時(shí)處理器B要讀這個(gè)long型變量。處理器A中64位的寫操作被拆分為兩個(gè)32位的寫操作,且這兩個(gè)32位的寫操作被分配到不同的寫事務(wù)中執(zhí)行。同時(shí)處理器B中64位的讀操作被分配到單個(gè)的讀事務(wù)中執(zhí)行。當(dāng)處理器A和B按上圖的時(shí)序來執(zhí)行時(shí),處理器B將看到僅僅被處理器A“寫了一半“的無效值。
??注意,在JSR -133(即)之前的舊內(nèi)存模型中,一個(gè)64位long/ double型變量的讀/寫操作可以被拆分為兩個(gè)32位的讀/寫操作來執(zhí)行。從JSR -133內(nèi)存模型開始(即從JDK5開始),僅僅只允許把一個(gè)64位long/ double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行,任意的讀操作在JSR -133中都必須具有原子性(即任意讀操作必須要在單個(gè)讀事務(wù)中執(zhí)行)。
??這里提一下,從JDK5開始,java使用新的JSR -133內(nèi)存模型(本文除非特別說明,針對(duì)的都是JSR-133內(nèi)存模型)。JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見,那么這兩個(gè)操作之間必須要存在happens-before關(guān)系。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間。

??需要我們從happens-before中了解的可以分為以下幾點(diǎn):

??程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before 于該線程中的任意后續(xù)操作。
??監(jiān)視器鎖規(guī)則:對(duì)一個(gè)監(jiān)視器的解鎖,happens-before 于隨后對(duì)這個(gè)監(jiān)視器的加鎖。
??volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。

二 : 鎖

??1: ReentrantLock
??2: synchronized
??3: volatile

??通過上面的學(xué)習(xí),我們發(fā)現(xiàn)在多線程并發(fā)的情況下如果希望程序按照我們順序進(jìn)行運(yùn)行,那就需要進(jìn)行同步操作,那實(shí)現(xiàn)多線程之間的同步操作利用到的就是鎖,這里的鎖我們就可以比如我們家用的鎖,比如現(xiàn)在有一個(gè)房子只能住一個(gè)人,一把鎖,但是有好幾個(gè)人有房子的鑰匙,那么當(dāng)這幾個(gè)人去使用房子的時(shí)候,每次就只能一個(gè)人進(jìn)去,不懂鎖概念的我們就可以這樣理解鎖,但是鎖的種類可能有很多種,下面一一介紹。

??ReentrantLock

??從字面意思的翻譯是 : 可重入鎖。
常用方法:

lock()
unlock()
tryLock()
lockInterruptibly
newCondition()
lock(),unlock()

??首先我們來看一個(gè)例子,比如現(xiàn)在一個(gè)商店買東西,還有100件貨物,但是可能有很多的顧客來搶著買,我們用代碼模擬一下,看看:

public class Shop {

    private int max = 1000;

    public void sell(){
        max--;
        Log.d("current_tickets", max + "");
    }
}

??首先建一個(gè)商店,里面有1000張票,然后再可以賣票,當(dāng)然,每賣一張票,總數(shù)就會(huì)減少,然后再模擬下很多人搶票。

final Shop shop = new Shop();
for (int i = 0; i < 1000; i++) {
     new Thread() {
          @Override
          public void run() {
                super.run();
                shop.sell();
                }
           }.start();
}

??這個(gè)場(chǎng)面太混亂了啊,1千人同時(shí)搶1000張票,那我們運(yùn)行程序看下日志:

D/current_tickets: 999
D/current_tickets: 998
...
D/current_tickets: 1
D/current_tickets: 0

??貌似沒啥問題哈,1000張正好全賣給了1000個(gè)人,票一個(gè)不剩,真的是這樣么?
??我們仔細(xì)翻看日志可能會(huì)發(fā)現(xiàn)有這種情況出現(xiàn),當(dāng)然并不是每次都會(huì)出現(xiàn),我們可以多次運(yùn)行,可以看到不同的運(yùn)行結(jié)果,如下:

...
D/current_tickets: 456
D/current_tickets: 456
...
D/current_tickets: 200
D/current_tickets: 200
...

??或者

...
D/current_tickets: 3
D/current_tickets: 2
D/current_tickets: 1
D/current_tickets: 1
D/current_tickets: 0
D/current_tickets: -1
...

??很顯然,在實(shí)際賣票的過程中這種事是不允許發(fā)生的,無論是錯(cuò)誤告訴顧客還有多少票或者票都沒了怎么還能賣。
??這就是一個(gè)線程并發(fā)的場(chǎng)景,一個(gè)人代表一個(gè)線程,1000個(gè)人代表1000個(gè)線程,那么我們要做到如何能夠有秩序的賣票就可以用ReentrantLock實(shí)現(xiàn),這里我們只需要改一下商店的代碼就可以。

public class Shop {

    private int max = 1000;
    private Lock lock = new ReentrantLock();

    public void sell(){
        try {
            lock.lock();
            max--;
            Log.d("current_tickets", max + "");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

??可以看到這里用到了lock(),和unlock()倆個(gè)方法,一個(gè)是上鎖,一個(gè)是解鎖,我們?cè)俅芜\(yùn)行看下結(jié)果,可以發(fā)現(xiàn),無論運(yùn)行多少次,結(jié)果始終是正確的,這里L(fēng)ock就起到一個(gè)很重要的作用,當(dāng)有一個(gè)顧客去買票的時(shí)候,買票廳會(huì)禁止下一個(gè)顧客緊跟著買票,告訴已經(jīng)有人在買了,麻煩你等一下,通過這個(gè)方法,就能夠讓售票廳有秩序的賣票,不會(huì)出現(xiàn)烏龍了,當(dāng)然我們需要注意的是lock和unlock必須要成對(duì)出現(xiàn),如果只lock沒有unlock,那么后面的都買不了票了,不管你售票廳還剩多少票。

可重入

??那上面介紹的可重入是啥意思呢,接著舉例子,現(xiàn)在有一戶人家爸爸正在買票,本來只打算買一個(gè)票,正在買票的過程中,但是此時(shí)突然兒子跑過來,跟爸爸說他也要一張票,那么后面的人就沒轍了啊,這是一家人,插隊(duì)也沒辦法,假如此時(shí)老婆也過來,那仍然可以擠進(jìn)來,誰叫這三個(gè)是一家人呢,這里就體現(xiàn)的是可重入,在線程中的體現(xiàn)就是同一個(gè)線程可以重復(fù)訪問一個(gè)鎖,每次訪問給鎖一個(gè)計(jì)數(shù)器加1,每次解鎖再減1,直到計(jì)數(shù)器等于0,后面的人家才可以買票,看看我們的代碼如何實(shí)現(xiàn)。
??仍然先建我們的售票廳

//售票廳代碼
public class Shop {

    private int max = 1000;
    private Lock lock = new ReentrantLock();

    public void sell(){
        try {
            lock.lock();
            max--;
            Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
        }
    }
}

??這里的Thread.currentThread().getName()是獲取線程名稱的方法,發(fā)現(xiàn)了沒有,finally里面沒有添加unlock方法,那我們?cè)倏聪氯藗內(nèi)绾钨I票:

   //買票代碼
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Thread personOne = new Thread(new Sell(), "personOne");
        Thread personTwo = new Thread(new Sell(), "personTwo");
        personOne.start();
        personTwo.start();
    }

  private class Sell implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 500; i++) {
                shop.sell();
            }
        }

    }

??這里我們只有倆戶人家買票,但是一個(gè)人一次性就買了500張(是票販子無疑了),當(dāng)我們運(yùn)行的時(shí)候,發(fā)現(xiàn)運(yùn)行結(jié)果如下:

current_tickets: personOne : 999
...
current_tickets: personOne : 500

??又或者是:

current_tickets: personTwo : 999
...
current_tickets: personTwo : 500

??反正每次運(yùn)行后發(fā)現(xiàn)只有一戶人家可以買票,另一戶人家插都插不上,這就是因?yàn)槲覀兩厦尜I票的地方?jīng)]有調(diào)用unlock,那么當(dāng)開始賣票的時(shí)候,誰先搶到位置,誰開始買票,并且除了當(dāng)前在買票的,其余的都只能干瞪眼了。那么如何讓第一家賣完,接著給別人買呢,我們修改上面的售票廳代碼:

public class Shop {
    //售票廳代碼
    private int max = 1000;
    private Lock lock = new ReentrantLock();

    public void sell() {
        try {
            lock.lock();
            max--;
            Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
            if (max == 500) {
                for (int i = 0; i < 500; i++) {
                    lock.unlock();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
    }
}

??我們加了一個(gè)判斷,當(dāng)買了500張票的時(shí)候,再解鎖500次,那么下一個(gè)人就可以接著買票了,當(dāng)然,這里只是給大家展示這個(gè)可重入的概念,切勿模仿這種low的寫法。

trylock(),tryLock(long time, TimeUnit unit)

??倆個(gè)方法的作用都是嘗試獲取鎖,并且都會(huì)返回一個(gè)boolean值,true表示成功獲取到了鎖,false表示沒有。
??tryLock(long time, TimeUnit unit)和trylock()的區(qū)別在于tryLock(long time, TimeUnit unit)會(huì)等待一段指定的時(shí)候,如果超過這個(gè)時(shí)間內(nèi)沒有獲取到鎖,會(huì)返回false,如果在指定時(shí)間內(nèi)獲取成功了,則返回true。
??那么再回到上面的賣票案例中,假如現(xiàn)在只有倆個(gè)人在買票,代碼演示如下,首先建立售票處:

//售票處
public class Shop {

    private int max = 1000;
    private Lock lock = new ReentrantLock();

    public void sell() {
        if (lock.tryLock()) {
            max--;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
            lock.unlock();
        } else {
            Log.d("current_tickets", Thread.currentThread().getName() + " :  不等了,我走了");
        }
    }
}

??再看下如何如何買票的:

//買票
Shop shop = new Shop();
Thread threadOne = new Thread(new Buy(), "threadOne");
Thread threadTwo = new Thread(new Buy(), "threadTwo");
threadOne.start();
threadTwo.start();

private class Buy implements Runnable {

      @Override
      public void run() {
          shop.sell();
      }
}

??運(yùn)行查看結(jié)果:

threadTwo : 999
threadOne :  不等了,我走了

??多次運(yùn)行我們可以發(fā)現(xiàn)每次只能又一個(gè)線程可以獲取到鎖,另一個(gè)直接就走 ‘不等了,我走了’ 邏輯,這是因?yàn)橄饶玫芥i的線程睡眠了一秒鐘,但是別人不能等你啊,所以直接就放棄了去獲取鎖。
??那可能有的人比較有耐心啊,那我們就可以使用tryLock(long time, TimeUnit unit)方法,修改上面的售票處代碼:

public class Shop {

    private int max = 1000;
    private Lock lock = new ReentrantLock();

    public void sell() {
        try {
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                max--;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
                lock.unlock();
            } else {
                Log.d("current_tickets", Thread.currentThread().getName() + " :  不等了,我走了");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

??現(xiàn)在就有這樣一個(gè)人,他愿意等,那查看運(yùn)行結(jié)果我們發(fā)現(xiàn)當(dāng)一個(gè)線程獲取鎖后,1秒后解鎖,那另一個(gè)鎖在1秒后就會(huì)接著去獲取鎖,因?yàn)樗敢獾?秒,最終倆個(gè)線程都能成功執(zhí)行,完美。

lockInterruptibly

??我們可以把lockInterruptibly當(dāng)lock或者trylock來看,但是卻帶有了另一種屬性,就是可以響應(yīng)線程當(dāng)前的狀態(tài)有沒有被打斷,比如當(dāng)前線程處于sleep或者wait狀態(tài),調(diào)用線程的interrupt()方法,該線程就會(huì)直接拋出lockInterruptibly異常,這樣線程就不會(huì)一直在那等著,如果線程已經(jīng)在運(yùn)行中調(diào)用這個(gè)interrupt()方法會(huì)怎么樣呢,線程不會(huì)被中斷,但是線程的狀態(tài)會(huì)發(fā)現(xiàn)改變,如果我們調(diào)用線程的isInterrupted()方法,就會(huì)返回true或false告訴我們現(xiàn)在線程的狀態(tài)有沒有被打斷,但是線程不會(huì)拋出異常和打斷運(yùn)行。
??那我們看下如何在買票案例中實(shí)現(xiàn),仍然實(shí)現(xiàn)售票廳,如下:

//售票廳
public class Shop {

    private int max = 1000;
    private Lock lock = new ReentrantLock();

    public void sell() {
        try {
            lock.lockInterruptibly();
            max--;
            Log.d("current_tickets", Thread.currentThread().getName() + " : " + max);
            Thread.sleep(3000);
            lock.unlock();
        } catch (InterruptedException e) {
            Log.d("current_tickets", Thread.currentThread().getName() + " :  不等了,我走了");
            e.printStackTrace();
        }
    }
}

??然后在看下如何買票:

//買票
Shop shop = new Shop();
Thread threadOne = new Thread(new Buy(), "threadOne");
Thread threadTwo = new Thread(new Buy(), "threadTwo");
threadOne.start();
threadTwo.start();

private class Buy implements Runnable {

    @Override
    public void run() {
        shop.sell();
    }
}

??我們執(zhí)行代碼開始運(yùn)行:

current_tickets: threadOne : 999
過幾秒后...
current_tickets: threadTwo : 998

??發(fā)現(xiàn)作用其實(shí)是跟lock一樣的是吧,當(dāng)有一個(gè)線程獲取了對(duì)象的鎖,另外的線程就等著直到鎖被釋放為止。
??但是我們可以手動(dòng)打斷這種狀態(tài),修改買票的代碼,我們只需要threadTwo.start();后面加上threadOne.interrupt()或threadTwo.interrupt()就可以了,記住是或,不是和,運(yùn)行一下就可以看到threadTwo始終走的是 ‘不等了,我走了’ 邏輯,這就是因?yàn)槲覀兇驍嗔藅hreadTwo的等待過程,讓threadTwo直接拋出了異常,于是就不再一直阻塞在那里了。

Condition()

??Condition是在java 1.5中才出現(xiàn)的,它用來替代傳統(tǒng)的Object的wait()、notify()實(shí)現(xiàn)線程間的協(xié)作,相比使用Object的wait()、notify(),使用Condition的await()、signal()這種方式實(shí)現(xiàn)線程間協(xié)作更加安全和高效。因此通常來說比較推薦使用Condition,阻塞隊(duì)列實(shí)際上是使用了Condition來模擬線程間協(xié)作。
??Condition是個(gè)接口,基本的方法就是await()和signal()方,生成一個(gè)Condition的基本代碼是lock.newCondition()調(diào)用Condition的await()和signal()方法,都必須在lock保護(hù)之內(nèi),就是說必須在lock.lock()和lock.unlock之間才可以使用,一個(gè)lock可以生成多個(gè)Condition,從而實(shí)現(xiàn)對(duì)鎖更細(xì)的粒度控制。
??Conditon中的await()對(duì)應(yīng)Object的wait();
??Condition中的signal()對(duì)應(yīng)Object的notify();
??Condition中的signalAll()對(duì)應(yīng)Object的notifyAll()
??Condition通常用于設(shè)計(jì)阻塞隊(duì)列,比如我們給了一個(gè)固定的容器,往里面添加數(shù)據(jù)的時(shí)候發(fā)現(xiàn)容量不夠了,就可以采取阻塞的方式,等到數(shù)據(jù)被取走的時(shí)候,騰出了空間,就可以接著添加數(shù)據(jù)了,看看我們用Condition如何實(shí)現(xiàn)這一效果。

//實(shí)現(xiàn)一個(gè)我們需要的阻塞隊(duì)列工具,包含添加和取數(shù)據(jù)功能
public class BlockingQueue {

    private final int MAX = 5;

    private Lock lock = new ReentrantLock();
    private Condition putCondition;
    private Condition getCondition;

    private List<String> list = new ArrayList<>();

    public BlockingQueue() {
        putCondition = lock.newCondition();
        getCondition = lock.newCondition();
    }

    public void add(String data) {
        lock.lock();
        try {
            while (list.size() == MAX) {
                try {
                    putCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(data);
            Log.d("BlockingQueue", "add : " + data + " : " + list.size());
            getCondition.signal();
        } finally {
            lock.unlock();
        }
    }

    public String get() {
        String result;
        lock.lock();
        try {
            while (list.size() == 0) {
                try {
                    getCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            result = list.get(0);
            list.remove(0);
            putCondition.signal();
            Log.d("BlockingQueue", "get : " + result + " : " + list.size());
        } finally {
            lock.unlock();
        }
        return result;
    }

}

??可以看到我們?cè)O(shè)置了一個(gè)最大容量為5的集合。
??當(dāng)我們不斷調(diào)用add方法時(shí),我們會(huì)檢查當(dāng)前容量是否已經(jīng)滿了,如果沒滿,則可以繼續(xù)添加,如果滿了,則執(zhí)行putCondition.await();add方法也將被阻塞,數(shù)據(jù)即停止往集合中添加,但是當(dāng)我們調(diào)用get方法是,我們的集合大小則會(huì)減小,并且會(huì)調(diào)用putCondition.signal();則會(huì)通知我們剛才被阻塞的add方法再次執(zhí)行。
??當(dāng)我們調(diào)用get方法時(shí),會(huì)檢查數(shù)據(jù)集合是否含有數(shù)據(jù),如果沒有,則執(zhí)行g(shù)etCondition.await();get方法開始阻塞,直到add方法被調(diào)用,添加了數(shù)據(jù)后,執(zhí)行g(shù)etCondition.signal();從而喚醒get方法。
??然后我們?cè)匍_兩個(gè)線程,一個(gè)用于添加數(shù)據(jù),一個(gè)用于取出數(shù)據(jù),代碼如下:

     new Thread() {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < 10; i++) {
                    blockingQueue.add(i + "");
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < 5; i++) {
                    blockingQueue.get();
                }
            }
        }.start();

??我們?cè)诳刂婆_(tái)看下輸出結(jié)果

BlockingQueue: add : 0 : 1
BlockingQueue: add : 1 : 2
BlockingQueue: get : 0 : 1
BlockingQueue: get : 1 : 0
BlockingQueue: add : 2 : 1
BlockingQueue: add : 3 : 2
BlockingQueue: add : 4 : 3
BlockingQueue: add : 5 : 4
BlockingQueue: add : 6 : 5
BlockingQueue: get : 2 : 4
BlockingQueue: get : 3 : 3
BlockingQueue: get : 4 : 2
BlockingQueue: add : 7 : 3
BlockingQueue: add : 8 : 4
BlockingQueue: add : 9 : 5

??可以看見add和get方法交替執(zhí)行,但是集合的大小確始終控制在5以內(nèi),這就是因?yàn)槲覀兩厦孀龅淖枞刂?,那如果我們注釋掉第二個(gè)線程,執(zhí)行一下,看一下輸出結(jié)果:

BlockingQueue: add : 0 : 1
BlockingQueue: add : 1 : 2
BlockingQueue: add : 2 : 3
BlockingQueue: add : 3 : 4
BlockingQueue: add : 4 : 5

??就會(huì)更加直觀的感受的add方法被調(diào)用5次后被阻塞。
??假如我現(xiàn)在希望集合滿的時(shí)候,add方法和get方法交替執(zhí)行,如何實(shí)現(xiàn)呢,那我們可以修改BlockingQueue的代碼,如下:

public class BlockingQueue {

    private final int MAX = 5;

    private Lock lock = new ReentrantLock();
    private Condition putCondition;
    private Condition getCondition;

    private List<String> list = new ArrayList<>();

    public BlockingQueue() {
        putCondition = lock.newCondition();
        getCondition = lock.newCondition();
    }

    public void add(String data) {
        lock.lock();
        try {
            while (list.size() == MAX) {
                try {
                    getCondition.signal();
                    putCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(data);
            Log.d("BlockingQueue", "add : " + data + " : " + list.size());
        } finally {
            lock.unlock();
        }
    }

    public String get() {
        String result;
        lock.lock();
        try {
            while (list.size() == 0 || list.size() == MAX - 1) {
                try {
                    getCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            result = list.get(0);
            list.remove(0);
            putCondition.signal();
            Log.d("BlockingQueue", "get : " + result + " : " + list.size());
        } finally {
            lock.unlock();
        }
        return result;
    }

}

??我們修改了get的喚醒條件和阻塞條件,查看輸出結(jié)果:

BlockingQueue: add : 0 : 1
BlockingQueue: add : 1 : 2
BlockingQueue: add : 2 : 3
BlockingQueue: add : 3 : 4
BlockingQueue: add : 4 : 5
BlockingQueue: get : 0 : 4
BlockingQueue: add : 5 : 5
BlockingQueue: get : 1 : 4
BlockingQueue: add : 6 : 5
BlockingQueue: get : 2 : 4
BlockingQueue: add : 7 : 5
BlockingQueue: get : 3 : 4
BlockingQueue: add : 8 : 5
BlockingQueue: get : 4 : 4
BlockingQueue: add : 9 : 5

??可以看到日志正好是我們想要的結(jié)果,當(dāng)數(shù)據(jù)滿的時(shí)候才開始執(zhí)行g(shù)et方法,然后和add方法交替執(zhí)行,有人可能會(huì)問,為啥每次都是add方法先打印啊,要是我就是先執(zhí)行g(shù)et怎么樣,看到我們上面的倆個(gè)線程了么,get和add方法都有可能先執(zhí)行,如果先執(zhí)行了get方法,因?yàn)闆]有數(shù)據(jù),被阻塞了,當(dāng)通過add方法添加過數(shù)據(jù)才被喚醒,所以在我們看來,好像每次都是add方法先執(zhí)行,其實(shí)不然。
??可能有的人會(huì)好奇了,這有啥用啊,為啥要交替執(zhí)行,其實(shí)這里只是展示Condition對(duì)鎖的更加精細(xì)的控制,如果你能夠控制日志按照你的想法想怎么執(zhí)行就怎么執(zhí)行,那也算是對(duì)這一知識(shí)點(diǎn)的熟練掌握吧,比如當(dāng)集合中只要添加了數(shù)據(jù),就讓add和get方法交替執(zhí)行如何實(shí)現(xiàn),這里就不再做演示,同學(xué)們可以自己嘗試一下,畢竟光看是不夠,自己動(dòng)手操作一下,才能有更加直觀的感受。
??事實(shí)上java.util.concurrent包下的BlockingQueue的一些子類就是通過這個(gè)原理實(shí)現(xiàn)的,有興趣的同學(xué)看可以去看看,這里不再贅述。

??synchronized

??這個(gè)同步關(guān)鍵字大概是我們平時(shí)用的次數(shù)最多的了。

?synchronized 包括三種用法:

1: 修飾實(shí)例方法

public synchronized void method() {
}

2: 修飾靜態(tài)方法

public static synchronized void method() {
}

3:修飾代碼塊

//成員鎖,鎖的對(duì)象是變量
public void synMethod(Object o) {
    synchronized(o) {
    }
}

//實(shí)例對(duì)象鎖,this 代表當(dāng)前實(shí)例
synchronized(this) {
}

//當(dāng)前類的 class 對(duì)象鎖
synchronized(Current.class) {
}

??下面我們用synchronized實(shí)現(xiàn)一個(gè)double check單例,代碼如下:

public class Single {

    private static Single single;

    private Single() {

    }

    public static Single getInstance() {
        if (single == null) {
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
            }
        }
        return single;
    }

}

??想必大家可能也常見這種,那知道為什么需要做倆次if判斷呢,外面的if是為了提高效率,避免多線程執(zhí)行每次都走到synchronized中,降低效率,里面的if不用多說,就是為了初始化實(shí)例對(duì)象。

??volatile

??volatile是java虛擬機(jī)提供的一種輕量級(jí)同步機(jī)制,使用起來很簡單,只需要修飾我們需要使用到的變量即可。
??具有以下特性:

保證可見性
禁止指令重排

??在上面的JMM部分我們提到過可見性和指令重排,被volatile修飾的變量會(huì)被編譯器識(shí)別并添加內(nèi)存屏障,如果你的字段是volatile,Java內(nèi)存模型將在寫操作后插入一個(gè)寫屏障 指令,在讀操作前插入一個(gè)讀屏障指令。
??那么寫屏障一旦你完成寫入,任何訪問這個(gè)字段的線程將 會(huì)得到最新的值。讀屏障在你寫入前,會(huì)保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的,因?yàn)閮?nèi)存屏障會(huì)把之前的寫入值及時(shí)的刷新到主內(nèi)存,確保下一個(gè)讀操作能獲取到最新的值,從而保證可見性,而在屏障(讀寫)的里外面,是互相隔絕的,不會(huì)出現(xiàn)被虛擬機(jī)優(yōu)化代碼而發(fā)生屏障內(nèi)外代碼執(zhí)行順序發(fā)生變化的情況,從而達(dá)到防止指令重排的作用。
??但是需要注意的是volatile不能保證自增操作,比如i++,因?yàn)閕++可以被編譯器識(shí)別后會(huì)分三步走,讀取,計(jì)算,存儲(chǔ),多并發(fā)情況下,這些步驟可能會(huì)被多個(gè)線程拆開進(jìn)行,因?yàn)榻Y(jié)果可能不是我們需要的。
??例如線程A,B都已經(jīng)從主內(nèi)存中獲取了a的值,如果cpu切換到A中進(jìn)行+1的操作的時(shí)候,雖然由于volatile的修飾,但是線程A的+1的操作仍然沒有同步主內(nèi)存中,因?yàn)榇藭r(shí)a的值還未發(fā)生變化,然后cpu又切換到B執(zhí)行,B也做了+1操作,但是也沒有同步到主內(nèi)存中,因?yàn)閍的值仍然沒有發(fā)生變化,只是做了一個(gè)計(jì)算操作,然后A,B分別計(jì)算出了結(jié)果,然后由于volatile的作用,開始同步主內(nèi)存中的a,但是此時(shí)i最后的結(jié)果可能就只加了1,這就是因?yàn)閕++的操作是可拆分的,所以i++不在volatile的使用范疇內(nèi)。
??實(shí)際上我們的volatile可以運(yùn)用在單例上,看一下下面的代碼:

public class Singleton {

    private volatile static Singleton singleton;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

??雙重檢查不用多說了,是為了減小synchronized的性能開銷,避免每次都走一下synchronized操作,但用volatile修飾了我們的單例變量,這樣的操作是為什么呢。
??我們看

 singleton = new Singleton();

??這一段操作,實(shí)際上可以分成三個(gè)部分

memory = allocate();  // 1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory); // 2:初始化對(duì)象
instance = memory;  // 3:設(shè)置instance指向剛分配的內(nèi)存地址

??我們上面這段代碼被編譯器優(yōu)化過后實(shí)際上看起來和我們想象的就不太一樣,可能如下:

memory = allocate();  // 1:分配對(duì)象的內(nèi)存空間
instance = memory;   // 3:設(shè)置instance指向剛分配的內(nèi)存地址
ctorInstance(memory); // 2:初始化對(duì)象

??這樣就會(huì)導(dǎo)致我們的第一層if判斷出現(xiàn)問題,instance可能已經(jīng)不會(huì)空了,但是卻沒有分配內(nèi)存地址,導(dǎo)致了 ‘只初始化了一半’ 這種情況的發(fā)生,但是如果用volatile修飾就可以避免了,因?yàn)関olatile可以防止指令重拍,執(zhí)行順序不會(huì)被打亂,結(jié)果就是我們所需要的了。

三 : 開發(fā)工具類

??1: CyclicBarrier
??2: Countdownlatch
??3: Semaphore

??CyclicBarrier

??字面翻譯的意思是柵欄,在多線程我們可以用它攔截多個(gè)線程執(zhí)行過程,比如上交作業(yè)一樣,每個(gè)同學(xué)寫好了作業(yè)交給了組長,組長會(huì)等所在一組的全部組員上交后才給班長,不會(huì)因?yàn)橹挥幸粋€(gè)人上交了就交給班長,我們從提供的方法看一下:

         public CyclicBarrier(int parties)
         public CyclicBarrier(int parties, Runnable barrierAction) 

??這倆個(gè)是構(gòu)造器,parties表示等待到達(dá)的線程數(shù)量,比如一個(gè)組有10個(gè)人,但是組長不管那么多,只要有五個(gè)人上交就給班長,剩余五個(gè)就不用管了,那么就是只要5個(gè)任務(wù)完成,就全部執(zhí)行提交作業(yè)動(dòng)作。barrierAction表示是完成執(zhí)行數(shù)量的任務(wù)后,再額外提前執(zhí)行一個(gè)任務(wù),比如五個(gè)人上交了, 組長會(huì)會(huì)先去吃飯,再給班長,這里的吃飯就是額外的任務(wù)。
??再看另一個(gè)方法:

         public int await() throws InterruptedException, BrokenBarrierException
         public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

??await方法的作用就是攔截提交作業(yè)給老師,就像上面說的,不會(huì)因?yàn)橐粋€(gè)組員交了作業(yè),組長就把作業(yè)給班長,我們可以調(diào)用這個(gè)方法,讓當(dāng)前的任務(wù)進(jìn)行等待,如果調(diào)用的是第二個(gè)方法,表示的是等待在愿意接受的時(shí)間,比如一個(gè)組員作業(yè)給了組長,但是組長需要等至少五個(gè)人上交作業(yè),才會(huì)把作業(yè)給班長,但是其中一個(gè)組員就不干了,說只能等到上課之前,上課前你必須得給我把作業(yè)上交,不然影響老師對(duì)我的印象啊。這里的上課之前就是我們這里輸入的等待時(shí)間,表示等待某段愿意的時(shí)間,當(dāng)然了,如果提前就有5個(gè)同學(xué)上交了,那這個(gè)等待時(shí)間就沒用了,主要是一個(gè)防止作用。
??下面看看我們?nèi)绾问褂肅yclicBarrier演示我們交作業(yè)這一過程

    CyclicBarrier cyclicBarrier;
    cyclicBarrier = new CyclicBarrier(5, new Runnable() {
         @Override
         public void run() {
             Log.d("cyclicBarrier", "等下,我吃個(gè)飯先!");
         }
     });
     for (int i = 0; i < 10; i++) {
         new CommitTask(cyclicBarrier).start();
     }

??這里表示等待五個(gè)同學(xué)交作業(yè)就先吃飯

   public class CommitTask extends Thread {

        private CyclicBarrier cyclicBarrier;

        public CommitTask(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            super.run();
            Log.d("cyclicBarrier", Thread.currentThread().getName() + " : 我交作業(yè)了");
            try {
                cyclicBarrier.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d("cyclicBarrier", Thread.currentThread().getName() + " : 終于批改我的作業(yè)了");

        }
    }

??這里演示了10個(gè)同學(xué)交作業(yè),但是只要有五個(gè)同學(xué)上交就給班長給老師批改的具體過程,我們查看下日志:

   cyclicBarrier: Thread-3 : 我交作業(yè)了
   cyclicBarrier: Thread-4 : 我交作業(yè)了
   cyclicBarrier: Thread-5 : 我交作業(yè)了
   cyclicBarrier: Thread-6 : 我交作業(yè)了
   cyclicBarrier: Thread-7 : 我交作業(yè)了
   cyclicBarrier: 等下,我吃個(gè)飯先!
   cyclicBarrier: Thread-7 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-3 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-4 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-5 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-6 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-8 : 我交作業(yè)了
   cyclicBarrier: Thread-9 : 我交作業(yè)了
   cyclicBarrier: Thread-10 : 我交作業(yè)了
   cyclicBarrier: Thread-11 : 我交作業(yè)了
   cyclicBarrier: Thread-12 : 我交作業(yè)了
   cyclicBarrier: 等下,我吃個(gè)飯先!
   cyclicBarrier: Thread-12 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-8 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-9 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-10 : 終于批改我的作業(yè)了
   cyclicBarrier: Thread-11 : 終于批改我的作業(yè)了

??很直觀了,等到五個(gè)人交齊后先吃的飯,然后再給老師批改作業(yè),然后等下次作業(yè)來了,仍然先吃飯,再批改,另外的

public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

??同學(xué)們可以自行嘗試,這里就不再做演示,其實(shí)查看CyclicBarrier的源碼可以發(fā)現(xiàn),使用到了ReentrantLock和Condition,這倆個(gè)類的作用上面我也已經(jīng)提到過,總體代碼量也不多,就500行左右,感興趣的同學(xué)可以自行研究下如何不使用CyclicBarrier也能實(shí)現(xiàn)上面的交作業(yè)演示效果。

??CountDownLatch

??這個(gè)我們可以看作是用來給線程計(jì)數(shù)判斷的工具,就拿上面交作業(yè)的例子,每交一個(gè)作業(yè),都會(huì)計(jì)一次數(shù),但是CountDownLatch的計(jì)數(shù)是逐漸減小的。我們看下api提供的方法:

   public CountDownLatch(int count)

??只提供了一個(gè)構(gòu)造器,這里的count就表示計(jì)數(shù)的總量,每次計(jì)算都會(huì)減小一個(gè),直到減小到0,觸發(fā)我們需要觸發(fā)的事件,再看其他方法:

public void await()
public boolean await(long timeout, TimeUnit unit) 

??這倆個(gè)方法好理解,就放在我們需要觸發(fā)事件的線程里面就可以,等到計(jì)數(shù)到0的時(shí)候就會(huì)觸發(fā)事件,第二個(gè)方法的時(shí)間表示等待指定時(shí)間,和我們上面cyclicBarrier的等待一樣,不會(huì)一直等下去,時(shí)間一過,自動(dòng)觸發(fā),再看一個(gè)方法:

public void countDown() 

??這個(gè)方法就是用來給我們的總數(shù)進(jìn)行減小的,每調(diào)用一次,總數(shù)減小1,直到-1,但是到0就會(huì)觸發(fā)我們的事件,下面我們演示收作業(yè)這一過程:

        CountDownLatch cdl = new CountDownLatch(10);
        WorkCorrecte workCorrecte = new WorkCorrecte(cdl);
        for (int i = 0; i < 10; i++) {
            CommitWork commitWork = new CommitWork(cdl);
            commitWork.start();
        }
        workCorrecte.start();

??這里表示我們有10個(gè)作業(yè)要收,再往下看:

    /**
     * 展示組員提交作業(yè)過程
     * */
    class CommitWork extends Thread {
        private CountDownLatch countDownLatch;

        public CommitWork(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            Log.d("countDownLatch", "組長等等我,我交下作業(yè),還有 " + (countDownLatch.getCount() - 1) + "個(gè)同學(xué)沒交");
            countDownLatch.countDown();
        }
    }


    /**
     * 展示組長提交作業(yè)過程
     * */
    private class WorkCorrecte extends Thread {

        private CountDownLatch countDownLatch;

        public WorkCorrecte(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }


        @Override
        public void run() {
            super.run();
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d("countDownLatch", "都交完了吧,我給老師了");
        }
    }

??我們要達(dá)到的效果就是全部組員交過了作業(yè)才能給老師,我們看下日志:

countDownLatch: 組長等等我,我交下作業(yè),還有 9個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 8個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 7個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 6個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 5個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 4個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 3個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 2個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 1個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 都交完了吧,我給老師了

??和我們想要的效果很一致,有的同學(xué)可能有疑問,如果修改CommitWork類中調(diào)用countDownLatch.countDown()的位置會(huì)怎么樣呢,其實(shí)這個(gè)問題就是考慮的就是是不是每個(gè)自線程必須執(zhí)行完,最后才能觸發(fā)我們的事件呢?其實(shí)不是的,我們要完成這一效果,就必須把 countDownLatch.countDown()的調(diào)用放在這些線程的最后,我們可以驗(yàn)證一下是不是這樣,修改一下CommitWork類,如下:

 /**
     * 展示組員提交作業(yè)過程
     */
    class CommitWork extends Thread {
        private CountDownLatch countDownLatch;

        public CommitWork(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            countDownLatch.countDown();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d("countDownLatch", "組長等等我,我交下作業(yè),還有 " + countDownLatch.getCount() + "個(gè)同學(xué)沒交");
        }
    }

??再看下我們的日志效果:

countDownLatch: 都交完了吧,我給老師了
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交
countDownLatch: 組長等等我,我交下作業(yè),還有 0個(gè)同學(xué)沒交

??很明顯,只要計(jì)數(shù)器到了0,就會(huì)觸發(fā)我們的事件,不管調(diào)用countDownLatch.countDown()的線程有沒有走完,這里提到這一點(diǎn)主要是為了和我們上面剛說過的CyclicBarrier工具類功能區(qū)別,一個(gè)會(huì)阻塞調(diào)用的線程,一個(gè)不會(huì),免得大家混淆,另外的倆個(gè)參數(shù)的await這里就不再演示了,同學(xué)們可以自行嘗試。

??Semaphore

??semaphore也是一個(gè)用計(jì)數(shù)完成功能的線程控制類,可以設(shè)置訪問資源指定數(shù)量的線程,我們看下提供的方法:

public Semaphore(int permits);
public Semaphore(int permits,boolean fair);

??這里的permits表示的是指定限制線程的數(shù)量,fair表示搶占資源的方式為公平或非公平,大家可能有點(diǎn)懵,慢慢往下看。

···
semaphore.acquire();
//to do something...
semaphore.release();
···

??acquire的作用是獲取資源的通行證,而release則是釋放獲取到的通行證,這倆個(gè)方法總是成對(duì)出現(xiàn)的,我們每調(diào)用一次acquire,semaphore的計(jì)數(shù)就會(huì)加1,直到達(dá)到的我們限制數(shù)量,別的線程再想進(jìn)來就不可以了,除非我們調(diào)用release方法,釋放出一個(gè)通行證,別的線程才可以繼續(xù)進(jìn)來,這一點(diǎn)跟前面說的synchroized作用很像,不同的是synchroized只能一次一個(gè)線程訪問,而semaphore可以限制指定數(shù)量的線程訪問,并且提供了更多的操作方法,我們可以把semaphore當(dāng)作synchroized的升級(jí)版。
??仍然用我們收作業(yè)來比喻,一個(gè)組長去收作業(yè)時(shí),一次只能從一對(duì)組員桌子前收,然后到下一對(duì)桌子,那我們演示一下這個(gè)過程:

public class Work {

    Semaphore semaphore;

    public void main(String[] arg) {
        semaphore = new Semaphore(2);
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    getBook();
                }
            }).start();
        }
    }

    public void getBook() {
        try {
            semaphore.acquire();
            Log.d("semaphore", "是誰?收走了我的作業(yè)? " + Thread.currentThread().getName());
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
}

??通過semaphore.acquire()和semaphore.release()將我們收取作業(yè)的部分給包裹住,并且中間處理了5秒,運(yùn)行可以發(fā)現(xiàn),每隔5秒才會(huì)有倆個(gè)線程進(jìn)入執(zhí)行收作業(yè)的代碼,這里的semaphore就很好的限制了訪問資源的線程數(shù)量。
??Semaphore類提供的方法很多,這里為了節(jié)省篇幅不做過多展示了,下面提供簡要注釋:

/ 從此信號(hào)量獲取一個(gè)許可,在提供一個(gè)許可前一直將線程阻塞,否則線程被中斷。
void acquire()
// 從此信號(hào)量獲取給定數(shù)目的許可,在提供這些許可前一直將線程阻塞,或者線程已被中斷。
void acquire(int permits)
// 從此信號(hào)量中獲取許可,在有可用的許可前將其阻塞。
void acquireUninterruptibly()
// 從此信號(hào)量獲取給定數(shù)目的許可,在提供這些許可前一直將線程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信號(hào)量中當(dāng)前可用的許可數(shù)。
int availablePermits()
// 獲取并返回立即可用的所有許可。
int drainPermits()
// 返回一個(gè) collection,包含可能等待獲取的線程。
protected Collection<Thread> getQueuedThreads()
// 返回正在等待獲取的線程的估計(jì)數(shù)目。
int getQueueLength()
// 查詢是否有線程正在等待獲取。
boolean hasQueuedThreads()
// 如果此信號(hào)量的公平設(shè)置為 true,則返回 true。
boolean isFair()
// 根據(jù)指定的縮減量減小可用許可的數(shù)目。
protected void reducePermits(int reduction)
// 釋放一個(gè)許可,將其返回給信號(hào)量。
void release()
// 釋放給定數(shù)目的許可,將其返回到信號(hào)量。
void release(int permits)
// 返回標(biāo)識(shí)此信號(hào)量的字符串,以及信號(hào)量的狀態(tài)。
String toString()
// 僅在調(diào)用時(shí)此信號(hào)量存在一個(gè)可用許可,才從信號(hào)量獲取許可。
boolean tryAcquire()
// 僅在調(diào)用時(shí)此信號(hào)量中有給定數(shù)目的許可時(shí),才從此信號(hào)量中獲取這些許可。
boolean tryAcquire(int permits)
// 如果在給定的等待時(shí)間內(nèi)此信號(hào)量有可用的所有許可,并且當(dāng)前線程未被中斷,則從此信號(hào)量獲取給定數(shù)目的許可。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
// 如果在給定的等待時(shí)間內(nèi),此信號(hào)量有可用的許可并且當(dāng)前線程未被中斷,則從此信號(hào)量獲取一個(gè)許可。
boolean tryAcquire(long timeout, TimeUnit unit)

四 : 原子操作

??1: 原子變量

??原子變量

??還記得我們之前說過的自增操作不屬于原子操作么,在多線程下進(jìn)行的操作是不安全的,簡單演示一下:

public class Work {

    private int i = 0;

    public void calculate() {
        for (int i = 0; i < 10000; i++) {
            i++;
        }
    }

}

??最后發(fā)現(xiàn)i的值并不等于10000,即使用了volatile進(jìn)行修飾,同時(shí)也是不行的,因?yàn)関olatile不能保證自增操作是安全的,上面也已經(jīng)提到過,這里不再贅述。
??那還有什么簡便的方法呢,原子變量橫空出世,如下:

AtomicInteger
AtomicLong
AtomicBoolean
AtomicArray
AtomicReference
...

??當(dāng)我們想讓一個(gè)int變量在多線程下是安全的話就直接用AtomicInteger修飾,放到多線程環(huán)境下,我們發(fā)現(xiàn)變量是安全的了,如下:

public class Work {

    private AtomicInteger i = new AtomicInteger(0);

    public void calculate() {
        for (int i = 0; i < 10000; i++) {
            i.incrementAndGet();
        }
    }

}

??嗯,使用起來還是非常簡單的,就修飾加初始化,然后就隨便多線程搞,nice。

未完待續(xù)...

流程圖繪制工具

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

相關(guān)閱讀更多精彩內(nèi)容

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