Java編程-多線程同步

同步的需求

例如你寫了一個金融類程序,使用取錢/存錢這一對操作來表示金融交易。在這個程序里,一個線程執行取錢操作,另一個線程負責存錢操作。每一個線程操作著一對代表著金融交易的名字和金額的共享變量、類和實例域變量。對于一個合法的交易,每一個線程都必須在下一個線程操作之前完成對變量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);
        }
    }
}

我們可能期望輸出結果為:

Deposit 2000
Withdrawal 250 

但是結果可能是以下的幾種組合

Withdrawal 250.0
Withdrawal 2000.0
Deposit 2000.0
Deposit 250.0

Java同步機制

Java的同步機制可以避免多于一個線程在同一時間執行同一段關鍵代碼。Java的同步機制基于監聽(monitor)和鎖(lock)的概念。將monitor想象成一個保護裝置,它保護著一段關鍵代碼,而lock是通過保護裝置的一個途徑。意思是:當一個線程想要訪問受保護裝置保護的代碼時,這個線程必須取得一個與這個monitor相關聯的鎖(每一個對象都有它自己的鎖)。如果其他線程持有這個鎖,那么JVM強制讓請求的線程等待直到鎖被釋放。JVM提供了monitorentermonitorexit指令,但是我們不必使用這種低等級的方法。我們可以使用synchronized關鍵字和對應的synchronized語句。

Synchronized語句

synchronized語句以synchronized關鍵字開始。sync object可以看做鎖,而下面的代碼塊可以看做monitor保護的關鍵代碼。

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:如果想要知道線程是否獲得了給定對象的鎖,可以調用Thread類的holdsLock(Object o)方法。

讓方法同步

過度的使用synchronized會導致代碼運行的極為低效。例如,你的程序的一個方法里面存在連續的兩個synchronized語段,每個synchronized代碼段都嘗試獲取同一個對象的關聯鎖。由于獲取和釋放資源都需要消耗時間,重復地調用這個方法會降低程序的性能。
當一個實例或類的方法被冠以synchronized關鍵字時,這個方法稱為同步的方法。例如:synchronized void print(String s)。當你對一個實例的方法進行同步時,每次調用這個方法都需要獲得與該實例相關聯的鎖。

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);
            }
        }

    }
}

類的方法也能被同步,一些程序混淆了同步的實例方法和類方法。以下兩點需要注意:

  1. 對象鎖和類鎖之間并不關聯。他們是不同的實體。獲取和釋放每個鎖都是相互獨立的。一個同步的實例方法調用一個同步的類方法兩種鎖都需要。首先,同步的實例方法需要對應實例的鎖,同時,實例方法需要類方法的鎖。
  2. 同步的類方法可以調用一個對象的同步方法。同步的類方法調用一個對象的同步方法也需要兩個鎖。

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() 
   }
}

同步失效

當一個線程自愿或非自愿地離開了臨界代碼,它會釋放鎖以便其他線程能獲得。假設兩個線程想要進入同一段臨界區。為了避免線程同時進入同一個臨界區,每一個線程必須嘗試去取得同一個鎖。如果每一個線程嘗試去獲得不同的鎖而且成功了,兩個線程都會進入臨界區,一個線程無須等待另一個線程結束,因為他們擁有的是兩個不同的鎖。

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是對當前線程對象的引用,所以上述代碼會導致同步失效。

死鎖

在一些程序里面,下面的場景可能會發生。線程A獲得了一個鎖,線程B也需要這個鎖來進入自己的臨界區。相同的,線程B也獲得一個鎖,線程A需要這個鎖來進入自己的臨界區。由于沒有一個線程獲得它們需要的鎖,每個線程必須等待以獲得鎖,此外,由于沒有線程能夠繼續執行以釋放對方所需要的鎖,程序就會進去死鎖狀態。

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:為了避免死鎖,我們必須仔細分析代碼當中是否存在線程之間的鎖依賴問題。

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

推薦閱讀更多精彩內容