Android並發編程 多線程與鎖


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


前言

前一篇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變量前面執行。即該關鍵字保證了時序性

如何正確使用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中的原子類與並發容器


此致,敬禮


免責聲明!

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



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