JAVA多執行緒-基礎篇-synchronized

程式設計師生態圈發表於2017-11-23

一、為什麼需要synchronized?

因為在併發的場景下,是需要保證執行緒安全的,產生執行緒安全問題的原因主要有兩個:一是臨界資源,二是多執行緒訪問臨界資源,為了解決這兩個問題,需要保證同一時刻只能有一個執行緒訪問臨界資源,synchronized就是為這個而生的,同時它還能保證記憶體可見性。

二、作用

synchronized我們通常翻譯為“同步”,一句話概括其作用:防止資源衝突。具體來說:當任務執行由synchronized保護的程式碼片段時,它首先會嘗試獲得物件的鎖,獲取到鎖以後,開始執行同步程式碼塊,然後退出,否則阻塞。

舉個例子:早晨到公司後,上廁所排號是常態。假設衛生間只有一個坐便器,即只能一個人進去方便,先進去的人肯定要把門鎖上(獲取鎖),然後.....此處省略500字,方便完了再開啟門(釋放鎖),後面排隊的人再進去,當然,這裡我說的都是文明員工,大家不會破門而入
三、原理

每個物件都關聯一個monitor(物件),執行緒在執行時,首先要獲得monitor的許可權(即物件鎖)才能執行,例如:如果兩個執行緒1和 2 都需要獲取obj的物件鎖,在某一時刻,執行緒1獲得到了obj的物件鎖,執行緒2如果也想獲得obj的物件鎖,只能等待,直到執行緒1釋放鎖以後執行緒2才有可能獲得到obj 的鎖進而執行任務

3.1 底層位元組碼

程式碼示例:
public class SynchronizedDemo1 {
public SynchronizedDemo1() {
}
//加鎖方法
public synchronized void test1() {
System.out.println("this is a synchronized method");
}
public void test2() {
//鎖程式碼塊
synchronized(this) {
System.out.println("this is a synchronized block");
}
}
public static void main(String[] args) {
SynchronizedDemo1 s1 = new SynchronizedDemo1();
s1.test1();
s1.test2();
}
}
上面程式通過javap -c得到的位元組碼結果如下:

javap -verbose得到的結果如下:

可以看到,test1方法的位元組碼是通過隱式(ACC_SYNCHRONIZED標誌)加鎖方法實現同步的,test2()方法是通過monitorenter和moniterexit指令來實現加鎖和釋放鎖的;

四、用法

java中每個物件都可以作為鎖,因此鎖是依賴物件存在的。根據鎖針對的物件不同,分為例項鎖和類鎖:

例項鎖:鎖針對某一個物件例項,用法主要有兩種:
a.普通方法加鎖,此時的鎖是當前物件例項,即該加鎖方法所屬的物件例項
public synchronized void method(){}
b.程式碼塊加鎖
synchronized(obj){
dosomething...
}
類鎖:鎖針對的是類,而不是具體例項,該類的所有例項共享該鎖,用法如下:
public static synchronized void method(){}
五、程式碼示例

5.1 示例1

