上一篇文章 并發(fā)基礎(chǔ)知識掃盲 講了一些 java 中 并發(fā)相關(guān)的基礎(chǔ)性的東西,這篇來了解下同步中常使用的關(guān)鍵字 synchronized。
synchronized 關(guān)鍵字是隨著 Java 的誕生就有的的,它對于開發(fā)者來說,使用起來非常方便,無需關(guān)心底層的復(fù)雜實現(xiàn)。但是在使用過程中開發(fā)者往往擔(dān)心 synchronized 帶來的性能問題,認(rèn)為它太重了,獲得鎖和釋放鎖的確會帶來性能上的消耗。在 Java SE 1.6 之后,synchronized 進(jìn)行了很大的優(yōu)化,引入了偏性鎖和輕量級鎖,來減少獲取鎖和釋放鎖帶來的性能問題,所以一般要求同步操作時,建議使用 synchronized。
1. synchronized 用法
synchronized 使用主要就是 3 種形式:
普通同步方法,鎖是當(dāng)前實例對象
靜態(tài)同步方法,鎖是當(dāng)前類的 Class 對象
同步代碼塊,鎖是 synchronized 括號中的對象
下面看下不同步的情況:
public class ThreadSync {
private static class NoSyncRunnable implements Runnable {
private int count = 1000;
@Override
public void run() {
while (count >= 0) {
System.out.println(Thread.currentThread().getName() + " --- " + count);
count--;
}
}
}
public static void main(String[] args) throws InterruptedException { testNoSyncMethod();
}
private static void testNoSyncMethod() throws InterruptedException {
NoSyncRunnable noSyncRunnable = new NoSyncRunnable();
Thread thread1 = new Thread(noSyncRunnable);
Thread thread2 = new Thread(noSyncRunnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
這個未加同步的例子中,我們期望 count 能夠從 1000 一直減少到 0,但實際結(jié)果中可能會出現(xiàn)一個線程打印的值,會大于或者等于前一個線程給出的值。出現(xiàn)這種情況主要是由于緩存的原因,在上一篇文章 并發(fā)基礎(chǔ)知識掃盲 已經(jīng)提到,由于Java 虛擬機(jī)的工作內(nèi)存形式,線程首先會從工作內(nèi)存中取值,所以讀取值和修改值是基于工作內(nèi)存,并未及時更新到主內(nèi)存中,所以不能保證線程之間的可見性。要想保證線程間同步,需要滿足同步的 3 個條件:原子性、可見性和順序性,synchronized 能夠保證這三個條件。
Thread-1 --- 1000
Thread-1 --- 999
Thread-1 --- 998
Thread-1 --- 997
Thread-1 --- 996
Thread-1 --- 995
Thread-1 --- 994
Thread-1 --- 993
Thread-1 --- 992
Thread-0 --- 1000
Thread-0 --- 990
Thread-0 --- 989
Thread-0 --- 988
Thread-0 --- 987
Thread-0 --- 986
Thread-0 --- 985
Thread-0 --- 984
...
使用同步操作
private static class SyncRunnable implements Runnable {
private static int count = 10;
// 特殊的instance變量
// 零長度的byte數(shù)組對象創(chuàng)建起來將比任何對象都經(jīng)濟(jì)――查看編譯后的字節(jié)碼:
// 生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。
private final byte[] lock = new byte[0];
@Override
public void run() {
// 普通同步方法
// countMethod0();
// 同步代碼塊 this 鎖
// countMethod1();
// 同步代碼塊 非 this 鎖
// countMethod2();
// 靜態(tài)同步方法
// countMethod3();
// 同步代碼塊 類的 Class 對象
countMethod4();
}
/**
* 同步方法
*/
private synchronized void countMethod0() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " --- " + count);
count--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 同步代碼塊(this 鎖)
*/
private void countMethod1() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " --- " + count);
count--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 同步代碼塊(非 this 鎖)
*/
private void countMethod2() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " --- " + count);
count--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 靜態(tài)同步方法
*/
private static synchronized void countMethod3() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " --- " + count);
count--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 同步代碼塊(類的 Class 對象鎖)
*/
private static void countMethod4() {
synchronized (SyncRunnable.class) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " --- " + count);
count--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
上述幾個操作都能夠保證線程之間的同步操作,其中,
(1)countMethod0 是普通同步方法,它獲取的當(dāng)前 SyncRunnable 實例對象的鎖,countMethod1 是同步代碼塊,同樣是當(dāng)前 SyncRunnable 實例對象的鎖,并且 countMethod1 和 countMethod0 等同,因為 countMethod1 同步代碼塊包裹的范圍是整個方法。同步代碼塊盡可能的保證同步位置的最小化,這樣能夠提高線程間的運(yùn)行效率,使用更加靈活。
(2)方法 countMethod2 是一個同步代碼塊,獲取的鎖對象是非 this 鎖,注釋中給出一個提示,使用 byte[] 對象只需 3 條操作碼,而Object lock = new Object() 則需要 7 行操作碼,所以使用 byte[] 對象 更加經(jīng)濟(jì)。使用非 this 對象鎖的優(yōu)勢,如果是同步方法或者 鎖是 this 的代碼塊,會阻塞其他同步方法和 this 的代碼塊,而 非 this 鎖不會阻塞同步方法和 this 的代碼塊,因為是不同的鎖對象。
(3)方法 countMethod3 是一個靜態(tài)同步方法,它的鎖是 SyncRunnable 的 Class 對象鎖,相當(dāng)于 countMethod4,屬于類的同步方法,這個鎖和普通同步方法的鎖是不一樣的。
2. synchronized 使用細(xì)節(jié)
上面演示了 synchronized 的 3 種用法,下面看看 synchronized 使用過程中的幾種阻塞情況。
阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時的狀態(tài)。阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起。調(diào)用線程只有在得到結(jié)果之后才會返回。非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會阻塞當(dāng)前線程。
使用 synchronized 同步時,一個線程沒有獲取到鎖,就是處于阻塞狀態(tài)。
這里演示的例子,通過建立一個同步類,里面包括各種同步方法,普通同步方法,同步代碼塊(this 鎖和非 this 鎖)、靜態(tài)同步方法,然后通過建立不同的線程,來演示這些方法之間組合時的阻塞情況。
同步類 SyncFun,包含多種同步方法。
public class SyncFun {
private final Object lock = new Object();
public synchronized void print0() {
int i = 0;
while (i < 10) {
System.out.println("SyncFun - 同步方法 - " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void print1() {
int i = 0;
while (i < 10) {
System.out.println("SyncFun - 同步方法 - " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void print2() {
synchronized (this) {
int i = 0;
while (i < 10) {
System.out.println("SyncFun - 同步代碼塊 this - " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void print3() {
synchronized (lock) {
int i = 0;
while (i < 5) {
System.out.println("SyncFun - 同步代碼塊 lock- " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void print4(Object o) {
synchronized (o) {
int i = 0;
while (i < 10) {
System.out.println("SyncFun - 同步方法 參數(shù) lock - " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static synchronized void print5() {
int i = 0;
while (i < 10) {
System.out.println("SyncFun - 類同步方法 - " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static synchronized void print6() {
int i = 0;
while (i < 10) {
System.out.println("SyncFun - 類同步方法 - " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void print7() {
synchronized (SyncFun.class) {
int i = 0;
while (i < 10) {
System.out.println("SyncFun - 類同步代碼塊- 類鎖 - " + Thread.currentThread().getName() + " - " + i++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
這里就直接把代碼貼出來,主要針對普通同步方法,不同對象鎖的同步代碼塊,和靜態(tài)同步方法這幾種情況的組合來進(jìn)行實驗。
public class ThreadSync1 {
private static class Runnable0 implements Runnable {
private SyncFun syncFun;
public Runnable0(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
syncFun.print0();
}
}
private static class Runnable1 implements Runnable {
private SyncFun syncFun;
public Runnable1(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
syncFun.print1();
}
}
private static class Runnable2 implements Runnable {
private SyncFun syncFun;
public Runnable2(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
syncFun.print2();
}
}
private static class Runnable3 implements Runnable {
private SyncFun syncFun;
public Runnable3(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
syncFun.print3();
}
}
private static class Runnable4 implements Runnable {
private SyncFun syncFun;
private static final Object o = new Object();
public Runnable4(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
syncFun.print4(o);
}
}
private static class Runnable5 implements Runnable {
private SyncFun syncFun;
public Runnable5(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
SyncFun.print5();
}
}
private static class Runnable6 implements Runnable {
private SyncFun syncFun;
public Runnable6(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
SyncFun.print6();
}
}
private static class Runnable7 implements Runnable {
private SyncFun syncFun;
public Runnable7(SyncFun syncFun) {
this.syncFun = syncFun;
}
@Override
public void run() {
syncFun.print7();
}
}
public static void main(String[] args) throws InterruptedException {
// (1)不同對象的同一個同步方法- 不阻塞
// test1_0();
// (2)同一個對象的不同的同步方法- 阻塞
// test1_1();
// (3)訪問同一個同步方法- 阻塞
// test1_2();
// (4)訪問同步方法 和 this 鎖- 阻塞
// test1_3();
// (5)訪問同一個非 this 鎖的同步代碼塊 - 阻塞
// test1_4();
// (6)同步方法和非 this 對象 - 不阻塞
// test1_5();
// (7)同步方法和參數(shù) lock,和非 this 對象鎖一樣 - 不阻塞
// test1_6();
// (8)訪問不同靜態(tài)同步方法, - 阻塞
// test6();
// (9)訪問靜態(tài)同步方法和該類對象的同步代碼塊(類鎖)- 阻塞
// test7();
// (10)同步方法和靜態(tài)同步方法, - 不阻塞
// test8();
// (11)不同對象訪問不同靜態(tài)同步方法, - 阻塞
test9();
}
private static void test1_0() throws InterruptedException {
SyncFun syncFun0 = new SyncFun();
SyncFun syncFun1 = new SyncFun();
Thread thread0 = new Thread(new Runnable0(syncFun0));
Thread thread1 = new Thread(new Runnable0(syncFun1));
thread0.start();
thread1.start();
Thread.sleep(3000);
}
private static void test1_1() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable0(syncFun));
Thread thread2 = new Thread(new Runnable1(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test1_2() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable1(syncFun));
Thread thread2 = new Thread(new Runnable1(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test1_3() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable1(syncFun));
Thread thread2 = new Thread(new Runnable2(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test1_4() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable3(syncFun));
Thread thread2 = new Thread(new Runnable3(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test1_5() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable1(syncFun));
Thread thread2 = new Thread(new Runnable3(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test1_6() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable1(syncFun));
Thread thread2 = new Thread(new Runnable4(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test6() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable5(syncFun));
Thread thread2 = new Thread(new Runnable6(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test7() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread1 = new Thread(new Runnable5(syncFun));
Thread thread2 = new Thread(new Runnable7(syncFun));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
private static void test8() throws InterruptedException {
SyncFun syncFun = new SyncFun();
Thread thread2 = new Thread(new Runnable5(syncFun));
Thread thread1 = new Thread(new Runnable1(syncFun));
thread2.start();
thread1.start();
Thread.sleep(3000);
}
private static void test9() throws InterruptedException {
SyncFun syncFun1 = new SyncFun();
SyncFun syncFun2 = new SyncFun();
Thread thread1 = new Thread(new Runnable5(syncFun1));
Thread thread2 = new Thread(new Runnable6(syncFun2));
thread1.start();
thread2.start();
Thread.sleep(3000);
}
}
(1)、(2)和(3)好理解,(1)調(diào)用不同對象的同步方法,自然不會阻塞,因為獲取的不是同一個鎖,(2)和(3)調(diào)用同一個對象的同步方法,無論是同一個同步方法還是不同的同步方法,都會阻塞,因為一個對象的同步方法的鎖對象時 this,所以同一個對象的同步方法都是互斥的。
(4)訪問同一個對象的同步方法和 this 代碼塊,會阻塞。因為當(dāng)前對象的同步方法和 this 代碼塊鎖的都是當(dāng)前對象,所以會互斥。
(5)訪問同一個對象的非 this 同步代碼塊也會阻塞,這個沒什么解釋的,僅僅為了說明使用非 this 鎖也是阻塞的。
(6)訪問同一個對象的同步方法和非 this 對象代碼塊,不會阻塞,因為不是同一個鎖對象,也就不會阻塞。
(7)訪問同一個對象的同步方法和參數(shù)傳遞的鎖對象,和(6)中非 this 對象鎖一樣,同樣不阻塞。
(8)訪問一個類的不同靜態(tài)同步方法,會阻塞,這是鎖對象是這個類的 Class 對象,所以所有的靜態(tài)同步方法會互斥,同時調(diào)用時會進(jìn)行阻塞。
(9)訪問靜態(tài)同步方法和該類對象的同步代碼塊(Class 對象鎖),會阻塞,此時鎖對象是同一個,都是 Class 對象鎖,所以會阻塞。
(10)訪問一個對象的同步方法和該類靜態(tài)同步方法,不會阻塞,這個可能有些疑惑,但是只要找到這兩個方法的鎖,就很容易理解了,對象的同步方法的鎖是 this,該類靜態(tài)同步方法的鎖是該類的 Class 對象,屬于不同的鎖,所以同時調(diào)用時不會阻塞。
(11)不同對象訪問不同靜態(tài)同步方法,會阻塞,此時與對象沒有關(guān)系,因為是靜態(tài)方法,屬于類的,相當(dāng)于直接調(diào)用類靜態(tài)的同步方法,會阻塞。
3.總結(jié)
1、synchronized 同步方法
(1)對其他 synchronized 同步方法或 synchronized(this) 同步代碼塊呈阻塞狀態(tài)
(2)同一時間只有一個線程可以執(zhí)行 synchronized 同步方法中的代碼
2、synchronized 同步代碼塊
(1)對其他 synchronized 同步方法或 synchronized(this) 同步代碼塊呈阻塞狀態(tài)
(2)同一時間只有一個線程可以執(zhí)行 synchronized(this) 同步代碼塊中的代碼
(3)當(dāng)一個線程訪問對象的 synchronized 代碼塊的時候,另一個線程依然可以訪問對象方法中其余非 synchronized 塊的部分
3、synchronized 同步方法 VS synchronized 同步代碼塊
(1) 鎖非 this 對象具有一定的優(yōu)點(diǎn):如果在一個類中有很多 synchronized 方法,這時雖然能實現(xiàn)同步,但會受到阻塞,從而影響效率。但如果同步代碼塊鎖的是非 this 對象,則 synchronized(非 this 對象 x)代碼塊中的程序與同步方法是異步的,不與其他鎖 this 同步方法爭搶 this 鎖,大大提高了運(yùn)行效率。
(2) synchronized(非 this 對象 x) 格式的寫法是將x對象本身作為對象監(jiān)視器,有三個結(jié)論得出:
當(dāng)多個線程同時執(zhí)行 synchronized(x){} 同步代碼塊時呈同步效果
當(dāng)其他線程執(zhí)行 x 對象中的 synchronized 同步方法時呈同步效果
當(dāng)其他線程執(zhí)行 x 對象方法中的 synchronized(this) 代碼塊時也呈同步效果
synchronized(非 this 對象 x),這個對象如果是實例變量的話,指的是對象的引用,只要對象的引用不變,即使改變了對象的屬性,運(yùn)行結(jié)果依然是同步的。
總結(jié):無論是方法鎖還是代碼鎖都是要以一個對象監(jiān)視器來鎖定,鎖定的代碼是同步的,鎖 this 是當(dāng)前對象,鎖 String 是 String 這個對象,鎖 Object 是 Object 這個對象,互不干擾,如果有其它線程調(diào)用同樣用到跟上面鎖 this、Objcet、String 相同對象的方法或代碼,就需要等待同步,鎖代碼塊比鎖方法更加靈活。因為鎖方法鎖的是 this 也就是當(dāng)前對象,當(dāng)一個線程正在調(diào)用當(dāng)前這個對象的鎖方法時,導(dǎo)致其它線程調(diào)用不了該對象的其它鎖 this 的代碼,也調(diào)不了所有該對象的鎖方法。