Android併發編程 多線程與鎖

該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,若是能給各位看官帶來一絲啓發或者幫助,那真是極好的。java


前言

前一篇Android併發編程開篇呢,主要是簡單介紹一下線程以及JMM,雖然文章不長,但倒是理解後續文章的基礎。本篇文章介紹多線程與鎖。面試

深刻認識Java中的Thread

Thread的三種啓動方式上篇文章已經說了,下面呢,咱們繼續看看Thread這個類。編程

線程的狀態

Java中線程的狀態分爲6種。安全

  1. 初始(NEW):新建立了一個線程對象,但尚未調用start()方法。
  2. 運行(RUNNABLE):Java線程中將就緒(ready)和運行中(running)兩種狀態籠統的稱爲「運行」。線程對象建立後,其餘線程(好比main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的線程在得到CPU時間片後變爲運行中狀態(running)。
  3. 阻塞(BLOCKED):表示線程阻塞於鎖。
  4. 等待(WAITING):進入該狀態的線程須要等待其餘線程作出一些特定動做(通知或中斷)。
  5. 超時等待(TIMED_WAITING):該狀態不一樣於WAITING,它能夠在指定的時間後自行返回。
  6. 終止(TERMINATED):表示該線程已經執行完畢。

線程的幾個常見方法的比較

  1. Thread.sleep(long millis),必定是當前線程調用此方法,當前線程進入TIMED_WAITING狀態,但不釋放對象鎖,millis後線程自動甦醒進入就緒狀態。做用:給其它線程執行機會的最佳方式。
  2. Thread.yield(),必定是當前線程調用此方法,當前線程放棄獲取的CPU時間片,但不釋放鎖資源,由運行狀態變爲就緒狀態,讓OS再次選擇線程。做用:讓相同優先級的線程輪流執行,但並不保證必定會輪流執行。實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中。Thread.yield()不會致使阻塞。該方法與sleep()相似,只是不能由用戶指定暫停多長時間。
  3. thread.join()/thread.join(long millis)當前線程裏調用其它線程thread的join方法,當前線程進入WAITING/TIMED_WAITING狀態,當前線程不會釋放已經持有的對象鎖。線程thread執行完畢或者millis時間到,當前線程進入就緒狀態。
  4. thread.interrupt(),當前線程裏調用其它線程thread的interrupt()方法,中斷指定的線程。
    若是指定線程調用了wait()方法組或者join方法組在阻塞狀態,那麼指定線程會拋出InterruptedException
  5. Thread.interrupted,必定是當前線程調用此方法,檢查當前線程是否被設置了中斷,該方法會重置當前線程的中斷標誌,返回當前線程是否被設置了中斷。
  6. thread.isInterrupted()當前線程裏調用其它線程thread的isInterrupted()方法,返回指定線程是否被中斷
  7. object.wait()當前線程調用對象的wait()方法,當前線程釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(long timeout) timeout時間到自動喚醒。
  8. object.notify()喚醒在此對象監視器上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象監視器上等待的全部線程。

線程安全

volatile 以及 synchronized 關鍵字

在上一篇博文中,各位看官已經對JMM模型有了初步的瞭解,咱們在談論線程安全的時候也無外乎解決上篇博文中提到的3個問題,原子性、可見性、時序性多線程

volatile

當一個共享變量被volatile修飾以後, 其就具有了兩個含義併發

  1. 線程修改了變量的值時, 變量的新值對其餘線程是當即可見的。 換句話說, 就是不一樣線程對這個變量進行操做時具備可見性。即該關鍵字保證了可見性
  2. 禁止使用指令重排序。這裏提到了重排序, 那麼什麼是重排序呢? 重排序一般是編譯器或運行時環境爲了優化程序性能而採起的對指令進行從新排序執行的一種手段。volatile關鍵字禁止指令重排序有兩個含

義: 一個是當程序執行到volatile變量的操做時, 在其前面的操做已經所有執行完畢, 而且結果會對後面的
操做可見, 在其後面的操做尚未進行; 在進行指令優化時, 在volatile變量以前的語句不能在volatile變量後面執行; 一樣, 在volatile變量以後的語句也不能在volatile變量前面執行。即該關鍵字保證了時序性ide

如何正確使用volatile關鍵字呢
一般來講, 使用volatile必須具有如下兩個條件:工具

  1. 對變量的寫操做不會依賴於當前值。 例如自增自減
  2. 該變量沒有包含在具備其餘變量的不變式中。

synchronized

去面試java或者Android相關職位的時候個東西貌似是必問的,關於synchronized這個關鍵字真是有太多太多東西了。尤爲是JDK1.6以後爲了優化synchronized的性能,引入了偏向鎖,輕量級鎖等各類聽起來就頭疼的概念,java還有Android面試世界流傳着一個古老的名言,考察一我的對線程的瞭解成度的話,一個synchronized就足夠了。不過本篇博文不講那些,本篇博文本着讓各位看官都能理解的初衷試着分析一下synchronized關鍵字把性能

重入鎖ReentrantLock

synchronized 關鍵字自動提供了鎖以及相關的條件。 大多數須要顯式鎖的狀況使用synchronized很是方
便, 可是等咱們瞭解了重入鎖和條件對象時, 能更好地理解synchronized關鍵字。 重入鎖ReentrantLock是
Java SE 5.0引入的, 就是支持重進入的鎖, 它表示該鎖可以支持一個線程對資源的重複加鎖。優化

ReentrantLock reentrantLock = new ReentrantLock();
   reentrantLock.lock();
try {
    ...

} finally {
    reentrantLock.unlock();
}

如上代碼所示,這一結構確保任什麼時候刻只有一個線程進入臨界區, 臨界區就是在同一時刻只能有一個任務訪問的代碼區。 一旦一個線程封鎖了鎖對象, 其餘任何線程都沒法進入Lock語句。 把解鎖的操做放在finally中是十分必要的。 若是在臨界區發生了異常, 鎖是必需要釋放的, 不然其餘線程將會永遠被阻塞。

synchronized關鍵字

咱們再來看看synchronized,synchronized關鍵字有如下幾種使用方式

  1. 同步方法(即直接在方法聲明處加上synchronized)

    private synchronized void test() {
    
     }

    等價於

    ReentrantLock reentrantLock = new ReentrantLock();
    
    private void test() {
        reentrantLock.lock();
        try {
           ...
        } finally {
            reentrantLock.unlock();
        }
    }
  2. 同步代碼塊

    上面咱們說過, 每個Java對象都有一個鎖, 線程能夠調用同步方法來得到鎖。 還有另外一種機制能夠獲
    得鎖, 那就是使用一個同步代碼塊, 以下所示:

    synchronized(obj){
    }

    其得到了obj的鎖, obj指的是一個對象。 同步代碼塊是很是脆弱的,一般不推薦使用。 通常實現同步最h好用java.util.concurrent包下提供的類, 好比阻塞隊列。 若是同步方法適合你的程序, 那麼請儘可能使用同步方法, 這樣能夠減小編寫代碼的數量, 減小出錯的機率。
    咱們在代碼中寫的synchronized(this){} 實際上是與上面同樣的,this指代當前對象

  3. 靜態方法加鎖

    static synchronized void test();

這種方式網上有人稱它爲「類鎖」,其實這種說法有些迷惑人,咱們只須要記住一點,全部的鎖都是鎖住的對象,也就是Object自己,你能夠簡單理解爲使用synchronized 是在堆內存中的某一個對象上加了一把鎖,而且這個鎖是可重入的,意思是說若是一個線程已經得到了某個對象的鎖,那麼該線程依然能夠從新得到這把鎖,可是其餘線程若是想訪問這個對象就必須等待上一個得到鎖的線程釋放鎖。

咱們在回過頭來看靜態方法加鎖,爲一個類的靜態方法加鎖,實際上等價於synchronized(Class),即鎖定的是該類的Class對象。

線程同步

Object.wait() / Object.notify() Object.notifyAll()

任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、
wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,能夠
實現等待/通知模式

  1. 使用的前置條件

    當咱們想要使用Object的監視器方法時,須要或者該Object的鎖,代碼以下所示

    synchronized(obj){
        .... //1
        obj.wait();//2
        obj.wait(long millis);//2
        ....//3
    }

    一個線程得到obj的鎖,作了一些時候事情以後,發現須要等待某些條件的發生,調用obj.wait(),該線程會釋放obj的鎖,並阻塞在上述的代碼2處
    obj.wait()和obj.wait(long millis)的區別在於

    obj.wait()是無限等待,直到obj.notify()或者obj.notifyAll()調用並喚醒該線程,該線程獲取鎖以後繼續執行代碼3

    obj.wait(long millis)是超時等待,我只等待long millis 後,該線程會本身醒來,醒來以後去獲取鎖,獲取鎖以後繼續執行代碼3

    obj.notify()是叫醒任意一個等待在該對象上的線程,該線程獲取鎖,線程狀態從BLOCKED進入RUNNABLE

    obj.notifyAll()是叫醒全部等待在該對象上的線程,這些線程會去競爭鎖,獲得鎖的線程狀態從BLOCKED進入RUNNABLE,其餘線程依然是BLOCKED,獲得鎖的線程執行代碼3完畢後釋放鎖,其餘線程繼續競爭鎖,如此反覆直到全部線程執行完畢。

    synchronized(obj){
        .... //1
        obj.notify();//2
        obj.notifyAll();//2
    }

    一個線程得到obj的鎖,作了一些時候事情以後,某些條件已經知足,調用obj.notify()或者obj.notifyAll(),該線程會釋放obj的鎖,並叫醒在obj上等待的線程,
    obj.notify()和obj.notifyAll()的區別在於

    obj.notify()叫醒在obj上等待的任意一個線程(由JVM決定)

    obj.notifyAll()叫醒在obj上等待的所有線程

  2. 使用範式

    synchronized(obj){
        //判斷條件,這裏使用while,而不使用if
        while(obj知足/不知足 某個條件){
            obj.wait()
        }
    }

    放在while裏面,是防止處於WAITING狀態下線程監測的對象被別的緣由調用了喚醒(notify或者notifyAll)方法,可是while裏面的條件並無知足(也可能當時知足了,可是因爲別的線程操做後,又不知足了),就須要再次調用wait將其掛起

條件對象Condition

JDK1.5後提供了Condition接口,該接口定義了相似Object的監視器方法,與Lock配合能夠實現等待/通知模式,可是這二者在使用方式以及功能特性上仍是有差異的

public interface Condition {
    //等待 同object.wait()
    void await() throws InterruptedException;

    //無視中斷等待 object沒有此類方法
    void awaitUninterruptibly();

    //超時等待 同object.wait(long millis)
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    //超時等待 
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    //超時等待 到未來的某個時間 object沒有此類方法
    boolean awaitUntil(Date deadline) throws InterruptedException;

    //通知 同object.notify()
    void signal();

    //通知 同object.notifyAll()
    void signalAll();
}

除了上述API之間的差異外,Condition與Object的監視器方法顯著的差異在於前置條件

wait和notify/notifyAll方法只能在同步代碼塊裏用(這個有的面試官也會考察)

Condition接口對象需和Lock接口配合,經過lock.lock()獲取鎖,lock.newCondition()獲取條件對象更爲靈活
關於Condition接口的具體實現請往下看

LockSupport.park(Object blocker) / LockSupport.unpark(Thread thread)

上面說的Condition是一個接口,咱們來看一下Condition接口的實現,Condition接口的實現主要是經過另一套等待/通知機制完成的。

LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,
而LockSupport也成爲構建同步組件的基礎工具。

LockSupport定義了一組以park開頭的方法用來阻塞當前線程,以及unpark(Thread thread)方法來喚醒一個被阻塞的線程。

既然JDK已經提供了Object的wait和notify/notifyAll方法等方法,那麼LockSupport定義的一組方法有何不一樣呢,咱們來看下面這段代碼就明白了

Thread A = new Thread(new Runnable() {
        @Override
        public void run() {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            try {
                Thread.sleep(10000);//睡眠10s,保證LockSupport.unpark(A);先調用
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //直接調用park方法阻塞當前線程,沒在同步方法或者代碼塊內
            LockSupport.park(this);
            System.out.println(sum);
        }
    });
A.start();

//調用unpark方法喚醒指定線程,即便unpark(Thread)方法先於park方法調用,依然能喚醒
LockSupport.unpark(A);

對比一下Object的wait和notify/notifyAll方法你就能明顯看出區別

final Object obj = new Object();

Thread B = new Thread(new Runnable() {
    @Override
    public void run() {
        synchronized (obj) {
            int sum = 0;
            for (int i = 0; i < 10; i++) {
                sum += i;
            }
            try {
                Thread.sleep(10000);//睡眠10s,保證obj.notify();先調用
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(sum);
        }

    }
});
B.start();


synchronized (obj) {
    //若是obj.notify();先於obj.wait()調用,那麼調用調用obj.wait()的線程會一直阻塞住
    obj.notify();
}

在LockSupport的類說明上其實已經說明了LockSupport相似於Semaphore,

Semaphore是計數信號量。Semaphore管理一系列許可證。每一個acquire方法阻塞,直到有一個許可證能夠得到而後拿走一個許可證;
每一個release方法增長一個許可證,這可能會釋放一個阻塞的acquire方法。

然而,其實並無實際的許可證這個對象,Semaphore只是維持了一個可得到許可證的數量。

Semaphore常常用於限制獲取某種資源的線程數量。

LockSupport經過許可證來聯繫使用它的線程。
若是許可證可用,調用park方法會當即返回並在這個過程當中消費這個許可,否則線程會阻塞。
調用unpark會使許可證可用。(和Semaphores有些許區別,許可證不會累加,最多隻有一張)
由於有了許可證,因此調用park和unpark的前後關係就不重要了,

如何正確中止一個線程

講解了上面那麼多內容,如今出一個小小的筆試題,如何正確中止一個線程,別說是thread.stop()哈,那個已經被標記過期了。若是您想參與這個問題請在評論區評論。


本篇總結

本篇主要是說了關於多線程與鎖的東西。這裏總結一下

volatile 保證了共享變量的可見性和禁止重排序,

Synchronized的做用主要有三個:

(1)確保線程互斥的訪問同步代碼

(2)保證共享變量的修改可以及時可見(這個可能會被許多人忽略了)

(3)有效解決重排序問題。

從JMM上來講

被volatile修飾的共享變量若是被一個線程更改,那麼會通知各個線程大家的副本已通過期了,趕快去內存拉取最新值吧

被Synchronized修飾的方法或者代碼塊,咱們都知道會線程互斥訪問,其實其有像volatile同樣的效果,若是被一個線程更改了共享變量,在Synchronized結束處那麼會通知各個線程大家的副本已通過期了,趕快去內存拉取最新值吧

因爲筆者能力有限,若有不到之處,還請不吝賜教。


下篇預告

Java中的原子類與併發容器


此致,敬禮

相關文章
相關標籤/搜索