synchronized鎖機制 之 程式碼塊鎖

AskHarries發表於2018-06-28

synchronized同步程式碼塊

用關鍵字synchronized宣告方法在某些情況下是有弊端的,比如A執行緒呼叫同步方法執行一個較長時間的任務,那麼B執行緒必須等待比較長的時間。這種情況下可以嘗試使用synchronized同步語句塊來解決問題。看一下例子:

下面例子是優化後的例子 使用程式碼塊鎖,原先例子是方法鎖,就是同步 必須要執行2個for

package org.java.base.sync;
public class ThreadDomain18
{
 public void doLongTimeTask() throws Exception
 {
 for (int i = 0; i < 100; i++)
 {
 System.out.println("nosynchronized threadName = " + 
 Thread.currentThread().getName() + ", i = " + (i + 1));
 }
 System.out.println();
 synchronized (this)
 {
 for (int i = 0; i < 100; i++)
 {
 System.out.println("synchronized threadName = " + 
 Thread.currentThread().getName() + ", i = " + (i + 1));
 }
 }
 }
}複製程式碼
package org.java.base.sync;
public class MyThread18 extends Thread
{
 private ThreadDomain18 td;
 
 public MyThread18(ThreadDomain18 td)
 {
 this.td = td;
 }
 
 public void run()
 {
 try
 {
 td.doLongTimeTask();
 } 
 catch (Exception e)
 {
 e.printStackTrace();
 }
 }
}複製程式碼
package org.java.base.sync;

public class Test1 {
 public static void main(String[] args)
 {
 ThreadDomain18 td = new ThreadDomain18();
 MyThread18 mt0 = new MyThread18(td);
 MyThread18 mt1 = new MyThread18(td);
 mt0.start();
 mt1.start();
 }
}複製程式碼

執行結果,分兩部分來看:

synchronized threadName = Thread-1, i = 1
synchronized threadName = Thread-1, i = 2
nosynchronized threadName = Thread-0, i = 95
synchronized threadName = Thread-1, i = 3
nosynchronized threadName = Thread-0, i = 96
synchronized threadName = Thread-1, i = 4
nosynchronized threadName = Thread-0, i = 97
synchronized threadName = Thread-1, i = 5
nosynchronized threadName = Thread-0, i = 98
synchronized threadName = Thread-1, i = 6
nosynchronized threadName = Thread-0, i = 99
synchronized threadName = Thread-1, i = 7
nosynchronized threadName = Thread-0, i = 100複製程式碼
...
synchronized threadName = Thread-1, i = 98
synchronized threadName = Thread-1, i = 99
synchronized threadName = Thread-1, i = 100
synchronized threadName = Thread-0, i = 1
synchronized threadName = Thread-0, i = 2
synchronized threadName = Thread-0, i = 3
...複製程式碼

這個實驗可以得出以下兩個結論:

1、當A執行緒訪問物件的synchronized程式碼塊的時候,B執行緒依然可以訪問物件方法中其餘非synchronized塊的部分,第一部分的執行結果證明了這一點

2、當A執行緒進入物件的synchronized程式碼塊的時候,B執行緒如果要訪問這段synchronized塊,那麼訪問將會被阻塞,第二部分的執行結果證明了這一點

所以,從執行效率的角度考慮,有時候我們未必要把整個方法都加上synchronized,而是可以採取synchronized塊的方式,對會引起執行緒安全問題的那一部分程式碼進行synchronized就可以了。

兩個synchronized塊之間具有互斥性

如果執行緒1訪問了一個物件A方法的synchronized塊,那麼執行緒B對同一物件B方法的synchronized塊的訪問將被阻塞,寫個例子來證明一下:

public class ThreadDomain19
{
    public void serviceMethodA()
    {
        synchronized (this)
        {
            try
            {
                System.out.println("A begin time = " + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("A end time = " + System.currentTimeMillis());
            } 
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            
        }
    }
    
    public void serviceMethodB()
    {
        synchronized (this)
        {
            System.out.println("B begin time = " + System.currentTimeMillis());
            System.out.println("B end time = " + System.currentTimeMillis());
        }
    }
}複製程式碼

寫兩個執行緒分別呼叫這兩個方法:

public class MyThread19_0 extends Thread
{
    private ThreadDomain19 td;
    
