同步的需求
例如你寫了一個金融類程序,使用取錢/存錢這一對操作來表示金融交易。在這個程序里,一個線程執行取錢操作,另一個線程負責存錢操作。每一個線程操作著一對代表著金融交易的名字和金額的共享變量、類和實例域變量。對于一個合法的交易,每一個線程都必須在下一個線程操作之前完成對變量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提供了monitorenter
和monitorexit
指令,但是我們不必使用這種低等級的方法。我們可以使用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);
}
}
}
}
類的方法也能被同步,一些程序混淆了同步的實例方法和類方法。以下兩點需要注意:
- 對象鎖和類鎖之間并不關聯。他們是不同的實體。獲取和釋放每個鎖都是相互獨立的。一個同步的實例方法調用一個同步的類方法兩種鎖都需要。首先,同步的實例方法需要對應實例的鎖,同時,實例方法需要類方法的鎖。
- 同步的類方法可以調用一個對象的同步方法。同步的類方法調用一個對象的同步方法也需要兩個鎖。
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:為了避免死鎖,我們必須仔細分析代碼當中是否存在線程之間的鎖依賴問題。