Java編程-多線程同步

同步的需求

例如你寫(xiě)了一個(gè)金融類程序搬卒,使用取錢/存錢這一對(duì)操作來(lái)表示金融交易结序。在這個(gè)程序里,一個(gè)線程執(zhí)行取錢操作辱士,另一個(gè)線程負(fù)責(zé)存錢操作泪掀。每一個(gè)線程操作著一對(duì)代表著金融交易的名字和金額的共享變量、類和實(shí)例域變量颂碘。對(duì)于一個(gè)合法的交易异赫,每一個(gè)線程都必須在下一個(gè)線程操作之前完成對(duì)變量name和mount的分配。下面的例子展示了為什么需要同步头岔。

NeedForSynchronizationDemo.java

// NeedForSynchronizationDemo.java
public class NeedForSynchronizationDemo
{
    public static void main(String[] args)
    {
        FinTrans ft = new FinTrans();
        FinTransThread depositThread = new FinTransThread(ft, "Deposit Thread");
        FinTransThread withdrawalThread = new FinTransThread(ft, "Withdrawal Thread");
        depositThread.start();
        withdrawalThread.start();
    }
}

class FinTrans
{
    public static String transName;
    public static int transAmount;
}

class FinTransThread extends Thread
{
    private FinTrans ft;

    public FinTransThread(FinTrans ft, String threadName)
    {
        super(threadName);
        this.ft = ft;
    }

    @Override 
    public void run()
    {
        if (getName().equals("Deposit Thread"))
        {
            ft.transName = "Deposit";
            try
            {
                Thread.sleep((int)(Math.random() * 1000));
            }
            catch (InterruptedException e)
            {

            }
            ft.transAmount = 2000;
            System.out.println(ft.transName + " " + ft.transAmount);
        }
        else
        {
            ft.transName = "Withdrawal";
            try
            {
                Thread.sleep((int)(Math.random() * 1000));
            }
            catch (InterruptedException e)
            {

            }
            ft.transAmount = 250;
            System.out.println(ft.transName + " " + ft.transAmount);
        }
    }
}

我們可能期望輸出結(jié)果為:

Deposit 2000
Withdrawal 250 

但是結(jié)果可能是以下的幾種組合

Withdrawal 250.0
Withdrawal 2000.0
Deposit 2000.0
Deposit 250.0

Java同步機(jī)制

Java的同步機(jī)制可以避免多于一個(gè)線程在同一時(shí)間執(zhí)行同一段關(guān)鍵代碼祝辣。Java的同步機(jī)制基于監(jiān)聽(tīng)(monitor)和鎖(lock)的概念。將monitor想象成一個(gè)保護(hù)裝置切油,它保護(hù)著一段關(guān)鍵代碼蝙斜,而lock是通過(guò)保護(hù)裝置的一個(gè)途徑。意思是:當(dāng)一個(gè)線程想要訪問(wèn)受保護(hù)裝置保護(hù)的代碼時(shí)澎胡,這個(gè)線程必須取得一個(gè)與這個(gè)monitor相關(guān)聯(lián)的鎖(每一個(gè)對(duì)象都有它自己的鎖)孕荠。如果其他線程持有這個(gè)鎖,那么JVM強(qiáng)制讓請(qǐng)求的線程等待直到鎖被釋放攻谁。JVM提供了monitorentermonitorexit指令稚伍,但是我們不必使用這種低等級(jí)的方法。我們可以使用synchronized關(guān)鍵字和對(duì)應(yīng)的synchronized語(yǔ)句戚宦。

Synchronized語(yǔ)句

synchronized語(yǔ)句以synchronized關(guān)鍵字開(kāi)始个曙。sync object可以看做鎖,而下面的代碼塊可以看做monitor保護(hù)的關(guān)鍵代碼受楼。

synchronized ("sync object")
{
   // Access shared variables and other shared resources
}

SynchronizationDemo.java

// SynchronizationDemo.java
public class SynchronizationDemo
{
    public static void main(String[] args)
    {
        FinTrans ft = new FinTrans();
        FinTransThread depositThread = new FinTransThread(ft, "Deposit Thread");
        FinTransThread withdrawalThread = new FinTransThread(ft, "Withdrawal Thread");
        depositThread.start();
        withdrawalThread.start();
    }
}

