JAVA多線程(二)

資源競爭 (線程互斥)
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,表明別的線程正在使用打印機,當前線程將簡單的返回,而不是等待別的線程釋放鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章