synchronized使用和原理全解
synchronized是Java中的關鍵字,是一種同步鎖。它修飾的對象有以下幾種:
修飾一個方法
被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調用這個方法的對象;
修飾一個靜態的方法
其作用的范圍是整個靜態方法,作用的對象是這個類的所有對象;
修飾一個代碼塊
被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象;
修飾一個類
其作用的范圍是synchronized后面括號括起來的部分,作用主的對象是這個類的所有對象。
?
修飾一個方法
被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調用這個方法的對象;
如果多個線程訪問同一個對象的實例變量,可能出現非線程安全問題。
例子:a線程set后sleep2000ms,看b線程是否可以趁機set,造成非線程安全
HasSelfPrivateNum.java:
package service;public class HasSelfPrivateNum {private int num = 0;public void addI(String username) {try {if (username.equals("a")) {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:
package extthread;import service.HasSelfPrivateNum;public class ThreadA extends Thread {private HasSelfPrivateNum numRef;public ThreadA(HasSelfPrivateNum numRef) {super();this.numRef = numRef;}@Overridepublic void run() {super.run();numRef.addI("a");}}B:
package extthread;import service.HasSelfPrivateNum;public class ThreadB extends Thread {private HasSelfPrivateNum numRef;public ThreadB(HasSelfPrivateNum numRef) {super();this.numRef = numRef;}@Overridepublic void run() {super.run();numRef.addI("b");}}run:
package test;import service.HasSelfPrivateNum; import extthread.ThreadA; import extthread.ThreadB;public class Run {public static void main(String[] args) {HasSelfPrivateNum numRef = new HasSelfPrivateNum();ThreadA athread = new ThreadA(numRef);athread.start();ThreadB bthread = new ThreadB(numRef);bthread.start();} }結果:a線程set后sleep2000ms,b線程可以趁機set,造成非線程安全
這時我們使用synchronized修飾addI方法:
package service;public class HasSelfPrivateNum {private int num = 0;synchronized public void addI(String username) {try {if (username.equals("a")) {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();}}}結果:B不能set了,說明線程安全。
注意:我們取得的是對象鎖,也就是說,一個對象一個鎖,而不是鎖住整個類或者代碼或者方法。
例子:兩個對象兩個鎖
myobject.java
打印名字后sleep,最后打印end
package extobject;public class MyObject {synchronized public void methodA() {try {System.out.println("begin methodA threadName="+ Thread.currentThread().getName());Thread.sleep(5000);System.out.println("end");} catch (InterruptedException e) {e.printStackTrace();}}}A:
package extthread;import extobject.MyObject;public class ThreadA extends Thread {private MyObject object;public ThreadA(MyObject object) {super();this.object = object;}@Overridepublic void run() {super.run();object.methodA();}}B:
package extthread;import extobject.MyObject;public class ThreadB extends Thread {private MyObject object;public ThreadB(MyObject object) {super();this.object = object;}@Overridepublic void run() {super.run();object.methodA();} }RUN:
package test.run;import extobject.MyObject; import extthread.ThreadA; import extthread.ThreadB;public class Run {public static void main(String[] args) {MyObject object = new MyObject();ThreadA a = new ThreadA(object);a.setName("A");ThreadB b = new ThreadB(object);b.setName("B");a.start();b.start();}}結果:兩個對象互不影響,各自運行各自上鎖
其它方法被調用是什么效果呢?
之前做實驗是因為怕大家不理解知識,現在大家已經有了一定的基礎,這個結論不再做實驗。
結論:
如果A線程持有x對象的鎖,B線程不可調用synchronized修飾的方法,但是可以異步調用沒有被synchronized修飾的方法
?
synchronized具有鎖重入功能,也就是說一個線程獲得鎖,再次請求是可以再次得到對象的鎖的
下面做實驗驗證這個結論:
service.java
package myservice;public class Service {synchronized public void service1() {System.out.println("service1");service2();}synchronized public void service2() {System.out.println("service2");service3();}synchronized public void service3() {System.out.println("service3");}}thread:
package extthread;import myservice.Service;public class MyThread extends Thread {@Overridepublic void run() {Service service = new Service();service.service1();}}run:
package test;import extthread.MyThread;public class Run {public static void main(String[] args) {MyThread t = new MyThread();t.start();} }結果驗證了上面的結論:
注:在父子類之間同樣適用,不再做實驗
?
但是如果一個線程出現了異常,難道就永遠鎖住了資源嗎?其實不是的,出現異常自動釋放鎖。
實驗:讓a鎖住對象后出現異常,看b是否可以拿到鎖,代碼不再展示。
結果:b可以正常執行。
?
修飾一個靜態的方法
?
其作用的范圍是整個靜態方法,作用的對象是這個類的所有對象;
也就是說整個類就一個鎖,這也和靜態的概念相符(靜態方法和屬性是屬于類的而不是具體一個對象的)
讓我們來驗證這個結論:
service:
package service;public class Service {synchronized public static void printA() {try {System.out.println("線程名稱為:" + Thread.currentThread().getName()+ "在" + System.currentTimeMillis() + "進入printA");Thread.sleep(3000);System.out.println("線程名稱為:" + Thread.currentThread().getName()+ "在" + System.currentTimeMillis() + "離開printA");} catch (InterruptedException e) {e.printStackTrace();}}synchronized public static void printB() {System.out.println("線程名稱為:" + Thread.currentThread().getName() + "在"+ System.currentTimeMillis() + "進入printB");System.out.println("線程名稱為:" + Thread.currentThread().getName() + "在"+ System.currentTimeMillis() + "離開printB");}}a:
package extthread;import service.Service;public class ThreadA extends Thread {private Service service;public ThreadA(Service service) {super();this.service = service;}@Overridepublic void run() {service.printA();} }b:
package extthread;import service.Service;public class ThreadB extends Thread {private Service service;public ThreadB(Service service) {super();this.service = service;}@Overridepublic void run() {service.printB();} }run:
package test;import service.Service; import extthread.ThreadA; import extthread.ThreadB;public class Run {public static void main(String[] args) {Service service1 = new Service();Service service2 = new Service();ThreadA a = new ThreadA(service1);a.setName("A");a.start();ThreadB b = new ThreadB(service2);b.setName("B");b.start();}}結果:
但是,請注意一個顯而易見的結論:a線程訪問synchronized修飾的靜態方法時,b線程可以訪問synchronized修飾的非靜態方法,原因也很容易想到:靜態方法是屬于類的,普通方法是屬于對象本身的,所以一個是對象鎖,一個是class鎖,不會影響。
為了驗證這個結論,我們做實驗看看結果。
service:
AB為靜態的,C普通的。
package service;public class Service {synchronized public static void printA() {try {System.out.println("線程名稱為:" + Thread.currentThread().getName()+ "在" + System.currentTimeMillis() + "進入printA");Thread.sleep(3000);System.out.println("線程名稱為:" + Thread.currentThread().getName()+ "在" + System.currentTimeMillis() + "離開printA");} catch (InterruptedException e) {e.printStackTrace();}}synchronized public static void printB() {System.out.println("線程名稱為:" + Thread.currentThread().getName() + "在"+ System.currentTimeMillis() + "進入printB");System.out.println("線程名稱為:" + Thread.currentThread().getName() + "在"+ System.currentTimeMillis() + "離開printB");}synchronized public void printC() {System.out.println("線程名稱為:" + Thread.currentThread().getName() + "在"+ System.currentTimeMillis() + "進入printC");System.out.println("線程名稱為:" + Thread.currentThread().getName() + "在"+ System.currentTimeMillis() + "離開printC");}}a:?
package extthread;import service.Service;public class ThreadA extends Thread {private Service service;public ThreadA(Service service) {super();this.service = service;}@Overridepublic void run() {service.printA();}}b:
package extthread;import service.Service;public class ThreadB extends Thread {private Service service;public ThreadB(Service service) {super();this.service = service;}@Overridepublic void run() {service.printB();} }c:
package extthread;import service.Service;public class ThreadC extends Thread {private Service service;public ThreadC(Service service) {super();this.service = service;}@Overridepublic void run() {service.printC();} }?run:
package test;import service.Service; import extthread.ThreadA; import extthread.ThreadB; import extthread.ThreadC;public class Run {public static void main(String[] args) {Service service = new Service();ThreadA a = new ThreadA(service);a.setName("A");a.start();ThreadB b = new ThreadB(service);b.setName("B");b.start();ThreadC c = new ThreadC(service);c.setName("C");c.start();}}結果:
說明:驗證了結論,因為AB同步執行,C異步執行。
?
修飾一個代碼塊
被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象;(而不一定是本對象了)
(synchronized方法是對當前對象加鎖,synchronized代碼塊是對某個對象加鎖)
用synchronized聲明方法是有弊端的,比如A調用同步方法執行一個很長時間的任務,這時B就必須等待,有時候這種長時間等待是低效率且沒有必要的,這時我們就要認識一下synchronized同步代碼塊了。
格式是這樣的:synchronized(類名){......}
我們先來認識第一種用法來體會修飾代碼塊的好處:
synchronized(this)同步代碼塊:a調用相關代碼后,b對其它synchronized方法和synchronized(this)同步代碼塊調用會阻塞。但是沒有被synchronized修飾的代碼就得以執行,不像之前修飾方法那么死板了。
我們來看一個例子:
第一個循環異步,第二個循環同步:
package mytask;public class Task {public void doLongTimeTask() {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));}}} }thread1:
package mythread;import mytask.Task;public class MyThread1 extends Thread {private Task task;public MyThread1(Task task) {super();this.task = task;}@Overridepublic void run() {super.run();task.doLongTimeTask();}}thread2:
package mythread;import mytask.Task;public class MyThread2 extends Thread {private Task task;public MyThread2(Task task) {super();this.task = task;}@Overridepublic void run() {super.run();task.doLongTimeTask();}}run:
package test;import mytask.Task; import mythread.MyThread1; import mythread.MyThread2;public class Run {public static void main(String[] args) {Task task = new Task();MyThread1 thread1 = new MyThread1(task);thread1.start();MyThread2 thread2 = new MyThread2(task);thread2.start();} }結果:
在非同步代碼塊時,是交叉打印的
同步代碼塊時排隊執行:
另一個用法:
synchronized(非this)同步代碼塊(也就是說將任意對象作為對象監視器):
格式:synchronized(非this對象x){......}
1、當多個線程持有的對象監聽器為同一個對象時,依舊是同步的,同一時間只有一個可以訪問,
2、但是對象不同,執行就是異步的。
這樣有什么好處呢?
(因為如果一個類有很多synchronized方法或synchronized(this)代碼塊,還是會影響效率,這時用synchronized(非this)同步代碼塊就不會和其它鎖this同步方法爭搶this鎖)
實驗
第一點我們就不驗證了,因為體現不出它的好處,我們驗證第二點:
service:
一個同步非this代碼塊,一個同步方法:
package service;public class Service {private String anyString = new String();public void a() {try {synchronized (anyString) {System.out.println("a begin");Thread.sleep(3000);System.out.println("a end");}} catch (InterruptedException e) {e.printStackTrace();}}synchronized public void b() {System.out.println("b begin");System.out.println("b end");}}a:
package extthread;import service.Service;public class ThreadA extends Thread {private Service service;public ThreadA(Service service) {super();this.service = service;}@Overridepublic void run() {service.a();}}b:
package extthread;import service.Service;public class ThreadB extends Thread {private Service service;public ThreadB(Service service) {super();this.service = service;}@Overridepublic void run() {service.b();}}?run:
package test;import service.Service; import extthread.ThreadA; import extthread.ThreadB;public class Run {public static void main(String[] args) {Service service = new Service();ThreadA a = new ThreadA(service);a.setName("A");a.start();ThreadB b = new ThreadB(service);b.setName("B");b.start();}}結果:
確實是異步的。
最后一個要注意的點:我們知道synchronized(非this對象x){......}是將對象x監視,這也就意味著當線程a調用這段代碼時,線程b調用類x中的同步方法和代碼塊也會是同步的效果(阻塞)。
為了讓大家更明白,做最后一個例子:
首先創建一個有靜態方法的類:
package test2.extobject;public class MyObject {synchronized public 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());} }然后用?synchronized(非this對象x){......}的形式引用它,并進入這個代碼塊,然后看看這時這個靜態方法是否可以被調用。
service:作用是synchronized(object)
package test2.service;import test2.extobject.MyObject;public class Service {public void testMethod1(MyObject object) {synchronized (object) {try {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();}}}}threadA:
package test2.extthread;import test2.extobject.MyObject; import test2.service.Service;public class ThreadA extends Thread {private Service service;private MyObject object;public ThreadA(Service service, MyObject object) {super();this.service = service;this.object = object;}@Overridepublic void run() {super.run();service.testMethod1(object);}}?threadB:
package test2.extthread;import test2.extobject.MyObject;public class ThreadB extends Thread {private MyObject object;public ThreadB(MyObject object) {super();this.object = object;}@Overridepublic void run() {super.run();object.speedPrintString();} }run:
?
package test2.run;import test2.extobject.MyObject; import test2.extthread.ThreadA; import test2.extthread.ThreadB; import test2.service.Service;public class Run {public static void main(String[] args) throws InterruptedException {Service service = new Service();MyObject object = new MyObject();ThreadA a = new ThreadA(service, object);a.setName("a");a.start();Thread.sleep(100);ThreadB b = new ThreadB(object);b.setName("b");b.start();}}結果:
?
是同步的。
當然,把修飾方法改為修飾代碼塊也是一樣不能被執行的。
Synchronized的作用主要有三個:(1)確保線程互斥的訪問同步代碼(2)保證共享變量的修改能夠及時可見(3)有效解決重排序問題。
synchronized底層原理
在 Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為監視器鎖(monitor)是依賴于底層的操作系統來實現,Java 的線程是映射到操作系統的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高。
在 Java 6 之后從 JVM 層面對synchronized 較大優化,鎖的實現引入了如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
?
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令。
其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。
當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因) 的持有權.當計數器為0則可以成功獲取,獲取后將鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令后,將鎖計數器設為0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的是ACC_SYNCHRONIZED標識,該標識指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。但是原理其實都是類似的。具體的實現是操作系統的知識可以去翻我操作系統的文章。
鎖詳解
鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
自旋:當有個線程A去請求某個鎖的時候,這個鎖正在被其它線程占用,但是線程A并不會馬上進入阻塞狀態,而是循環請求鎖(自旋)。這樣做的目的是因為很多時候持有鎖的線程會很快釋放鎖的,線程A可以嘗試一直請求鎖,沒必要被掛起放棄CPU時間片,因為線程被掛起然后到喚醒這個過程開銷很大,當然如果線程A自旋指定的時間還沒有獲得鎖,仍然會被掛起。
自適應性自旋:自適應性自旋是自旋的升級、優化,自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態決定。例如線程如果自旋成功了,那么下次自旋的次數會增多,因為JVM認為既然上次成功了,那么這次自旋也很有可能成功,那么它會允許自旋的次數更多。
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。
偏向鎖的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那么偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不用做了。偏向鎖默認是開啟的,也可以關閉。
?
總結
以上是生活随笔為你收集整理的synchronized使用和原理全解的全部內容,希望文章能夠幫你解決所遇到的問題。