Android多線程之線程鎖

前言

上篇文章講了線程安全問題,要保證原子性,可見性和有序性的操作才能保證線程安全。也講到了synchronized、volatile,本章講講這些是什么。

什么是鎖

首先要知道一個東西,鎖(Lock)。鎖,大家都知道吧,把東西鎖起來不讓別人拿到,直到把鎖打開,才可以拿到。把東西比作數(shù)據(jù),把人比作線程,多線程間能對同個數(shù)據(jù)進行操作是不安全的,但是鎖能保證你當前只有一個線程能對數(shù)據(jù)操作,直到線程操作完,下個線程接著操作。這就是鎖的作用,讓多個線程更好地協(xié)作,避免多個線程的操作交錯導致數(shù)據(jù)異常的問題。接下來看看鎖的一些特點和問題,一定要耐心看完對鎖才能進一步了解。

鎖的特點

  • 臨界區(qū)
    當我們給東西上鎖后,進行操作,操作完再把鎖打開,這個上鎖和解鎖的中間,就是臨界區(qū)。放在代碼中是這么解釋的,持有鎖的線程獲取鎖后和釋放鎖前執(zhí)行的代碼叫做臨界區(qū)。(看到后面怎么用鎖,大概就知道是什么意思了)

  • 排他性
    還記得原子性是什么嗎?操作不可分割,也就是說整個操作只能是一個線程去執(zhí)行,那這個操作的范圍是多大呢?就是剛才說的臨界區(qū)。總結(jié)一點:排他性能夠保障一個共享變量在任一時刻只能被一個線程訪問,這就保證了臨界區(qū)代碼一次只能夠被一個線程執(zhí)行,臨界區(qū)的操作具有不可分割性

  • 串行
    在沒有鎖的時候,多線程下并發(fā)對數(shù)據(jù)進行操作,這是很危險的。所以在有鎖的情況下,只能一個個線程訪問數(shù)據(jù),就體現(xiàn)出串行了

  • 三種保障
    鎖能夠保護共享變量實現(xiàn)線程安全,它的作用包括保障原子性、可見性和有序性。

  • 調(diào)度策略
    鎖的調(diào)度策略分為公平策略和非公平策略,對應的鎖就叫公平鎖和非公平鎖。多線程情況下,會有多個線程要訪問數(shù)據(jù),只能有一個進去訪問,其他都在外面等待,但是公平策略情況下,這個等待是按照先到排前面的規(guī)則來執(zhí)行;不公平策略情況下,按照搶占式來搶占,沒有順序。公平鎖以增加上下文切換為代價,保障了鎖調(diào)度的公平性,增加了線程暫停和喚醒的可能性。

鎖的兩個問題

  • 泄漏鎖
    鎖泄漏是指一個線程獲得鎖后,由于程序的錯誤導致鎖一直無法被釋放,導致其他線程一直無法獲得該鎖。

  • 活躍性問題
    鎖泄漏會導致活躍性問題,這些問題包括死鎖、和鎖死等。(后面會講到死鎖和鎖死的差別,別混淆了)

鎖的類型

鎖可分為內(nèi)部鎖(synchronized)、顯式鎖(ReentrantLock)、讀寫鎖(ReentrantReadWriteLock)、輕量級鎖(volatile)四種。

內(nèi)部鎖(synchronized)

synchronized是個關(guān)鍵字應該都有見過吧,它可以修飾方法(同步方法),也可以修飾代碼塊,也可以修飾靜態(tài)方法給它加鎖。

修飾實例方法

    private var count = 0
    @Synchronized
    fun log(text: String) {
        for (index in 0..4) {
            count++
            println(Thread.currentThread().name + ":" + text + ":" + count)
        }
    }

    fun getLog(){
        thread { log("測試1") }
        thread { log("測試2") }
    }
        //測試結(jié)果
        Thread-292:測試1:1
        Thread-292:測試1:2
        Thread-292:測試1:3
        Thread-292:測試1:4
        Thread-292:測試1:5
        Thread-293:測試2:6
        Thread-293:測試2:7
        Thread-293:測試2:8
        Thread-293:測試2:9
        Thread-293:測試2:10

由于用synchronized修飾了方法,測試2的線程需要等待測試1的線程執(zhí)行完才可以執(zhí)行

修飾靜態(tài)方法

    companion object{
        private var count = 0
        @Synchronized
        fun log(text: String){
            for (index in 0..4) {
                count++
                println(Thread.currentThread().name + ":" + text + ":" + count)
            }
        }
    }

修飾代碼塊

    private var count = 0
    fun println1(text: String) {
        synchronized(this) {
            count++
            println(Thread.currentThread().name + ":" + text + ":" + count)
        }
    }
//或者
    private var count = 0
    private val lock = Any()
    fun println1(text: String) {
        synchronized(lock) {
            count++
            println(Thread.currentThread().name + ":" + text + ":" + count)
        }
    }

其中,修飾方法時候,鎖住的是當前類的字節(jié)碼文件;修飾代碼塊的時候,鎖住的是對象


synchronized

還是拿圖出來比較好解釋