class FinTrans
{
    public static String transName;
    public static int transAmount;
}

class FinTransThread extends Thread
{
    private FinTrans ft;

    public FinTransThread(FinTrans ft, String threadName)
    {
        super(threadName);
        this.ft = ft;
    }

    @Override 
    public void run()
    {
        for (int i = 0; i < 100; i++)
        {
            if (getName().equals("Deposit Thread"))
            {
                synchronized(ft)
                {
                    ft.transName = "Deposit";
                    try
                    {
                        Thread.sleep((int)(Math.random() * 1000));
                    }
                    catch (InterruptedException e)
                    {

                    }
                    ft.transAmount = 2000;
                    System.out.println(ft.transName + " " + ft.transAmount);    
                }

            }
            else
            {
                synchronized(ft)
                {
                    ft.transName = "Withdrawal";
                    try
                    {
                        Thread.sleep((int)(Math.random() * 1000));
                    }
                    catch (InterruptedException e)
                    {

                    }
                    ft.transAmount = 250;
                    System.out.println(ft.transName + " " + ft.transAmount);
                }

            }
        }

    }
}

Tips:如果想要知道線程是否獲得了給定對(duì)象的鎖垦搬,可以調(diào)用Thread類的holdsLock(Object o)方法。

讓方法同步

過(guò)度的使用synchronized會(huì)導(dǎo)致代碼運(yùn)行的極為低效艳汽。例如猴贰,你的程序的一個(gè)方法里面存在連續(xù)的兩個(gè)synchronized語(yǔ)段,每個(gè)synchronized代碼段都嘗試獲取同一個(gè)對(duì)象的關(guān)聯(lián)鎖河狐。由于獲取和釋放資源都需要消耗時(shí)間米绕,重復(fù)地調(diào)用這個(gè)方法會(huì)降低程序的性能。
當(dāng)一個(gè)實(shí)例或類的方法被冠以synchronized關(guān)鍵字時(shí)馋艺,這個(gè)方法稱為同步的方法栅干。例如:synchronized void print(String s)。當(dāng)你對(duì)一個(gè)實(shí)例的方法進(jìn)行同步時(shí)捐祠,每次調(diào)用這個(gè)方法都需要獲得與該實(shí)例相關(guān)聯(lián)的鎖碱鳞。

SynchronizationDemo2.java

//SynchronizationDemo2.java
public class SynchronizationDemo2
{
    public static void main(String[] args)
    {
        FinTrans ft = new FinTrans();
        FinTransThread depositThread = new FinTransThread(ft, "Deposit Thread");
        FinTransThread withdrawalThread = new FinTransThread(ft, "Withdrawal Thread");
        depositThread.start();
        withdrawalThread.start();
    }
}

class FinTrans
{
    public static String transName;
    public static int transAmount;

    synchronized public void update(String transName, int transAmount)
    {
        this.transName = transName;
        this.transAmount = transAmount;
        System.out.println(transName + " " + transAmount);
    }
}

class FinTransThread extends Thread
{
    private FinTrans ft;

    public FinTransThread(FinTrans ft, String threadName)
    {
        super(threadName);
        this.ft = ft;
    }

    @Override 
    public void run()
    {
        for (int i = 0; i < 100; i++)
        {
            if (getName().equals("Deposit Thread"))
            {
                ft.update("Deposit Thread", 2000);
            }
            else
            {
                ft.update("Withdrawal Thread", 250);
            }
        }

    }
}

類的方法也能被同步,一些程序混淆了同步的實(shí)例方法和類方法雏赦。以下兩點(diǎn)需要注意:

  1. 對(duì)象鎖和類鎖之間并不關(guān)聯(lián)劫笙。他們是不同的實(shí)體芙扎。獲取和釋放每個(gè)鎖都是相互獨(dú)立的。一個(gè)同步的實(shí)例方法調(diào)用一個(gè)同步的類方法兩種鎖都需要填大。首先戒洼,同步的實(shí)例方法需要對(duì)應(yīng)實(shí)例的鎖,同時(shí)允华,實(shí)例方法需要類方法的鎖圈浇。
  2. 同步的類方法可以調(diào)用一個(gè)對(duì)象的同步方法。同步的類方法調(diào)用一個(gè)對(duì)象的同步方法也需要兩個(gè)鎖靴寂。

