Java多線程4:synchronized鎖機制

髒讀java

一個常見的概念。在多線程中,不免會出如今多個線程中對同一個對象的實例變量進行併發訪問的狀況,若是不作正確的同步處理,那麼產生的後果就是"髒讀",也就是取到的數據實際上是被更改過的。安全

 

多線程線程安全問題示例多線程

看一段代碼:併發

public class ThreadDomain13
{
    private int num = 0;
    
    public void addNum(String userName)
    {
        try
        {
            if ("a".equals(userName))
            {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }
            else
            {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(userName + " num = " + num);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

寫兩個線程分別去add字符串"a"和字符串"b":異步

public class MyThread13_0 extends Thread
{
    private ThreadDomain13 td;
    
    public MyThread13_0(ThreadDomain13 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.addNum("a");
    }
}
public class MyThread13_1 extends Thread
{
    private ThreadDomain13 td;
    
    public MyThread13_1(ThreadDomain13 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.addNum("b");
    }
}

寫一個主函數分別運行這兩個線程:函數

public static void main(String[] args)
{
    ThreadDomain13 td = new ThreadDomain13();
    MyThread13_0 mt0 = new MyThread13_0(td);
    MyThread13_1 mt1 = new MyThread13_1(td);
    mt0.start();
    mt1.start();
}

看一下運行結果:學習

a set over!
b set over!
b num = 200
a num = 200

按照正常來看應該打印"a num = 100"和"b num = 200"纔對,如今卻打印了"b num = 200"和"a num = 200",這就是線程安全問題。咱們能夠想一下是怎麼會有線程安全的問題的:this

  1. mt0先運行,給num賦值100,而後打印出"a set over!",開始睡覺
  2. mt0在睡覺的時候,mt1運行了,給num賦值200,而後打印出"b set over!",而後打印"b num = 200"
  3. mt1睡完覺了,因爲mt0的num和mt1的num是同一個num,因此mt1把num改成了200了,mt0也沒辦法,對於它來講,num只能是100,mt0繼續運行代碼,打印出"a num = 200"

分析了產生問題的緣由,解決就很簡單了,給addNum(String userName)方法加同步便可:spa

public class ThreadDomain13
{
    private int num = 0;
    
    public synchronized void addNum(String userName)
    {
        try
        {
            if ("a".equals(userName))
            {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }
            else
            {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(userName + " num = " + num);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

看一下運行結果:線程

a set over!
a num = 100
b set over!
b num = 200

 

多個對象多個鎖

在同步的狀況下,把main函數內的代碼改一下:

public static void main(String[] args)
{
    ThreadDomain13 td0 = new ThreadDomain13();
    ThreadDomain13 td1 = new ThreadDomain13();
    MyThread13_0 mt0 = new MyThread13_0(td0);
    MyThread13_1 mt1 = new MyThread13_1(td1);
    mt0.start();
    mt1.start();
}

看一下運行結果:

a set over!
b set over!
b num = 200
a num = 100

打印結果的方式變了,打印的順序是交叉的,這又是爲何呢?

這裏有一個重要的概念。關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼或方法(函數)看成鎖,哪一個線程先執行帶synchronized關鍵字的方法,哪一個線程就持有該方法所屬對象的鎖,其餘線程都只能呈等待狀態。可是這有個前提:既然鎖叫作對象鎖,那麼勢必和對象相關,因此多個線程訪問的必須是同一個對象

若是多個線程訪問的是多個對象,那麼Java虛擬機就會建立多個鎖,就像上面的例子同樣,建立了兩個ThreadDomain13對象,就產生了2個鎖。既然兩個線程持有的是不一樣的鎖,天然不會受到"等待釋放鎖"這一行爲的制約,能夠分別運行addNum(String userName)中的代碼。

 

synchronized方法與鎖對象

上面咱們認識了對象鎖,對象鎖這個概念,比較抽象,確實不太好理解,看一個例子,在一個實體類中定義一個同步方法和一個非同步方法:

public class ThreadDomain14_0
{
    public synchronized void methodA()
    {
        try
        {
            System.out.println("Begin methodA, threadName = " + 
                    Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("End methodA, threadName = " + 
                    Thread.currentThread().getName() + ", end Time = " + 
                    System.currentTimeMillis());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    
    public void methodB()
    {
        try
        {
            System.out.println("Begin methodB, threadName = " + 
                    Thread.currentThread().getName() + ", begin time = " + 
                    System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("End methodB, threadName = " + 
                    Thread.currentThread().getName());
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

一個線程調用其同步方法,一個線程調用其非同步方法:

public class MyThread14_0 extends Thread
{
    private ThreadDomain14_0 td;
    
    public MyThread14_0(ThreadDomain14_0 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodA();
    }
}
public class MyThread14_1 extends Thread
{
    private ThreadDomain14_0 td;
    
    public MyThread14_1(ThreadDomain14_0 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodB();
    }
}

寫一個main函數去調用這兩個線程:

public static void main(String[] args)
{
    ThreadDomain14_0 td = new ThreadDomain14_0();
    MyThread14_0 mt0 = new MyThread14_0(td);
    mt0.setName("A");
    MyThread14_1 mt1 = new MyThread14_1(td);
    mt1.setName("B");
    mt0.start();
    mt1.start();
}

看一下運行效果:

Begin methodA, threadName = A
Begin methodB, threadName = B, begin time = 1443697780869
End methodB, threadName = B
End methodA, threadName = A, end Time = 1443697785871

從結果看到,第一個線程調用了實體類的methodA()方法,第二個線程徹底能夠調用實體類的methodB()方法。可是咱們把methodB()方法改成同步就不同了,就不列修改以後的代碼了,看一下運行結果:

Begin methodA, threadName = A
End methodA, threadName = A, end Time = 1443697913156
Begin methodB, threadName = B, begin time = 1443697913156
End methodB, threadName = B

從這個例子咱們得出兩個重要結論:

  1. A線程持有Object對象的Lock鎖,B線程能夠以異步方式調用Object對象中的非synchronized類型的方法
  2. A線程持有Object對象的Lock鎖,B線程若是在這時調用Object對象中的synchronized類型的方法則須要等待,也就是同步

 

synchronized鎖重入

關鍵字synchronized擁有鎖重入的功能。所謂鎖重入的意思就是:當一個線程獲得一個對象鎖後,再次請求此對象鎖時能夠再次獲得該對象的鎖。看一個例子:

public class ThreadDomain16
{
    public synchronized void print1()
    {
        System.out.println("ThreadDomain16.print1()");
        print2();
    }
    
    public synchronized void print2()
    {
        System.out.println("ThreadDomain16.print2()");
        print3();
    }
    
    public synchronized void print3()
    {
        System.out.println("ThreadDomain16.print3()");
    }
}
public class MyThread16 extends Thread
{
    public void run()
    {
        ThreadDomain16 td = new ThreadDomain16();
        td.print1();
    }
}
public static void main(String[] args)
{
    MyThread16 mt = new MyThread16();
    mt.start();
}

看一下運行結果:

ThreadDomain16.print1()
ThreadDomain16.print2()
ThreadDomain16.print3()

看到能夠直接調用ThreadDomain16中的打印語句,這證實了對象能夠再次獲取本身的內部鎖。這種鎖重入的機制,也支持在父子類繼承的環境中

 

異常自動釋放鎖

最後一個知識點是異常。當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。模擬的是把一個long型數做爲除數,從MAX_VALUE開始遞減,直至減爲0,從而產生ArithmeticException。看一下例子:

public class ThreadDomain17
{
    public synchronized void testMethod()
    {
        try
        {
            System.out.println("Enter ThreadDomain17.testMethod, currentThread = " + 
                    Thread.currentThread().getName());
            long l = Integer.MAX_VALUE;
            while (true)
            {
                long lo = 2 / l;
                l--;
            }
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}
public class MyThread17 extends Thread
{
    private ThreadDomain17 td;
    
    public MyThread17(ThreadDomain17 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.testMethod();
    }
}
public static void main(String[] args)
{
    ThreadDomain17 td = new ThreadDomain17();
    MyThread17 mt0 = new MyThread17(td);
    MyThread17 mt1 = new MyThread17(td);
    mt0.start();
    mt1.start();
}

看一下運行結果:

Enter ThreadDomain17.testMethod, currentThread = Thread-0
Enter ThreadDomain17.testMethod, currentThread = Thread-1
java.lang.ArithmeticException: / by zero
    at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)
    at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)
java.lang.ArithmeticException: / by zero
    at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)
    at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)

由於打印結果是靜態的,因此不是很明顯。在l--前一句加上Thread.sleep(1)結論會更明顯,第一句打出來以後,整個程序都停住了,直到Thread-0拋出異常後,Thread-1才能夠運行,這也證實了咱們的結論。

 

後記

文章裏面的這些個結論,記一下都是很快的,可是是否記一下就行了?我認爲記住這些結論一點都不重要,重要的應該是學習如何經過代碼去驗證這些結論。由於只有知道了如何經過代碼去驗證結論,才能夠說真正對於synchronized關鍵字的各類細節有了感性、有了深刻的理解,之後碰到其餘synchronized的場景就能夠以本身的理解去正確分析問題。

相關文章
相關標籤/搜索