synchronized同步代碼塊
用關鍵字synchronized聲明方法在某些情況下是有弊端的,比如A線程調用同步方法執行一個較長時間的任務,那么B線程必須等待比較長的時間。這種情況下可以嘗試使用synchronized同步語句塊來解決問題。看一下例子:
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鎖,大大提高了運行效率。
注意一下"private String anyString = new String();"這句話,現在它是一個全局對象,因此監視的是同一個對象。如果移到try里面,那么對象的監視器就不是同一個了,調用的時候自然是異步調用,可以自己試一下。
最后提一點,synchronized(非this對象x),這個對象如果是實例變量的話,指的是對象的引用,只要對象的引用不變,即使改變了對象的屬性,運行結果依然是同步的。
細化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)"方法執行完畢才可以執行,沒有辦法異步執行,證明了第二點的結論。第三點的驗證方法類似,就不寫代碼證明了。