LockTypes.java

// LockTypes.java
class LockTypes
{
   // Object lock acquired just before execution passes into instanceMethod()
   synchronized void instanceMethod ()
   {
      // Object lock released as thread exits instanceMethod()
   }
   // Class lock acquired just before execution passes into classMethod()
   synchronized static void classMethod (LockTypes lt)
   {
      lt.instanceMethod ();
      // Object lock acquired just before critical code section executes
 
      synchronized (lt)
      {
         // Critical code section
         // Object lock released as thread exits critical code section
      }
      // Class lock released as thread exits classMethod() 
   }
}

同步失效

當(dāng)一個(gè)線程自愿或非自愿地離開(kāi)了臨界代碼磷蜀,它會(huì)釋放鎖以便其他線程能獲得。假設(shè)兩個(gè)線程想要進(jìn)入同一段臨界區(qū)百炬。為了避免線程同時(shí)進(jìn)入同一個(gè)臨界區(qū)褐隆,每一個(gè)線程必須嘗試去取得同一個(gè)鎖。如果每一個(gè)線程嘗試去獲得不同的鎖而且成功了剖踊,兩個(gè)線程都會(huì)進(jìn)入臨界區(qū)庶弃,一個(gè)線程無(wú)須等待另一個(gè)線程結(jié)束,因?yàn)樗麄儞碛械氖莾蓚€(gè)不同的鎖德澈。

NoSynchronizationDemo.java

// NoSynchronizationDemo.java
class NoSynchronizationDemo
{
   public static void main (String [] args)
   {
      FinTrans ft = new FinTrans ();
      TransThread tt1 = new TransThread (ft, "Deposit Thread");
      TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
      tt1.start ();
      tt2.start ();
   }
}
class FinTrans
{
   public static String transName;
   public static double amount;
}
class TransThread extends Thread
{
   private FinTrans ft;
   TransThread (FinTrans ft, String name)
   {
      super (name); // Save thread's name
      this.ft = ft; // Save reference to financial transaction object
   }
   public void run ()
   {
      for (int i = 0; i < 100; i++)
      {
           if (getName ().equals ("Deposit Thread"))
           {
               synchronized (this)
               {
                  ft.transName = "Deposit";
                  try
                  {
                     Thread.sleep ((int) (Math.random () * 1000));
                  }
                  catch (InterruptedException e)
                  {
                  }
                  ft.amount = 2000.0;
                  System.out.println (ft.transName + " " + ft.amount);
               }
           }
           else
           {
               synchronized (this)
               {
                  ft.transName = "Withdrawal";
                  try
                  {
                     Thread.sleep ((int) (Math.random () * 1000));
                  }
                  catch (InterruptedException e)
                  {
                  }
                  ft.amount = 250.0;
                  System.out.println (ft.transName + " " + ft.amount);
               }
           }
      }
   }
}

由于this是對(duì)當(dāng)前線程對(duì)象的引用歇攻,所以上述代碼會(huì)導(dǎo)致同步失效。

死鎖

在一些程序里面梆造,下面的場(chǎng)景可能會(huì)發(fā)生缴守。線程A獲得了一個(gè)鎖,線程B也需要這個(gè)鎖來(lái)進(jìn)入自己的臨界區(qū)镇辉。相同的屡穗,線程B也獲得一個(gè)鎖,線程A需要這個(gè)鎖來(lái)進(jìn)入自己的臨界區(qū)摊聋。由于沒(méi)有一個(gè)線程獲得它們需要的鎖鸡捐,每個(gè)線程必須等待以獲得鎖栈暇,此外麻裁,由于沒(méi)有線程能夠繼續(xù)執(zhí)行以釋放對(duì)方所需要的鎖,程序就會(huì)進(jìn)去死鎖狀態(tài)源祈。

DeadlockDemo.java

// DeadlockDemo.java
class DeadlockDemo
{
   public static void main (String [] args)
   {
      FinTrans ft = new FinTrans ();
      TransThread tt1 = new TransThread (ft, "Deposit Thread");
      TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
      tt1.start ();
      tt2.start ();
   }
}
class FinTrans
{
   public static String transName;
   public static double amount;
}
class TransThread extends Thread
{
   private FinTrans ft;
   private static String anotherSharedLock = "";
   TransThread (FinTrans ft, String name)
   {
      super (name); // Save thread's name
      this.ft = ft; // Save reference to financial transaction object
   }
   public void run ()
   {
      for (int i = 0; i < 100; i++)
      {
           if (getName ().equals ("Deposit Thread"))
           {
               synchronized (ft)
               {
                  synchronized (anotherSharedLock)
                  {
                     ft.transName = "Deposit";
                     try
                     {
                        Thread.sleep ((int) (Math.random () * 1000));
                     }
                     catch (InterruptedException e)
                     {
                     }
                     ft.amount = 2000.0;
                     System.out.println (ft.transName + " " + ft.amount);
                  }
               }
           }
           else
           {
               synchronized (anotherSharedLock)
               {
                  synchronized (ft)
                  {
                     ft.transName = "Withdrawal";
                     try
                     {
                        Thread.sleep ((int) (Math.random () * 1000));
                     }
                     catch (InterruptedException e)
                     {
                     }
                     ft.amount = 250.0;
                     System.out.println (ft.transName + " " + ft.amount);
                  }
               }
           }
      }
   }
}

Tip:為了避免死鎖煎源,我們必須仔細(xì)分析代碼當(dāng)中是否存在線程之間的鎖依賴問(wèn)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末香缺,一起剝皮案震驚了整個(gè)濱河市手销,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌图张,老刑警劉巖锋拖,帶你破解...
    沈念sama閱讀 212,657評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诈悍,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡兽埃,警方通過(guò)查閱死者的電腦和手機(jī)侥钳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,662評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)柄错,“玉大人舷夺,你說(shuō)我怎么就攤上這事∈勖玻” “怎么了给猾?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,143評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)颂跨。 經(jīng)常有香客問(wèn)我敢伸,道長(zhǎng),這世上最難降的妖魔是什么恒削? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,732評(píng)論 1 284
  • 正文 為了忘掉前任详拙,我火速辦了婚禮,結(jié)果婚禮上蔓同,老公的妹妹穿的比我還像新娘饶辙。我一直安慰自己,他們只是感情好斑粱,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,837評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布弃揽。 她就那樣靜靜地躺著,像睡著了一般则北。 火紅的嫁衣襯著肌膚如雪矿微。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,036評(píng)論 1 291
  • 那天尚揣,我揣著相機(jī)與錄音涌矢,去河邊找鬼。 笑死快骗,一個(gè)胖子當(dāng)著我的面吹牛娜庇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播方篮,決...
    沈念sama閱讀 39,126評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼名秀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了藕溅?” 一聲冷哼從身側(cè)響起匕得,我...
    開(kāi)封第一講書(shū)人閱讀 37,868評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巾表,沒(méi)想到半個(gè)月后汁掠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體略吨,經(jīng)...
    沈念sama閱讀 44,315評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,641評(píng)論 2 327
  • 正文 我和宋清朗相戀三年考阱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了晋南。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,773評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡羔砾,死狀恐怖负间,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情姜凄,我是刑警寧澤政溃,帶...
    沈念sama閱讀 34,470評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站态秧,受9級(jí)特大地震影響董虱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜申鱼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,126評(píng)論 3 317
  • 文/蒙蒙 一愤诱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捐友,春花似錦淫半、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,859評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吁峻。三九已至艳吠,卻和暖如春购城,著一層夾襖步出監(jiān)牢的瞬間休弃,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,095評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工雕欺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留富雅,地道東北人蒲犬。 一個(gè)月前我還...
    沈念sama閱讀 46,584評(píng)論 2 362
  • 正文 我出身青樓宜狐,卻偏偏與公主長(zhǎng)得像势告,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肌厨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,676評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容