public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Runnable r = new MyRunnable();
Thread t1 = new Thread(r, "t1");
Thread t2 = new Thread(r, "t2");
t1.start();
t2.start();
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("分割線---------");
Thread thread1 = new MyThread("thread1");
Thread thread2 = new MyThread("thread2");
thread1.start();
thread2.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
synchronized (this) {
try {
for (int i = 0; i < 4; i++) {
TimeUnit.MILLISECONDS.sleep(10);
System.out.println(Thread.currentThread().getName() + "--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
synchronized (this) {
try {
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(10);
System.out.println(Thread.currentThread().getName() + "--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

程式輸出:
t2--0
t2--1
t2--2
t2--3
t1--0
t1--1
t1--2
t1--3
分割線---------
thread2--0
thread1--0
thread2--1
thread1--1
thread2--2
thread1--2
thread2--3
thread1--3
thread1--4
thread2--4
5.1.1 結果說明

主執行緒中啟動4個執行緒:t1,t2,thread1,thread2.先說t1,t2,這兩個執行緒共享r(MyRunnable)例項,MyRunnable中的run方法是加鎖方法,當一個執行緒執行run方法時,另外一個執行緒只能等待;再說thread1和thread2,這兩個執行緒沒有共享任務例項,是兩個不同的MyThread例項,雖然MyThread的run方法也是加鎖方法,但是兩個執行緒交替執行,並沒有出現等待

5.1.2 總結

當一個執行緒訪問某物件的同步方法或者同步程式碼塊時,其他執行緒如果試圖訪問“該物件”的“該同步方法或者程式碼塊”將被阻塞;如果兩個執行緒獲得不同物件的同步鎖,將不會阻塞

5.2 示例2

public class SynchronizedExample2 {
public static void main(String[] args) {
method();
}
public static void method() {
SynchronizedExample2 se = new SynchronizedExample2();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
se.syncMethod();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
se.nonSyncMethod();
}
}, "t2");
t1.start();
t2.start();
}
public void syncMethod() {
synchronized (this) {
try {
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(50);
System.out.println(Thread.currentThread().getName() + "--syncMethod--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void nonSyncMethod() {
try {
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(50);
System.out.println(Thread.currentThread().getName() + "--nonSyncMethod--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

結果輸出:
t1--syncMethod--0
t2--nonSyncMethod--0
t1--syncMethod--1
t2--nonSyncMethod--1
t1--syncMethod--2
t2--nonSyncMethod--2
t1--syncMethod--3
t2--nonSyncMethod--3
t2--nonSyncMethod--4
t1--syncMethod--4
5.2.1 結果說明

主執行緒中啟動兩個執行緒t1,t2,這兩個執行緒分別訪問同一個例項se的兩個方法,一個是同步方法,一個是普通方法,結果顯示兩個方法可以併發執行,沒有發生阻塞

5.2.2 總結

當一個執行緒訪問某物件的同步方法時,其他執行緒可以同時訪問“該物件”的非同步方法

5.3 示例3

public class SynchronizedExample3 {
public static void main(String[] args) {
method();
}

public static void method() {
SynchronizedExample3 se = new SynchronizedExample3();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
se.syncMethod1();
}
}, "t1");

    Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
          se.syncMethod2();
      }
    }, "t2");
    t1.start();
    t2.start();複製程式碼

}

public void syncMethod1() {
synchronized (this) {
try {
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(50);
System.out.println(Thread.currentThread().getName() + "--syncMethod1--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void syncMethod2() {
synchronized (this) {
try {
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(50);
System.out.println(Thread.currentThread().getName() + "--syncMethod2--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

結束輸出:
t2--syncMethod2--0
t2--syncMethod2--1
t2--syncMethod2--2
t2--syncMethod2--3
t2--syncMethod2--4
t1--syncMethod1--0
t1--syncMethod1--1
t1--syncMethod1--2
t1--syncMethod1--3
t1--syncMethod1--4
5.3.1 結果說明

例項物件se中有2個加鎖方法,主執行緒中啟動2個執行緒t1,t2,分別執行se的2個加鎖方法,從結果看,只有執行緒2執行完加鎖方法2後,另外執行緒1才能開始執行加鎖方法1,即兩個執行緒沒有併發執行,發生阻塞

5.3.2 總結

當一個執行緒訪問某物件的同步方法或程式碼塊時,其他執行緒訪問“該物件”的其他同步方法或者程式碼塊將被阻塞

5.4 示例

public class SynchronizedExample4 {

public static void main(String[] args) {
test();
}

public static void test() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
syncMethod1();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
syncMethod2();
}
}, "t2");
t1.start();
t2.start();
}

public static synchronized void syncMethod1() {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + "--syncMethod1--" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static synchronized void syncMethod2() {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + "--syncMethod2--" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
結果輸出:
t1--syncMethod1--0
t1--syncMethod1--1
t1--syncMethod1--2
t1--syncMethod1--3
t1--syncMethod1--4
t2--syncMethod2--0
t2--syncMethod2--1
t2--syncMethod2--2
t2--syncMethod2--3
t2--syncMethod2--4
5.4.1 結果說明

兩個同步方法都是static的,所以兩個執行緒中的加鎖方法相當於執行SynchronizedExample4.syncMethod1()和SynchronizedExample4.syncMethod2(),從結果看,兩個執行緒沒有併發執行,發生阻塞

5.4.2 總結

當同步方法由static修飾時(類鎖),多個執行緒執行該同步方法或者其他static同步方法會發生阻塞

5.5 示例5

public class SynchronizedExample5 {
public static void main(String[] args) {
SynchronizedExample5 se = new SynchronizedExample5();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
method1();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
se.method2();
}
}, "t2");
t1.start();
t2.start();
}

public static synchronized void method1() {
try {
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + "--method1--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void method2() {
synchronized (this) {
try {
for (int i = 0; i < 5; i++) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread().getName() + "--method2--" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
結果輸出:
t1--method1--0
t2--method2--0
t1--method1--1
t2--method2--1
t2--method2--2
t1--method1--2
t1--method1--3
t2--method2--3
t2--method2--4
t1--method1--4
5.5.1 結果說明

主執行緒啟動兩個執行緒,分別訪問static同步方法(類鎖)和非static同步程式碼塊(例項鎖),兩個執行緒併發執行,沒有發生阻塞

5.5.2 總結

當多執行緒在同時訪問某物件的類鎖和例項鎖時,是可以併發訪問的

六、全篇總結

synchronized的使用準則:
1.當一個執行緒訪問某物件的同步方法或者同步程式碼塊時,其他執行緒如果試圖訪問“該物件”的“該同步方法或者程式碼塊”將被阻塞;如果兩個執行緒獲得不同物件的同步鎖,將不會阻塞
2.當一個執行緒訪問某物件的同步方法時,其他執行緒可以同時訪問“該物件”的非同步方法
3.當一個執行緒訪問某物件的同步方法或程式碼塊時,其他執行緒訪問“該物件”的其他同步方法或者程式碼塊將被阻塞
4.當同步方法由static修飾時(類鎖),多個執行緒執行該同步方法或者其他static同步方法會發生阻塞
5.當多執行緒在同時訪問某物件的類鎖和例項鎖時,是可以併發訪問的

相信這些例子可以幫助大家學習Synchronized!

轉載請註明作者及出處,並附上鍊接www.jianshu.com/u/ada8c4ee3…

作者:x_coder
連結:www.jianshu.com/p/dca064fe5…
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關文章