synchronized鎖機制 之 代碼塊鎖(轉)


synchronized同步代碼塊

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

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

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));
            }
        }
    }
}

 

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();
        }
    }
}
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),這個對象如果是實例變量的話,指的是對象的引用,只要對象的引用不變,即使改變了對象的屬性,運行結果依然是同步的

 臟讀數據的情況

  線程調用順序是無序的,在一個沒有上鎖的方法里面調用任意已經上鎖兩個的方法,不會保證調兩個上鎖的方法會同時阻塞被一個線程調用,而是第一個上鎖方法阻塞,然后第二個再阻塞,調用第一個方法和第二個方法的中間是異步的,這里做一點操作的話,比如if分支判斷,極有可能導致臟讀數據發生,參考下面:

  臟讀出現了,出現的原因是兩個線程以異步的代碼調用2個同步的方法,中間做了if分支判斷並且sleep延遲,由於調用是在沒有同步的代碼里面,第一個線程進入了if睡覺,還沒開始做插入操作,第二個線程又進入了if,所以導致臟讀出現。

  解決:使用鎖方法  或者 鎖代碼塊都行 在分支前面鎖定代碼塊。

  打印 listsize = 1 臟讀不再出現。

細化synchronized(非this對象x)的三個結論

  synchronized(非this對象x)格式的寫法是將x對象本身作為對象監視器,有三個結論得出:

    1、當多個線程同時執行synchronized(x){}同步代碼塊時呈同步效果

    2、當其他線程執行x對象中的synchronized同步方法時呈同步效果

    3、當其他線程執行x對象方法中的synchronized(this)代碼塊時也呈同步效果

  第一點很明顯,第二點和第三點意思類似,無非一個是同步方法,一個是同步代碼塊罷了,舉個例子驗證一下第二點:

public class MyObject
{
    public synchronized void speedPrintString()
    {
        System.out.println("speedPrintString__getLock time = " + 
                System.currentTimeMillis() + ", run ThreadName = " + 
                Thread.currentThread().getName());
        System.out.println("----------");
        System.out.println("speedPrintString__releaseLock time = " + 
                System.currentTimeMillis() + ", run ThreadName = " + 
                Thread.currentThread().getName());
    }
}

  ThreadDomain24中持有MyObject的引用:

public class ThreadDomain24
{
    public void testMethod1(MyObject mo)
    {
        try
        {
            synchronized (mo)
            {
                System.out.println("testMethod1__getLock time = " + 
                        System.currentTimeMillis() + ", run ThreadName = " + 
                        Thread.currentThread().getName());
                Thread.sleep(5000);
                System.out.println("testMethod1__releaseLock time = " + 
                        System.currentTimeMillis() + ", run ThreadName = " + 
                        Thread.currentThread().getName());
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

  寫兩個線程分別調用"speedPrintString()"方法和"testMethod1(MyObject mo)"方法:

public class MyThread24_0 extends Thread
{
    private ThreadDomain24 td;
    private MyObject mo;
    
    public MyThread24_0(ThreadDomain24 td, MyObject mo)
    {
        this.td = td;
        this.mo = mo;
    }
    
    public void run()
    {
        td.testMethod1(mo);
    }
}
public class MyThread24_1 extends Thread
{
    private MyObject mo;
    
    public MyThread24_1(MyObject mo)
    {
        this.mo = mo;
    }
    
    public void run()
    {
        mo.speedPrintString();
    }
}

  寫一個main函數啟動這兩個線程:

public static void main(String[] args)
{
    ThreadDomain24 td = new ThreadDomain24();
    MyObject mo = new MyObject();
    MyThread24_0 mt0 = new MyThread24_0(td, mo);
    MyThread24_1 mt1 = new MyThread24_1(mo);
    mt0.start();
    mt1.start();
}

  看一下運行結果:

testMethod1__getLock time = 1443855939811, run ThreadName = Thread-0
testMethod1__releaseLock time = 1443855944812, run ThreadName = Thread-0
speedPrintString__getLock time = 1443855944812, run ThreadName = Thread-1
----------
speedPrintString__releaseLock time = 1443855944812, run ThreadName = Thread-1

  看到"speedPrintString()"方法必須等待"testMethod1(MyObject mo)"方法執行完畢才可以執行,沒有辦法異步執行,證明了第二點的結論。第三點的驗證方法類似,就不寫代碼證明了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM