總結(jié)Java的多線程處理--線程與鎖(一)同步synchronized

線程與鎖模型

線程與鎖模型是比較原始的一種處理并發(fā)的方式,主要是對(duì)底層硬件的運(yùn)行過程形式化,這是它的優(yōu)點(diǎn)也是缺點(diǎn)。
線程與鎖模型非常直接,幾乎所有的編程語(yǔ)言都提供了支持,但是如果不了解該模型,那么程序會(huì)很容易出錯(cuò),而且難以維護(hù)。

為什么需要鎖

我們先來看一段多線程的代碼:

public class Counting {
  public static void main( String[] args) throws InterruptedException {
    class Counter {
      private int count = 0;
      public void increment() { ++count; }
      public int getCount() { return count;}
    }
    final Counter counter = new Counter();
    class CountingThread extends Thread {
    public void run() {
      for(int x = 0; x < 10000; x++)
        counter.increment();
      }
    }
    CountingThread t1 = new CountingThread();
    CountingThread t2 = new CountingThread();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.getCount());

這是創(chuàng)建了兩個(gè)線程t1與t2,每一個(gè)線程都調(diào)用了counter.increment(),10000次,看上去特別簡(jiǎn)單,但是每次運(yùn)行都會(huì)有不同的結(jié)果,這是因?yàn)樵诓僮鱟ounter對(duì)象的時(shí)候發(fā)生了競(jìng)態(tài)條件。

競(jìng)態(tài)條件是指代碼的行為取決于各操作的時(shí)序。


如果不理解我們先看一下java編譯器是如何處理的++count

//獲取值
getfield #2
//將常量i 1 進(jìn)棧
iconst_1
//加i
iadd
//更新值
putfield#2

問題就出在這里,如果同時(shí)調(diào)用increment(),兩個(gè)線程在獲取值的時(shí)候是同一個(gè)值如100,那么放回去的時(shí)候雖然操作了兩次increment(),但是實(shí)際結(jié)果是101。

synchronize

java中遇到這種問題可以有一種的解決辦法,進(jìn)行同步(synchronize)訪問。
只需要在之前的代碼中這么改一下:

······
  public synchronized void increment() { ++count; }
······

那么在線程使用increment函數(shù)的時(shí)候會(huì)獲得該函數(shù)的鎖,其他線程將不能訪問,直到該線程返回時(shí)釋放鎖。
現(xiàn)在因?yàn)樵黾恿送降拇a,執(zhí)行都會(huì)獲得正確的結(jié)果--20000。

但是synchronize也會(huì)帶來很多坑,下面一一介紹。

亂序編譯

static boolan isReady = false
static int number = 0;
static Thread t1 = new Thread() {
    public void run() {
      number = 100;
      isReady = true;
    }
static Thread t2 = new Thread() {
    public void run() {
      if (isReady)
     System.out.println(number);
      else 
     System.out.println("not ready");
    }

想想看如果同時(shí)運(yùn)行上面的代碼會(huì)發(fā)生什么事?

結(jié)果:

  1. 打印not ready
  2. 打印100。
  3. 打印0。 (為什么?)

為什么 number = 100; isReady = true;語(yǔ)句發(fā)生了顛倒?

但是事實(shí)上是有可能發(fā)生的:

  1. 編譯器的靜態(tài)優(yōu)化會(huì)打亂。
  2. JVM的動(dòng)態(tài)優(yōu)化會(huì)打亂。
  3. 硬件可以通過亂序執(zhí)行來優(yōu)化性能。

但實(shí)際上還有更糟糕的,有時(shí)候一個(gè)線程產(chǎn)生的修改可能對(duì)于另外一個(gè)線程來講是不可見的。

從常識(shí)上來說,無論是編譯器,JVM還是硬件都不應(yīng)該改變代碼的原有邏輯,這里我們需要有明確的標(biāo)準(zhǔn)來知道可能會(huì)發(fā)生什么,那就是Java內(nèi)存模型。

在Java內(nèi)存模型中還說明了上面一個(gè)問題的答案:

如果讀線程與寫線程不進(jìn)行同步,就不能保證可見性。
所以除了increment()之外,也應(yīng)該對(duì)getCount()方法同步,不然可能會(huì)得到一個(gè)已經(jīng)失效的值。

死鎖

有一個(gè)著名的問題--哲學(xué)家進(jìn)餐問題


1.png

如圖。
哲學(xué)家的狀態(tài)可能是「思考」也可能是「饑餓」,如果是饑餓,他就會(huì)將兩邊的筷子拿起來,并且進(jìn)餐一段時(shí)間。進(jìn)餐結(jié)束后哲學(xué)家就會(huì)返回筷子。
那么代碼可以這么寫

synchronized(左邊的筷子) {
  synchronized(右邊的筷子)
}

這樣會(huì)出現(xiàn)一個(gè)問題,如果所有的哲學(xué)家在某個(gè)時(shí)刻,將左邊的筷子都拿起來,就都不能拿到右邊的筷子了,而且也不能釋放左邊的筷子,這樣程序就會(huì)一直卡組,這就是死鎖。

如果解決?
我們可以給筷子設(shè)置編號(hào),只能先拿小的,然后拿大的。
或者給哲學(xué)家拿筷子的順序進(jìn)行設(shè)置。
雖然可以解決但是依然暴露出synchronized的問題。

方法內(nèi)部的陷阱

private synchronized void update {
  for (Person person: persons)
    person.eat();
}

這段邏輯看上去沒有問題,方法加上了synchronized,所以多線程使用的時(shí)候也會(huì)同步訪問。
但實(shí)際上也是有一個(gè)陷阱,在eat方法這個(gè)地方。
因?yàn)閷?duì)eat方法不了解,所以可能eat方法中也調(diào)用了synchronized函數(shù),這樣就是使用了兩把鎖,就像之前的哲學(xué)家進(jìn)餐問題一樣,可能會(huì)發(fā)生死鎖。
解決的辦法是將persons拷貝一份:

private void update {
  ArrayList<Person> personsCopy;
  synchronized(this) {
  personCopy = (ArrayList<Person>)persons.clone();
  for (Person person: persons)
    person.eat();
}

這樣調(diào)用方法的時(shí)候不需要加鎖,而且也減少了持有鎖的時(shí)間。

總結(jié)

  1. 對(duì)共享變量需要同步化。
  2. 讀線程和寫線程需要同步化。
  3. 按照約定的全局順序來獲得多把鎖。
  4. 持有鎖的時(shí)候盡量不要調(diào)用外部方法。
  5. 持有鎖的時(shí)間應(yīng)該盡量短。
最后編輯于
?著作權(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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