1、什麼是資源競爭
有這樣一種資源,在某一時刻只能被一個線程所使用:比如打印機、某個文件等等,如果多個線程不加控制的同時使用這類資源,必然會導至錯誤。
下面的例子模擬了一個打印機,多個線程不加控制的同時使用這個打印機:
public class Printer
{
public void print(int printer, String content)
{
System.out.println("Start working for [" +printer+"]");
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println(content);
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println("Work complete for [" +printer+"]\n");
}
public static void main(String[] args)
{
Printer p = new Printer();
for (int i=0; i<3; i++)
new Thread(new MyThread(p)).start();
}
}
class MyThread implements Runnable
{
private static int counter = 0;
private final int id = counter++;
private Printer printer;
public MyThread(Printer printer)
{
this.printer = printer;
}
@Override
public void run()
{
printer.print(id, "Content of " + id);
}
}
輸出結果(sample)
Start working for [0] Start working for [1] =================== =================== Start working for [2] Content of 0 Content of 1 =================== =================== =================== Content of 2 Work complete for [0] Work complete for [1] =================== Work complete for [2]
從結果可以看到,打印機的輸出完全亂套了,各個線程想要打印的內容全部參雜在一起了。
2、解決資源競爭問題
原則上要解決這類問題並不難,只需要一個鎖的機制。任何線程在使用打印機前必須先對打印機上鎖;在使用完打印機後釋放鎖;如果線程嘗試對打印機上鎖時別的線程已經上了鎖,則該線程必須等待別的線程先釋放鎖。
Java中,解決上述資源共享類的問題是通過關鍵字synchronized實現的。java中的對象都有一個‘鎖’,這樣,任何一個線程嘗試訪問對象的synchronized方法時,必須要先獲得對象的'鎖',否則必須等待。
一個對象可能會有多個synchronized方法,比如synchronized a()方法和synchronized b()方法。當一個線程獲得了對象的鎖,進行a()方法、或b()方法了,那麼在線程釋放該對象的鎖之前,別的線程是不能訪問該對象的其它synchronized方法的。
下面例子是之前的例子的改良版本,只需要簡單的把Printer對象的print方法定義成synchronized的就可以達到我們的要求了:
public class Printer
{
public synchronized void print(int printer, String content)
{
System.out.println("Start working for [" +printer+"]");
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println(content);
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println("Work complete for [" +printer+"]\n");
}
public static void main(String[] args)
{
Printer p = new Printer();
for (int i=0; i<3; i++)
new Thread(new MyThread(p)).start();
}
}
class MyThread implements Runnable
{
private static int counter = 0;
private final int id = counter++;
private Printer printer;
public MyThread(Printer printer)
{
this.printer = printer;
}
@Override
public void run()
{
printer.print(id, "Content of " + id);
}
}
輸出結果
Start working for [0] =================== Content of 0 =================== Work complete for [0] Start working for [2] =================== Content of 2 =================== Work complete for [2] Start working for [1] =================== Content of 1 =================== Work complete for [1]
從結果的輸出可以看出來,被模擬的打印機資源在某一時刻,僅被一個線程所使用。
3、臨界區
有些時候,你可能不需要隔離整個方法,而只需要隔離方法中的部分代碼,這部分被隔離的代碼就叫做臨界區。臨界區中的代碼在某一時刻,只能被一個線程訪問。
下面的例子,是用臨界區的方式實現了前面的例子:
public class Printer
{
public void print(int printer, String content)
{
synchronized(this)
{
System.out.println("Start working for [" +printer+"]");
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println(content);
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println("Work complete for [" +printer+"]\n");
}
}
public static void main(String[] args)
{
Printer p = new Printer();
for (int i=0; i<3; i++)
new Thread(new MyThread(p)).start();
}
}
class MyThread implements Runnable
{
private static int counter = 0;
private final int id = counter++;
private Printer printer;
public MyThread(Printer printer)
{
this.printer = printer;
}
@Override
public void run()
{
printer.print(id, "Content of " + id);
}
}
可以看到,在Java中臨界區也是通過synchronized關鍵字實現的。在synchronized關鍵字後面,要傳一個對象參數,任何線程要進入臨界區時必須先要獲得該對象的鎖,退出臨界區時要釋放該對象的鎖,這樣別的線程纔有機會進入臨界區。
從上面兩個例子可以看出,臨界區和synchronized方法,其原理都是一樣的,都是通過在對象上加鎖來實現的,只不過臨界區來得更加靈活,因爲它不光可以對this對象加鎖,也可以對任何別的對象加鎖。
4、Lock
Java1.5提供了一個顯示加鎖的機制,比起synchronized方式來說,顯示加鎖的方法可能讓代碼看上去更加複雜,但是也帶來了更好的靈活性。
下面的例子,用Lock的機制實現了前面的例子:
public class Printer
{
private Lock lock = new ReentrantLock();
public void print(int printer, String content)
{
lock.lock();
try
{
System.out.println("Start working for [" +printer+"]");
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println(content);
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println("Work complete for [" +printer+"]\n");
}
finally
{
lock.unlock();
}
}
public static void main(String[] args)
{
Printer p = new Printer();
for (int i=0; i<3; i++)
new Thread(new MyThread(p)).start();
}
}
class MyThread implements Runnable
{
private static int counter = 0;
private final int id = counter++;
private Printer printer;
public MyThread(Printer printer)
{
this.printer = printer;
}
@Override
public void run()
{
printer.print(id, "Content of " + id);
}
}
使用Lock的時候,必須要注意兩點:
- 鎖的釋放必須放在finally塊裏面,以保證鎖被正確的釋放;
- 如果被隔間的方法或臨界間需要返回一個值,那麼return語句應該放在try塊中,從而不至於使unlock發生得過早而導至錯誤的發生。
使用顯示的Lock機制,可以讓程序更加的靈活。比如上面的例子中,如果嘗試使用打印機的時候,打印機正被別的線程所使用,那麼早取消本次打印。要實現這樣的功能,使用synchronized可能不太容易實現,但是使用Lock機制的話,就非常簡單了:
public class Printer
{
private Lock lock = new ReentrantLock();
public void print(int printer, String content)
{
boolean isLocked = lock.tryLock();
if (!isLocked)
return;
try
{
System.out.println("Start working for [" +printer+"]");
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println(content);
Thread.yield();
System.out.println("===================");
Thread.yield();
System.out.println("Work complete for [" +printer+"]\n");
}
finally
{
if(isLocked)
lock.unlock();
}
}
public static void main(String[] args)
{
Printer p = new Printer();
for (int i=0; i<3; i++)
new Thread(new MyThread(p)).start();
}
}
class MyThread implements Runnable
{
private static int counter = 0;
private final int id = counter++;
private Printer printer;
public MyThread(Printer printer)
{
this.printer = printer;
}
@Override
public void run()
{
printer.print(id, "Content of " + id);
}
}
Lock.tryLock()方法嘗試對lock對象加鎖並返回一個boolean值,如果成功了,返回true,表明當前沒有別的線程在使用打印機,那麼當前線程將獲得lock對象的鎖,並繼續打印;如果失敗了,返回false,表明別的線程正在使用打印機,當前線程將簡單的返回,而不是等待別的線程釋放鎖。