    public MyThread19_0(ThreadDomain19 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.serviceMethodA();
    }
}複製程式碼
public class MyThread19_1 extends Thread
{
    private ThreadDomain19 td;
    
    public MyThread19_1(ThreadDomain19 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.serviceMethodB();
    }
}複製程式碼

寫個main函式:

public static void main(String[] args)
{
    ThreadDomain19 td = new ThreadDomain19();
    MyThread19_0 mt0 = new MyThread19_0(td);
    MyThread19_1 mt1 = new MyThread19_1(td);
    mt0.start();
    mt1.start();
}複製程式碼

看一下執行結果:

A begin time = 1443843271982
A end time = 1443843273983
B begin time = 1443843273983
B end time = 1443843273983複製程式碼

看到對於serviceMethodB()方法synchronized塊的訪問必須等到對於serviceMethodA()方法synchronized塊的訪問結束之後。那其實這個例子,我們也可以得出一個結論:synchronized塊獲得的是一個物件鎖,換句話說,synchronized塊鎖定的是整個物件

synchronized塊和synchronized方法

既然上面得到了一個結論synchronized塊獲得的是物件鎖,那麼如果執行緒1訪問了一個物件方法A的synchronized塊,執行緒2對於同一物件同步方法B的訪問應該是會被阻塞的,因為執行緒2訪問同一物件的同步方法B的時候將會嘗試去獲取這個物件的物件鎖,但這個鎖卻線上程1這裡。寫一個例子證明一下這個結論:

public class ThreadDomain20
{
    public synchronized void otherMethod()
    {
        System.out.println("----------run--otherMethod");
    }
    