在多線程運行過程中, 線程會去先搶對象的監(jiān)視器(monitor) ,這個監(jiān)視器是對象獨有的,其實就相當于一把鑰匙,搶到了,那你就獲得了當前代碼塊兒的執(zhí)行權(quán)。

其他沒有搶到的線程會進入隊列(SynchronizedQueue)當中等待,等待當前線程執(zhí)行完后,釋放鎖。最后當前線程執(zhí)行完畢后通知出隊然后繼續(xù)重復當前過程。從 jvm 的角度來看 monitorenter 和 monitorexit 指令代表著代碼的執(zhí)行與結(jié)束 。

再來看看這個鎖的特點:
1、監(jiān)視器鎖:因為使用 synchronized 實現(xiàn)的線程同步是通過監(jiān)視器(monitor)來實現(xiàn)的,所以內(nèi)部鎖也叫監(jiān)視器鎖。

2、自動獲取/釋放:線程對同步代碼塊的鎖的申請和釋放由 JVM 內(nèi)部實施,線程在進入同步代碼塊前會自動獲取鎖,并在退出同步代碼塊時自動釋放鎖,這也是同步代碼塊被稱為內(nèi)部鎖的原因。(異常時也是會自動釋放的)

3、鎖定方法/類/對象:synchronized 關(guān)鍵字可以用來修飾方法,鎖住特定類和特定對象。

4、臨界區(qū):同步代碼塊就是內(nèi)部鎖的臨界區(qū),線程在執(zhí)行臨界區(qū)代碼前必須持有該臨界區(qū)的內(nèi)部鎖。

5、鎖句柄:內(nèi)部鎖鎖的對象就叫鎖句柄(就是剛才代碼里面的this或者lock對象),鎖句柄通常會用 private 和 final 關(guān)鍵字進行修飾。因為鎖句柄變量一旦改變,會導致執(zhí)行同一個同步代碼塊的多個線程實際上用的是不同的鎖。

6、不會泄漏:泄漏指的是鎖泄漏,內(nèi)部鎖不會導致鎖泄漏,因為 javac 編譯器把同步代碼塊編譯為字節(jié)碼時,對臨界區(qū)中可能拋出的異常做了特殊處理,這樣臨界區(qū)的代碼出了異常也不會妨礙鎖的釋放。

7、非公平鎖:內(nèi)部鎖是使用的是非公平策略,是非公平鎖,也就是不會增加上下文切換開銷。

顯式鎖(ReentrantLock)

ReentrantLock 的使用同 synchronized 有點不同,它的加鎖和解鎖操作都需要手動完成

private int count = 0;
private ReentrantLock reentrantLock = new ReentrantLock();
public void print(String text) {
  reentrantLock.lock();
  try {
    System.out.println(Thread.currentThread().getName() + ":" + text + ":" + count);
  } catch (Exception e) {
  } finally {
    reentrantLock.unlock();
  }
}

lock() 和 unlock() 分別是加鎖和解鎖操作。ReentrantLock 與 synchronized 不同,當異常發(fā)生時 synchronized 會自動釋放鎖,但是 ReentrantLock 并不會自動釋放鎖。因此好的方式是將 unlock 操作放在 finally 代碼塊中,保證任何時候鎖都能夠被正常釋放掉。

默認情況下,synchronized 和 ReentrantLock 都是非公平鎖。但是 ReentrantLock 可以通過傳入 true 來創(chuàng)建一個公平鎖。所謂公平鎖就是通過同步隊列來實現(xiàn)多個線程按照申請鎖的順序獲取鎖。

創(chuàng)建一個公平鎖:

private int count = 0;
private ReentrantLock reentrantLock = new ReentrantLock(true);
public void print(String text) {
    reentrantLock.lock();
    try {
    System.out.println(Thread.currentThread().getName() + ":" + text + ":" + count);
    } catch (Exception e) {
    } finally {
    reentrantLock.unlock();
    }
}
讀寫鎖
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void test() {
  // 讀操作
  reentrantReadWriteLock.readLock().lock();
  try {
  } catch (Exception e) {
  } finally {
    reentrantReadWriteLock.readLock().unlock();
  }
  // 寫操作
  reentrantReadWriteLock.writeLock().lock();
  try {
  } catch (Exception e) {
  } finally {
    reentrantReadWriteLock.writeLock().unlock();
  }
}

讀寫鎖和顯式鎖的差別在于,把讀和寫的操作細分出來,這樣在讀寫時不會發(fā)生沖突。

輕量級鎖(volatile)

volatile,用來修飾變量的關(guān)鍵字,當我們多線程對一個變量進行修改時,會有線程安全問題。volatile就是把這個變量改動時候會設(shè)立一個屏障,簡單點說,就是你要去取值時是寫的操作,那么你在取值前去把要把這個值修改的線程給攔住,你寫完之后,再加一層存儲的屏障,這屏障是為了讓你把修改的值更新到主存去,等更新完后兩個屏障一起去掉,之后用這個volatile修飾的變量,每次取值都要去主存中取,這樣就能保住讀的時候能讀到正取的數(shù)據(jù),也就保住了可見性。

注:volatile不能保證原子性,因為當一個變量進行x++的操作后實際是分為temp = x,temp = x + 1,x = temp,這三個操作,那么在中途另一個線程插進來了,值就不正確了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。