簡介
本文將介紹Java中兩種對象鎖及其實現方式。分別是同步代碼塊鎖和方法鎖。我們將解決上一篇文章《Java中線程安全和線程不安全解析和示例》中提到的線程不安全問題,看看通過synchronized
的對象鎖,怎樣輕鬆解決該問題。
一.同步代碼塊鎖
案例:通過同步代碼塊鎖,實現兩個線程對同一個全局變量count
,各自執行1萬次count++
,驗證結果是否等於2萬,而不會出現小於2萬的情況。
完整代碼實現:
public class SynchronizeCodeBlockLock implements Runnable {
private static SynchronizeCodeBlockLock instance = new SynchronizeCodeBlockLock();
private static int count = 0;
@Override
public void run() {
method();
}
private void method() {
// 關鍵:同步代碼塊的方式,操作同一變量,達到線程安全的效果
synchronized (this) {
System.out.println("線程名:" + Thread.currentThread().getName() + ",運行開始");
for (int i = 0; i < 10000; i++) {
count++;
}
System.out.println("線程:" + Thread.currentThread().getName() + ",運行結束");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
// 若有線程如果還在活動,則不執行下一步(等同於thread.join()方法)
}
System.out.println("期待結果:20000,實際結果:" + count);
}
}
運行結果:
線程名:Thread-0,運行開始
線程:Thread-0,運行結束
線程名:Thread-1,運行開始
線程:Thread-1,運行結束
期待結果:20000,實際結果:20000
分析運行結果:
我們發現使用了synchronized
關鍵字後,線程Thread-0先執行,等到其結束後,Thread-1纔開始執行。如果不使用synchronized
關鍵字,執行結果可能會是Thread-0和Thread-1幾乎同時執行,幾乎同時運行結束。這就說明使用了synchronized
關鍵字後,將多個線程的並行,變爲了串行。
二.方法鎖
本例僅展示方法鎖,在控制線程串行執行的示例。方法鎖保證線程安全的效果,跟同步代碼塊是一致的。
public class MethodLock implements Runnable {
private static MethodLock instance = new MethodLock();
@Override
public void run() {
method();
}
//關鍵:synchronized可以保證此方法被順序執行,線程1執行完4秒鐘後,線程2再執行4秒。不加synchronized,線程1和線程2將同時執行
private synchronized void method() {
System.out.println("線程:" + Thread.currentThread().getName() + ",運行開始");
try {
//模擬執行一段操作,耗時4秒鐘
Thread.sleep(4000);
System.out.println("線程:" + Thread.currentThread().getName() + ",運行結束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 模擬:同一個對象下,兩個線程,同步執行一個方法(串行執行則爲線程安全,並行執行,則爲線程不安全,)
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("測試結束");
}
}
運行結果:
線程:Thread-0,運行開始
線程:Thread-0,運行結束
線程:Thread-1,運行開始
線程:Thread-1,運行結束
測試結束
發現運行結果中,多個線程也是串行執行的,效果跟同步代碼塊鎖是一致的。
三.synchronized關鍵字,是怎麼保證線程安全的呢?
在Java中所有的對象,都會有一把鎖,叫做內置鎖,也稱作監視器鎖。這是種排他鎖。
排他鎖:一個線程獲取後,其他線程只能等待其釋放後,纔有機會獲得該鎖。
Java中每個對象,都可以把內置鎖,當做一個同步鎖來使用。當一個線程在進入到synchronized
代碼塊前時,會自動獲取到監視器鎖,此時其他線程在訪問synchronized
代碼塊時,就會被堵塞掛起(被拒之門外的意思)。拿到鎖的線程會在執行完成、或者拋出異常、或者調用wait系列方法時釋放該鎖。其他線程只能等待鎖被釋放後才能獲取該鎖。
通俗的講,synchronized
關鍵字將代碼塊中代碼由並行
轉變成了串行
,這樣就保證了代碼被順序執行。
四.synchronized在內存層面,是如何實現加鎖和釋放鎖的?
- 進入
synchronized
代碼塊時,會將代碼塊內用到的變量從該線程的工作內存中清除,轉而從主內存中獲取。 - 退出
synchronized
代碼塊時,會將代碼塊內用到的變量的修改,刷新到主內存中。
這其實就是synchronized
解決共享變量內存可見性
的原理。關於synchronized
的性質(可見性、可重入性),我會在後續其他文章中詳細解釋。
五.synchronized將線程的並行處理轉爲串行處理,有什麼缺點?
synchronized
將並行改爲串行,當然會影響程序的執行效率,執行速度會受到影響。其次,synchronized
操作線程的堵塞,也就是由操作系統控制CPU的內核進行上下文的切換,這個切換本身也是耗時的。所以使用synchronized
關鍵字會降低程序的運行效率。
六. 使用Synchronized關鍵字需要注意什麼?
1.Synchronized使用時需要注意的地方鎖對象不能爲空。
鎖對象的信息是保留在對象頭中的,如果對象爲空,則鎖的信息也就不存在了。
2.作用域不宜過大
synchronized代碼塊的代碼量不宜過多,如果把過多的代碼放在其中,程序的運行會變爲串行,速度會下降。各個線程並行可以提高效率,我們應該僅把那些影響線程安全的代碼,放入synchronized代碼塊中,串行執行;不需要考慮線程安全的代碼,並行執行,達到效率最高。
3.避免死鎖
避免讓線程對鎖持有並等待的情況出現(後續文章將講解死鎖的相關知識)。
七.總結
本文講解了Java中的對象鎖的兩種實現方式,分別是以下兩種實現形式:
方式一:同步代碼塊鎖:
synchronized (共享變量) {
//需要同步的代碼
}
方式二:方法鎖:
private synchronized void method() {
//需要同步的代碼
}
無論你使用哪一種形式,都應該在保證同步的情況下,儘量減少同步代碼的內容,這樣可以提高程序的運行效率,還能保證線程的安全。喜歡本文請點贊和收藏,也歡迎繼續閱讀本專欄多線程的其他文章。