    public void doLongTask()
    {
        synchronized (this)
        {
            for (int i = 0; i < 1000; i++)
            {
                System.out.println("synchronized threadName = " + 
                        Thread.currentThread().getName() + ", i = " + (i + 1));
                try
                {
                    Thread.sleep(5);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
}複製程式碼

寫兩個執行緒分別呼叫這兩個方法:

public class MyThread20_0 extends Thread
{
    private ThreadDomain20 td;
    
    public MyThread20_0(ThreadDomain20 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.doLongTask();
    }
}複製程式碼
public class MyThread20_1 extends Thread
{
    private ThreadDomain20 td;
    
    public MyThread20_1(ThreadDomain20 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.otherMethod();
    }
}複製程式碼

寫個main函式呼叫一下,這裡”mt0.start()”後sleep(100)以下是為了確保mt0執行緒先啟動:

public static void main(String[] args) throws Exception
    {
        ThreadDomain20 td = new ThreadDomain20();
        MyThread20_0 mt0 = new MyThread20_0(td);
        MyThread20_1 mt1 = new MyThread20_1(td);
        mt0.start();
        Thread.sleep(100);
        mt1.start();
    }複製程式碼

看一下執行結果:

...
synchronized threadName = Thread-0, i = 995
synchronized threadName = Thread-0, i = 996
synchronized threadName = Thread-0, i = 997
synchronized threadName = Thread-0, i = 998
synchronized threadName = Thread-0, i = 999
synchronized threadName = Thread-0, i = 1000
----------run--otherMethod複製程式碼

證明了我們的結論。為了進一步完善這個結論,把”otherMethod()”方法的synchronized去掉再看一下執行結果:

...
synchronized threadName = Thread-0, i = 16
synchronized threadName = Thread-0, i = 17
synchronized threadName = Thread-0, i = 18
synchronized threadName = Thread-0, i = 19
synchronized threadName = Thread-0, i = 20
----------run--otherMethod
synchronized threadName = Thread-0, i = 21
synchronized threadName = Thread-0, i = 22
synchronized threadName = Thread-0, i = 23
...複製程式碼

“otherMethod()”方法和”doLongTask()”方法中的synchronized塊非同步執行了

將任意物件作為物件監視器

總結一下前面的內容:

1、synchronized同步方法

(1)對其他synchronized同步方法或synchronized(this)同步程式碼塊呈阻塞狀態

(2)同一時間只有一個執行緒可以執行synchronized同步方法中的程式碼

2、synchronized同步程式碼塊

(1)對其他synchronized同步方法或synchronized(this)同步程式碼塊呈阻塞狀態

(2)同一時間只有一個執行緒可以執行synchronized(this)同步程式碼塊中的程式碼

前面都使用synchronized(this)的格式來同步程式碼塊,其實Java還支援對”任意物件”作為物件監視器來實現同步的功能。這個”任意物件”大多數是例項變數方法的引數,使用格式為synchronized(非this物件)。看一下將任意物件作為物件監視器的使用例子:

public class ThreadDomain21
{
    private String userNameParam;
    private String passwordParam;
    private String anyString = new String();
    
    public void setUserNamePassword(String userName, String password)
    {
        try
        {
            synchronized (anyString)
            {
                System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                        "在 " + System.currentTimeMillis() + " 進入同步程式碼塊");
                userNameParam = userName;
                Thread.sleep(3000);
                passwordParam = password;
                System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                        "在 " + System.currentTimeMillis() + " 離開同步程式碼塊");
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}複製程式碼

寫兩個執行緒分別呼叫一下:

public class MyThread21_0 extends Thread
{
    private ThreadDomain21 td;
    
    public MyThread21_0(ThreadDomain21 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.setUserNamePassword("A", "AA");
    }
}複製程式碼
public class MyThread21_1 extends Thread
{
    private ThreadDomain21 td;
    
    public MyThread21_1(ThreadDomain21 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.setUserNamePassword("B", "B");
    }
}複製程式碼

寫一個main函式呼叫一下:

public static void main(String[] args)
{
    ThreadDomain21 td = new ThreadDomain21();
    MyThread21_0 mt0 = new MyThread21_0(td);
    MyThread21_1 mt1 = new MyThread21_1(td);
    mt0.start();
    mt1.start();
}複製程式碼

看一下執行結果:

執行緒名稱為:Thread-0在 1443855101706 進入同步程式碼塊
執行緒名稱為:Thread-0在 1443855104708 離開同步程式碼塊
執行緒名稱為:Thread-1在 1443855104708 進入同步程式碼塊
執行緒名稱為:Thread-1在 1443855107708 離開同步程式碼塊複製程式碼

這個例子證明了:多個執行緒持有”物件監視器”為同一個物件的前提下,同一時間只能有一個執行緒可以執行synchronized(非this物件x)程式碼塊中的程式碼。

鎖非this物件具有一定的優點:如果在一個類中有很多synchronized方法,這時雖然能實現同步,但會受到阻塞,從而影響效率。但如果同步程式碼塊鎖的是非this物件,則synchronized(非this物件x)程式碼塊中的程式與同步方法是非同步的,不與其他鎖this同步方法爭搶this鎖,大大提高了執行效率。

其實無論是方法所還是程式碼鎖都是要以一個物件監視器來鎖定,鎖定的程式碼是同步的,鎖this是當前物件,鎖String是String這個物件,鎖Object是Object這個物件,互不干擾,如果有其它執行緒呼叫同樣用到跟上面鎖this、Objcet、String相同物件的方法或程式碼,就需要等待同步,鎖程式碼塊比鎖方法更加靈活。因為鎖方法鎖的是this 也就是當前物件,當一個執行緒正在呼叫當前這個物件的所方法時,導致其它執行緒呼叫不了該物件的其它鎖this的程式碼,也調不了所有該物件的鎖方法

鎖的是當前這個執行緒,針對鎖的物件的這段程式碼或方法,一次只能一個執行緒執行,其它執行緒執行到此的話會暫停,如果是執行其它非鎖的則是非同步的,注意這裡不要被多執行緒搞迷糊了。單個執行緒執行的時候都是同步的,當這個執行緒被阻塞後,之後的程式碼(鎖內的和鎖外的)無論什麼都不會執行,只有當喚醒或者恢復正常時才會繼續往下走,走完鎖內的程式碼就會放鎖,然後繼續走剩餘的程式碼

注意一下”private String anyString = new String();”這句話,現在它是一個全域性物件,因此監視的是同一個物件。如果移到try裡面,那麼物件的監視器就不是同一個了,呼叫的時候自然是非同步呼叫,可以自己試一下。

最後提一點,synchronized(非this物件x),這個物件如果是例項變數的話,指的是物件的引用,只要物件的引用不變,即使改變了物件的屬性,執行結果依然是同步的

synchronized鎖機制 之 程式碼塊鎖


相關文章