通過上篇博文(android 多線程 — java 內(nèi)存模型)我們知道了多個線程同時多同一個對象讀寫可能會造成數(shù)據(jù)混亂,結(jié)果錯誤。
同步干啥了
那么 java 如果解決的這個問題呢,就是同步機制 — synchronized。什么是同步呢,就是讓Object 象同一時間只能被一個 Thread 讀寫。那么又是如何讓 Object 同一時間只能被一個 Thread 讀寫呢,是給每個 Object 里面加一把鎖,哪個 Thread 在使用這個 Object 就把這個對象上的鎖給誰,直到這個 Thread 執(zhí)行完對這個 Object 的操作,把 Object 上的鎖還給這個 Object ,然后下一個 Thread 才能對這個 Object 進行操作
synchronized 干的事就是這樣,管理對象上鎖,只給一個線程對象,保證同一時刻只有一個線程能操作這個對象
多余我們來說茶不必直接操作對象上的鎖,我們只要把對象傳給 synchronized 就行,至于是哪個對象,根據(jù)實際來選擇。
synchronized
先不忙來看看其他人的描述,這個最好:
Synchronized,它就是一個:非公平,悲觀,獨享,互斥,可重入鎖。
優(yōu)化以后,是基于JVM內(nèi)部實現(xiàn),可以根據(jù)競爭激烈程度,從偏向鎖-->輕量級鎖-->重量級鎖升級
synchronized 本身是一個關(guān)鍵字,用來修飾普通方法,靜態(tài)方法和代碼塊
修飾方法
synchronized public static void staticMethod()
修飾代碼塊
synchronized (SynchronizedObject.class) {
xxxxxxxxxx
}
synchronized 作為關(guān)鍵字,使用是很方便的,看這2個代碼片段就能體會到,方法,代碼塊加了 synchronized 就能在多線程中保持內(nèi)存同步,同一時間內(nèi)只能有一個 Thread 進來操作 synchronized 標記的方法和代碼塊。
synchronized 用的誰身上的鎖
然后我們進一步思考,synchronized 需要一把對象鎖,在 synchronized 修飾方法時,不論是靜態(tài)方法還是普通方法,都是在方法前面加上 synchronized 就行了,那么和這個 synchronized 對應的鎖是用的誰的
synchronized 修飾普通方法
用的是這個方法所在對象的鎖synchronized 修飾靜態(tài)方法
用的是這個方法所在對象的類的鎖
同步代碼塊在具體書寫時,我們會碰到下面幾種使用鎖的方式
// 使用 .class 鎖
synchronized (SynchronizedObject.class) {
xxxxxxxxxx
}
// 使用 Object 對象鎖
Object c = new Object ();
synchronized (c) {
xxxxxxxxxx
}
// 使用當前對象的鎖
synchronized (this) {
xxxxxxxxxx
}
kotlin 上的寫法,使用的是 @Synchronized
@Synchronized
fun test(){}
this 就是對象的鎖一種寫法,本質(zhì)和普通同步方法相同,效果也相同
多線程并發(fā)環(huán)境下會造成阻塞,會影響執(zhí)行效率,所以對象鎖的阻塞范圍要有清晰的了解,這是 synchronized 的特征,因為 synchronized 本身不帶鎖,要用別人的。
不同鎖對應的阻塞范圍
synchronized 的鎖本質(zhì)上2種,寫法3種:
- Object.class 類.class鎖
- this 當前對象鎖,等同于同步方法思路,這個對象就是容器對象。
- object 成員變量鎖
我翻了好多書和資料,沒看到有人仔細說不同的對象鎖對應的阻塞范圍,那咱們就自己東西來試試。
其實我們要搞清楚的就是下面幾個問題沒,前提是多線程環(huán)境下對個線程同時操作同一個對象的方法和成員變量:
- 調(diào)用同一個同步方法會不會阻塞
- 在別的線程調(diào)用同步方法時,非同步方法能不能同時調(diào)用,成員變量等同于方法
- 在別的線程調(diào)用同步方法時,該對象的成員變量中的同步方法能不能同時調(diào)用
- Object.class 和 對象鎖一樣不一樣
測試用例:
- 我們設計一個對象 Animal, 提供同步和非同步打印方法,連續(xù)打印5次,每次間隔1秒。
- 這個對象有個成員變量 Book,Book 也能提供非同和非同步的打印方法
- 我們在 UI 線程啟動2個 thread 出來,分別調(diào)用 animal 對象的方法和 animal 的成員變量 Book 的方法
Animal 對象
public class Animal {
public String name;
public Book book;
public Animal(String name) {
this.name = name;
book = new Book("《Android 開發(fā)藝術(shù)探索》");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void speak() {
for (int i = 0; i <= 5; i++) {
Log.d("AAA", name + "第 " + i + " 次" + "非同步叫喚," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized public void speakSynchronized() {
for (int i = 0; i <= 5; i++) {
Log.d("AAA", name + "第 " + i + " 次" + "同步叫喚," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized public void OhterSynchronized() {
for (int i = 0; i <= 5; i++) {
Log.d("AAA", name + "其他同步方法," + " 第 " + i + " 次" + ", Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Book 對象
public class Book {
public String name;
public Book(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void speak() {
for (int i = 0; i <= 5; i++) {
Log.d("AAA", name + "第 " + i + " 次" + "非同步閱讀," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized public void speakSynchronized() {
for (int i = 0; i <= 5; i++) {
Log.d("AAA", name + "第 " + i + " 次" + "同步閱讀," + "Thread: " + Thread.currentThread().getName() + " / Time: " + System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
測試1
2個線程同時調(diào)用 animal 的同步方法
測試代碼
Animal dog = new Animal("汪醬");
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.speakSynchronized();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.speakSynchronized();
}
};
t1.start();
t2.start();

不出所料,同一個對象里同一個同步方法只能有一個 Thread 調(diào)用,其他想調(diào)用改方法的 Thread 都得在后面排隊,也就是阻塞。這是 synchronized 最常見的使用,也是 synchronized 的初衷。
測試2
1個線程調(diào)用 animal 對象同步方法的同時,另一個線程調(diào)用 animal 對象的非同步的普通方法
測試代碼
Animal dog = new Animal("汪醬");
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.speakSynchronized();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.speak();
}
};
t1.start();
t2.start();

可以看到,同步和非同步方法一起執(zhí)行。著說明對象鎖的阻塞范圍不包括非同步方法。這下我們心里有跟了,沒有同步標記的方法多線程中沒有使用限制
測試3
1個線程調(diào)用 animal 對象同步方法的同時,另一個線程調(diào)用 animal 對象的另一個同步方法
測試代碼
Animal dog = new Animal("汪醬");
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.speakSynchronized();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.OtherSynchronized();
}
};
t1.start();
t2.start();

可以看到,2個不同的同步方法還是阻塞執(zhí)行的,同一時間只有一個同步方法能跑。說明對象鎖的阻塞范圍是這個對象內(nèi)的所有同步方法的。
測試4
1個線程調(diào)用 animal 對象同步方法的同時,另一個線程調(diào)用 animal 對象成員變量 book 的同步方法
測試代碼
Animal dog = new Animal("汪醬");
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.speakSynchronized();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.book.speakSynchronized();
}
};
t1.start();
t2.start();

結(jié)果可能出乎我們意料,但是想想又是非常合理的,2個同步方法同時執(zhí)行餓了。著說明對象鎖的阻塞范圍僅限于自身直接的方法,而對于自身成員變量的同步方法是阻塞不了的,大家想想啊,我的成員變量是個對象,那么這個對象有自己的鎖,肯定頁不應該受外部容器對象鎖的影響
測試5
對象鎖我們基本摸清規(guī)律了,剩下的場景我們也能根據(jù)上面的阻塞規(guī)則分析出來了,現(xiàn)在我們還要解決 Object.class 的問題,和對象鎖一樣嗎
我們先來測下靜態(tài)同步方法,靜態(tài)方法是屬于類的,而不是對象的,這個好理解我們先來測
給 Animal 對象添加一個靜態(tài)同步方法,我就不再上 Animal 的代碼了,大家想象下,同時 new 2個 animal 對象出來,調(diào)用同一個靜態(tài)同步方法
測試代碼
Animal dog = new Animal("汪醬");
Animal dog2 = new Animal("papi醬");
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.staticSynchronized();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog2.staticSynchronized();
}
};
t1.start();
t2.start();

不愧為 static 屬于類本身一說啊,不管 new 幾個同一類型的對象出來,類本身的 static 的靜態(tài)同步方法同一時刻只能有一個線程調(diào)用。
測試6
我們來試試在代碼塊內(nèi)使用 Object.class ,在 Animal 中添加一個普通方法內(nèi)有同步代碼塊,使用 Object.class 的類鎖。new 2個 Animal 對象,2個線程同時調(diào)用 這個方法
測試代碼
Animal dog = new Animal("汪醬");
Animal dog2 = new Animal("papi醬");
Thread t1 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog.codeBlock();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
Log.d("AAA", Thread.currentThread().getName() + "開始執(zhí)行......" + " / Time: " + System.currentTimeMillis());
dog2.codeBlock();
}
};
t1.start();
t2.start();

恩,可以看到,2個 Animal 對象的 Object.class 同步代碼塊方法同時只能有一個線程跑。這說明在代碼塊中使用 Object.class 相當于把這個方法標記為靜態(tài)同步的。
總結(jié)下對象鎖的阻塞范圍:
- 對象鎖的阻塞先于自身的同步方法,同步方法沒有數(shù)量限制,一個線程正在調(diào)用對象的摸某一個同步方法,那么此時另一個線程調(diào)用這個對象的另一個同步方法也是會被阻塞的
- 對象鎖的不會阻塞非同步的阻塞方法,即使此時一個線程正在調(diào)用這個對象的同步方法,其他線程這個時候也是可以調(diào)用這個對象的非同步方法的
- 對象鎖的范圍僅限自身,對象的成員變量不受外部對象鎖的阻塞影響,這符合一個對象一把鎖的設計思路
- 靜態(tài)同步方法屬于類本身,不管這個類有多少個實例,同一時刻只能有一個線程操作這個類的這個靜態(tài)的同步方法,和對象實例沒關(guān)系,只和類有關(guān)系
- 同步代碼塊使用 Object.class 等同于把方法標記為靜態(tài)同步的
- 同步代碼塊使用 this.class 等同于把方法標記為同步的
synchronized 扯了半天,但是只要我們把 synchronized 搞清楚了,同步基本就沒問題了,實際編碼時,同步我們都是使用 synchronized 的,synchronized 玩好了就差不多成了。