在Java中synchronized可用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多只有一個線程執行這段代碼。
而synchronized底層是通過使用對象的監視器鎖(monitor)來確保同一時刻只有一個線程執行被修飾的方法或者代碼塊。
對於同步控制,我們需要明確幾點:
A.無論synchronized關鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或函數當作鎖――而且同步方法很可能還會被其他線程的對象訪問。
B.每個對象只有一個鎖(lock)與之相關聯。
C.實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以盡量避免無謂的同步控制。
Synchronized修飾的對象:
1.修飾一個代碼塊,被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象(對象鎖);
2.修飾一個方法,被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調用這個方法的對象(對象鎖);
3.修改一個靜態的方法,其作用的范圍是整個靜態方法,作用的對象是這個類的所有對象(類鎖);
4.修改一個類,其作用的范圍是synchronized后面括號括起來的部分,作用主的對象是這個類的所有對象(類鎖)。
從上面的鎖對象可以看出,Synchronized修飾的鎖對象其實就2種:對象鎖,類鎖。
解釋:
(1)對象鎖:Synchronized修飾非靜態方法、代碼塊(引入的是對象是:非靜態變量、非類);
非靜態方法:
public synchronized void method(){// todo}
非靜態變量:
private Object o = new Object(); public void method(){ //o 鎖定的對象 synchronized(o){ // todo } }
非類(這個可以與類對比,就可以明白):
public void method(){ //方法里的this 指的是當前的方法,方法鎖 synchronized(this){ // todo } }
測試:
1)當多個線程使用同一個對象sync1,即:
SyncThread sync1 = new SyncThread(); Thread thread1 = new Thread(sync1, "thread1"); Thread thread2 = new Thread(sync1, "thread2"); thread1.start(); thread2.start();
因為線程thread1與thread2引入同一個對象sync1,屬於同一個鎖對象,所以線程執行是按照先執行thread1后釋放鎖,thread2才能獲取鎖並執行其方法。
2)當多個線程使用不同對象sync1與sync2,即:
SyncThread sync1 = new SyncThread(); SyncThread sync2 = new SyncThread(); Thread thread1 = new Thread(sync1, "thread1"); Thread thread2 = new Thread(sync2, "thread2"); thread1.start(); thread2.start();
因為線程thread1與thread2引入不同對象sync1與sync2,屬於不同鎖對象,所以線程thread1與thread2並行執行方法。
(2)類鎖:Synchronized修飾靜態方法、代碼塊(引入的是對象是:靜態變量、類);
靜態方法:
public synchronized static void method(){// todo}
靜態變量:
private static Object o = new Object(); public void method(){ //o 鎖定的對象 synchronized(o){ // todo } }
類(與非類對比,就可以明白):
class SyncThread implements Runnable{ public void method(){ synchronized(SyncThread.class){ // todo 同步代碼塊 } } public void run(){ method(); } }
測試:
1)當多個線程使用同一個對象,還是不同對象,即:
Thread thread1 = new Thread(對象1, "thread1"); Thread thread2 = new Thread(對象2, "thread2"); thread1.start(); thread2.start();
線程thread1與thread2肯定是按照先后順序執行,即:先執行thread1后,才能執行thread2。
總結:
(1)如果synchronized引入的鎖對象是類鎖(該類所有的對象同一把鎖),那么多個線程訪問該鎖鎖定的功能時,肯定是有先后順序的,而不能同時執行。
(2)如果synchronized引入的鎖對象是方法鎖,那么多個線程訪問該鎖鎖定的功能時,要區分是是不是同一個鎖對象,是同一個鎖對象,功能與類鎖類似;否則,多個線程可以同時執行該鎖鎖定的功能。
具體功能詳解與代碼示例:
1.1修飾代碼塊
1、synchronized修飾代碼塊:
說明:一個線程訪問一個對象中的synchronized(this)同步代碼塊時,其他試圖訪問該對象的線程將被阻塞。
例子:
public class SyncThread implements Runnable { private static int count; public SyncThread(){ count = 0; } @Override public void run() { synchronized (this) { //鎖對象是該方法,即方法鎖 for (int i = 0; i < 5; i++) { try { System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } } } } public static int getCount() { return count; } }
測試代碼:
public class SyncThreadTest { public static void main(String[] args) { SyncThread sync = new SyncThread(); Thread thread1 = new Thread(sync, "thread1"); Thread thread2 = new Thread(sync, "thread2"); thread1.start(); thread2.start(); } }
測試結果:
當前線程名稱thread1:0 當前線程名稱thread1:1 當前線程名稱thread1:2 當前線程名稱thread1:3 當前線程名稱thread1:4 當前線程名稱thread2:5 當前線程名稱thread2:6 當前線程名稱thread2:7 當前線程名稱thread2:8 當前線程名稱thread2:9
結果分析:
當並發的2個線程(thread1,thread2)訪問同一個對象(sync)中的synchronized代碼塊時,同一個時刻只能被一個線程訪問;而另一個線程只能被堵塞,且必須要等待當前線程訪問完,並獲取已釋放對象鎖,才能訪問該同步的代碼塊。線程Thread1和thread2是互斥的,因為在執行synchronized代碼塊時會鎖定當前的對象(同一個對象sync),只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象。
那怎么才能引入不同的對象?
修改一下測試代碼:
public class SyncThreadTest { public static void main(String[] args) { SyncThread sync1 = new SyncThread(); SyncThread sync2 = new SyncThread(); Thread thread1 = new Thread(sync1, "thread1"); Thread thread2 = new Thread(sync2, "thread2"); thread1.start(); thread2.start(); } }
測試結果:
當前線程名稱thread1:0 當前線程名稱thread2:1 當前線程名稱thread2:3 當前線程名稱thread1:2 當前線程名稱thread1:4 當前線程名稱thread2:5 當前線程名稱thread2:6 當前線程名稱thread1:7 當前線程名稱thread2:8 當前線程名稱thread1:9
結果分析:
不是說一個線程執行synchronized代碼塊時其它的線程受阻塞嗎?為什么上面的例子中thread1和thread2同時在執行?
原因:這時創建了2個SyncThread對象分別為sync1和sync2,線程thread1執行的是對象sync1中的run;而線程thread2執行的是對象sync2中的run。由於2個不同的對象sync1和sync2,synchronized會為它們分別分配2個對象鎖,而這2把鎖是互不干擾,不形成互斥關系,所以會並行執行。
2、代碼塊中引入不同的對象,即指定對象上加鎖:(成員變量,類變量-靜態變量)
public class SynchronizedObjectDemo { //靜態的全局變量,類變量(可以是自定義對象) private static Object staticObject = new Object(); //非靜態變量,成員變量(可以是自定義對象) private Object o = new Object(); /** * 功能描述: <br> * 類變量的方法上鎖 */ public void printStaticObject(){ synchronized (staticObject) { //staticObject是靜態變量,即類變量上鎖 try { //調用線程休眠5秒,鎖競爭效果更加明顯 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }finally{ //輸出表示被調用 System.out.println(System.currentTimeMillis()+" printByStaticObj is called"); } } } /** * 功能描述: <br> * 在成員變量上鎖的方法 */ public void printObj(){ synchronized (o) { //o是成員變量 try { //調用線程休眠5秒,鎖競爭效果更加明顯 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }finally{ //輸出表示被調用 System.out.println(System.currentTimeMillis()+" printByObj is called"); } } } public static void main(String[] args) { //定義一個線程組 ThreadGroup tgroups = new ThreadGroup(Thread.currentThread().getThreadGroup(), "SynchronizedObjectDemo"); //循環,每次啟動一個線程 for (int i = 0; i < 5 ; i++) { //每一個線程,重新構造對象 final SynchronizedObjectDemo syncObj = new SynchronizedObjectDemo(); //構造新線程的時候把它加入到定義的線程組 Thread obj = new Thread(tgroups, new Runnable() { @Override public void run() { syncObj.printObj(); } }); obj.start(); } while (true) {//直到定義的線程組中的線程都終止才執行后續步驟 if(tgroups.activeCount() == 0){ for (int i = 0; i < 5; i++) { final SynchronizedObjectDemo syncObj = new SynchronizedObjectDemo(); //構造新線程的時候把它加入到定義的線程組 Thread obj = new Thread(tgroups, new Runnable() { @Override public void run() { syncObj.printStaticObject(); } }); obj.start(); } break; } } } }
分析:
從這個輸出結果可以看出,其實對於一個類變量加鎖就類似給靜態方法加鎖,需要注意的一點是靜態方法鎖定的是Class對象,而由於類變量全局只有一個,所以行為是類似靜態方法,但是鎖定的不是同一個對象。
同樣,而對於一個成員變量加鎖就類似給非靜態方法加鎖。
注意:如果把第一次for循環中的SynchronizedObjectDemo對象的構造放到第一次for循環之前的話,輸出的結果就會類似第二次for循環了,因為此時鎖的是同一個對象。
除此之外還可以有其他方式指定鎖對象:
方式一(作為方法參數):
public void method3(SomeObject obj){ //obj 鎖定的對象 synchronized(obj){ // todo } }
方式二(當沒有明確的對象作為鎖,只是想讓一段代碼同步時,可以創建一個特殊的對象來充當鎖,如上面的Object對象,但是並不是最經濟的方式):
class Test implements Runnable{ private byte[] lock = new byte[0]; // 特殊的instance變量 public void method(){ synchronized(lock) { // todo 同步代碼塊 } } public void run() { } }
說明:零長度的byte數組對象創建起來將比任何對象都經濟――查看編譯后的字節碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。
3、當一個線程訪問對象的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該對象中的非synchronized(this)同步代碼塊。
代碼示例:
public class Counts implements Runnable { private int count; public Counts(){ count = 0; } //synchronized方法 public void countAdd(){ synchronized (this) { for (int i = 0; i < 5; i++) { try { System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } } } } //非synchronized方法,未對count進行寫操作,所以可以不用synchronized public void countRead(){ for (int i = 0; i < 5; i++) { try { System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + count); Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } } } @Override public void run() { String threadName = Thread.currentThread().getName(); if (threadName.equals("A")) { countAdd(); } else if (threadName.equals("B")) { countRead(); } } }
測試代碼:
public class CountsTest { public static void main(String[] args) { Counts counter = new Counts(); Thread thread1 = new Thread(counter, "A"); Thread thread2 = new Thread(counter, "B"); thread1.start(); thread2.start(); } }
測試結果:
當前線程名稱A:0 當前線程名稱B:1 當前線程名稱A:1 當前線程名稱B:2 當前線程名稱A:2 當前線程名稱B:3 當前線程名稱A:3 當前線程名稱B:3 當前線程名稱B:4 當前線程名稱A:4
結果分析:
上面代碼中countAdd是一個synchronized的,countRead是非synchronized的。從上面的結果中可以看出,在線程A訪問synchronized的countAdd方法過程中,線程B可以同時訪問非synchronized的countRead方法。也就是說:一個線程訪問一個對象的synchronized代碼塊時,別的線程可以訪問該對象的非synchronized代碼塊,而不受阻塞。
1.2修飾方法
1.修飾非靜態方法
說明:Synchronized修飾一個方法很簡單,就是在方法的前面加synchronized,
public synchronized void method(){//todo};
synchronized修飾方法和修飾一個代碼塊類似,只是作用范圍不一樣,修飾代碼塊是大括號括起來的范圍,而修飾方法范圍是整個方法。如將【Demo1】中的run方法改成如下的方式,實現的效果一樣。
Synchronized作用於整個方法的寫法。
寫法一:
public synchronized void method(){ // todo }
寫法二:
public void method(){ synchronized(this) { // todo } }
寫法一修飾的是一個方法,寫法二修飾的是一個代碼塊,但寫法一與寫法二是等價的,都是鎖定了整個方法時的內容。
注意點:
(1)synchronized關鍵字不能繼承。
雖然可以使用synchronized來定義方法,但synchronized並不屬於方法定義的一部分。因此,synchronized關鍵字不能被繼承。
如果在父類中的某個方法使用了synchronized關鍵字,而在子類中覆蓋了這個方法,在子類中的這個方法默認情況下並不是同步的,而必須顯式地在子類的這個方法中加上synchronized關鍵字才可以。
當然,還可以在子類方法中調用父類中相應的方法,這樣雖然子類中的方法不是同步的,但子類調用了父類的同步方法,因此,子類的方法也就相當於同步了。
示例代碼如下:
1)在子類方法中加上synchronized關鍵字
父類:
class Parent { public synchronized void method() { } }
子類:
class Child extends Parent { public synchronized void method() { } }
2)在子類方法中調用父類的同步方法
父類:
class Parent { public synchronized void method() { } }
子類:
class Child extends Parent { public void method() { super.method(); } }
(2)在定義接口方法時不能使用synchronized關鍵字。
(3)構造方法不能使用synchronized關鍵字,但可以使用synchronized代碼塊來進行同步。
2.修飾靜態方法
代碼示例:
public class StaticMethod implements Runnable{ private static int count; public StaticMethod(){ count = 0; } //靜態方法 public synchronized static void getCounts(){ //屬於類級別的鎖,對於不同對象不可同時進入同一個類 for (int i = 0; i < 5; i++) { try { System.out.println("當前線程名稱" + Thread.currentThread().getName() + ":" + (count++)); Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } } } @Override public synchronized void run() { //屬於方法級別的鎖,對於不同對象可同時進入 getCounts(); } }
測試代碼:
public class StaticMethodTest { public static void main(String[] args) { StaticMethod sync1 = new StaticMethod(); StaticMethod sync2 = new StaticMethod(); Thread thread1 = new Thread(sync1, "thread1"); Thread thread2 = new Thread(sync2, "thread2"); thread1.start(); thread2.start(); } }
結果分析:
sync1和sync2是StaticMethod的兩個對象,但在thread1和thread2並發執行時卻保持了線程同步。這是因為run中調用了靜態方法getCounts,而靜態方法是屬於類的,所以sync1和sync2相當於用了同一把鎖。這與非靜態方法是不同的。
1.3修飾一個類
說明:對於類鎖,有幾種方式:(1)synchronized修飾靜態方法;(2)synchronized修飾靜態變量;(3)synchronized代碼塊中直接引入類名稱。前2種方式,前面已經做了詳細的解釋,這里主要解釋第三種。
代碼示例:
class ClassName { public void method() { synchronized(ClassName.class) { // todo } } }
synchronized作用於一個類ClassName時,是給這個類ClassName加鎖,ClassName的所有對象用的是同一把鎖。
參考資料:
http://www.importnew.com/21866.html
