同步的需求
例如你寫(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提供了monitorenter
和monitorexit
指令稚伍,但是我們不必使用這種低等級(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)需要注意:
- 對(duì)象鎖和類鎖之間并不關(guān)聯(lián)劫笙。他們是不同的實(shí)體芙扎。獲取和釋放每個(gè)鎖都是相互獨(dú)立的。一個(gè)同步的實(shí)例方法調(diào)用一個(gè)同步的類方法兩種鎖都需要填大。首先戒洼,同步的實(shí)例方法需要對(duì)應(yīng)實(shí)例的鎖,同時(shí)允华,實(shí)例方法需要類方法的鎖圈浇。
- 同步的類方法可以調(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)題。