前面我們介紹了很多關於多線程的內容,在多線程中有一個很重要的課題需要我們攻克,那就是線程安全問題。線程安全問題指的是在多線程中,各線程之間因爲同時操作所產生的數據污染或其他非預期的程序運行結果。
在此,如果有對併發編程感興趣的或者想系統掌握併發編程的,這邊推薦一個欄目,
線程安全
1)非線程安全事例
比如 A 和 B 同時給 C 轉賬的問題,假設 C 原本餘額有 100 元,A 給 C 轉賬 100 元,正在轉的途中,此時 B 也給 C 轉了 100 元,這個時候 A 先給 C 轉賬成功,餘額變成了 200 元,但 B 事先查詢 C 的餘額是 100 元,轉賬成功之後也是 200 元。當 A 和 B 都給 C 轉賬完成之後,餘額還是 200 元,而非預期的 300 元,這就是典型的線程安全的問題。
2)非線程安全代碼示例
上面的內容沒看明白沒關係,下面來看非線程安全的具體代碼:
class ThreadSafeTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -› addNumber());
Thread thread2 = new Thread(() -› addNumber());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("number:" + number);
}
public static void addNumber() {
for (int i = 0; i ‹ 10000; i++) {
++number;
}
}
}
以上程序執行結果如下:
number:12085
每次執行的結果可能略有差異,不過幾乎不會等於(正確的)累計之和 20000。
3)線程安全的解決方案
線程安全的解決方案有以下幾個維度:
-
數據不共享,單線程可見,比如 ThreadLocal 就是單線程可見的;
-
使用線程安全類,比如 StringBuffer 和 JUC(java.util.concurrent)下的安全類(後面文章會專門介紹);
-
使用同步代碼或者鎖。
線程同步和鎖
1)synchronized
① synchronized 介紹
synchronized 是 Java 提供的同步機制,當一個線程正在操作同步代碼塊(synchronized 修飾的代碼)時,其他線程只能阻塞等待原有線程執行完再執行。
② synchronized 使用
synchronized 可以修飾代碼塊或者方法,示例代碼如下:
// 修飾代碼塊
synchronized (this) {
// do something
}
// 修飾方法
synchronized void method() {
// do something
}
使用 synchronized 完善本文開頭的非線程安全的代碼。
方法一:使用 synchronized 修飾代碼塊,代碼如下:
class ThreadSafeTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread sThread = new Thread(() -› {
// 同步代碼
synchronized (ThreadSafeTest.class) {
addNumber();
}
});
Thread sThread2 = new Thread(() -› {
// 同步代碼
synchronized (ThreadSafeTest.class) {
addNumber();
}
});
sThread.start();
sThread2.start();
sThread.join();
sThread2.join();
System.out.println("number:" + number);
}
public static void addNumber() {
for (int i = 0; i ‹ 10000; i++) {
++number;
}
}
}
以上程序執行結果如下:
number:20000
方法二:使用 synchronized 修飾方法,代碼如下:
class ThreadSafeTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread sThread = new Thread(() -› addNumber());
Thread sThread2 = new Thread(() -› addNumber());
sThread.start();
sThread2.start();
sThread.join();
sThread2.join();
System.out.println("number:" + number);
}
public synchronized static void addNumber() {
for (int i = 0; i ‹ 10000; i++) {
++number;
}
}
}
以上程序執行結果如下:
number:20000
③ synchronized 實現原理
synchronized 本質是通過進入和退出的 Monitor 對象來實現線程安全的。
以下面代碼爲例:
public class SynchronizedTest {
public static void main(String[] args) {
synchronized (SynchronizedTest.class) {
System.out.println("Java");
}
}
}
當我們使用 javap 編譯之後,生成的字節碼如下:
Compiled from "SynchronizedTest.java"
public class com.interview.other.SynchronizedTest {
public com.interview.other.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."‹init›":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/interview/other/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String Java
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}
可以看出 JVM(Java 虛擬機)是採用 monitorenter 和 monitorexit 兩個指令來實現同步的,monitorenter 指令相當於加鎖,monitorexit 相當於釋放鎖。而 monitorenter 和 monitorexit 就是基於 Monitor 實現的。
2)ReentrantLock
① ReentrantLock 介紹
ReentrantLock(再入鎖)是 Java 5 提供的鎖實現,它的功能和 synchronized 基本相同。再入鎖通過調用 lock() 方法來獲取鎖,通過調用 unlock() 來釋放鎖。
② ReentrantLock 使用
ReentrantLock 基礎使用,代碼如下:
Lock lock = new ReentrantLock();
lock.lock(); // 加鎖
// 業務代碼...
lock.unlock(); // 解鎖
使用 ReentrantLock 完善本文開頭的非線程安全代碼,請參考以下代碼:
public class LockTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
// ReentrantLock 使用
Lock lock = new ReentrantLock();
Thread thread1 = new Thread(() -› {
try {
lock.lock();
addNumber();
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -› {
try {
lock.lock();
addNumber();
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("number:" + number);
}
public static void addNumber() {
for (int i = 0; i ‹ 10000; i++) {
++number;
}
}
}
嘗試獲取鎖
ReentrantLock 可以無阻塞嘗試訪問鎖,使用 tryLock() 方法,具體使用如下:
Lock reentrantLock = new ReentrantLock();
// 線程一
new Thread(() -› {
try {
reentrantLock.lock();
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}).start();
// 線程二
new Thread(() -› {
try {
Thread.sleep(1 * 1000);
System.out.println(reentrantLock.tryLock());
Thread.sleep(2 * 1000);
System.out.println(reentrantLock.tryLock());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
以上代碼執行結果如下:
false
true
嘗試一段時間內獲取鎖
tryLock() 有一個擴展方法 tryLock(long timeout, TimeUnit unit) 用於嘗試一段時間內獲取鎖,具體實現代碼如下:
Lock reentrantLock = new ReentrantLock();
// 線程一
new Thread(() -› {
try {
reentrantLock.lock();
System.out.println(LocalDateTime.now());
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}).start();
// 線程二
new Thread(() -› {
try {
Thread.sleep(1 * 1000);
System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS));
System.out.println(LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
以上代碼執行結果如下:
2019-07-05 19:53:51
true
2019-07-05 19:53:53
可以看出鎖在休眠了 2 秒之後,就被線程二直接獲取到了,所以說 tryLock(long timeout, TimeUnit unit) 方法內的 timeout 參數指的是獲取鎖的最大等待時間。
③ ReentrantLock 注意事項
使用 ReentrantLock 一定要記得釋放鎖,否則該鎖會被永久佔用。
相關面試題
1.ReentrantLock 常用的方法有哪些?
答:ReentrantLock 常見方法如下:
-
lock():用於獲取鎖
-
unlock():用於釋放鎖
-
tryLock():嘗試獲取鎖
-
getHoldCount():查詢當前線程執行 lock() 方法的次數
-
getQueueLength():返回正在排隊等待獲取此鎖的線程數
-
isFair():該鎖是否爲公平鎖
2.ReentrantLock 有哪些優勢?
答:ReentrantLock 具備非阻塞方式獲取鎖的特性,使用 tryLock() 方法。ReentrantLock 可以中斷獲得的鎖,使用 lockInterruptibly() 方法當獲取鎖之後,如果所在的線程被中斷,則會拋出異常並釋放當前獲得的鎖。ReentrantLock 可以在指定時間範圍內獲取鎖,使用 tryLock(long timeout,TimeUnit unit) 方法。
3.ReentrantLock 怎麼創建公平鎖?
答:new ReentrantLock() 默認創建的爲非公平鎖,如果要創建公平鎖可以使用 new ReentrantLock(true)。
4.公平鎖和非公平鎖有哪些區別?
答:公平鎖指的是線程獲取鎖的順序是按照加鎖順序來的,而非公平鎖指的是搶鎖機制,先 lock() 的線程不一定先獲得鎖。
5.ReentrantLock 中 lock() 和 lockInterruptibly() 有什麼區別?
答:lock() 和 lockInterruptibly() 的區別在於獲取線程的途中如果所在的線程中斷,lock() 會忽略異常繼續等待獲取線程,而 lockInterruptibly() 則會拋出 InterruptedException 異常。
題目解析:執行以下代碼,在線程中分別使用 lock() 和 lockInterruptibly() 查看運行結果,代碼如下:
Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
interruptLock.lock();
//interruptLock.lockInterruptibly(); // java.lang.InterruptedException
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);
執行以下代碼會發現使用 lock() 時程序不會報錯,運行完成直接退出;而使用 lockInterruptibly() 則會拋出異常 java.lang.InterruptedException,這就說明:在獲取線程的途中如果所在的線程中斷,lock() 會忽略異常繼續等待獲取線程,而 lockInterruptibly() 則會拋出 InterruptedException 異常。
6.synchronized 和 ReentrantLock 有什麼區別?
答:synchronized 和 ReentrantLock 都是保證線程安全的,它們的區別如下:
-
ReentrantLock 使用起來比較靈活,但是必須有釋放鎖的配合動作;
-
ReentrantLock 必須手動獲取與釋放鎖,而 synchronized 不需要手動釋放和開啓鎖;
-
ReentrantLock 只適用於代碼塊鎖,而 synchronized 可用於修飾方法、代碼塊等;
-
ReentrantLock 性能略高於 synchronized。
7.ReentrantLock 的 tryLock(3, TimeUnit.SECONDS) 表示等待 3 秒後再去獲取鎖,這種說法對嗎?爲什麼?
答:不對,tryLock(3, TimeUnit.SECONDS) 表示獲取鎖的最大等待時間爲 3 秒,期間會一直嘗試獲取,而不是等待 3 秒之後再去獲取鎖。
8.synchronized 是如何實現鎖升級的?
答:在鎖對象的對象頭裏面有一個 threadid 字段,在第一次訪問的時候 threadid 爲空,JVM(Java 虛擬機)讓其持有偏向鎖,並將 threadid 設置爲其線程 id,再次進入的時候會先判斷 threadid 是否尤其線程 id 一致,如果一致則可以直接使用,如果不一致,則升級偏向鎖爲輕量級鎖,通過自旋循環一定次數來獲取鎖,不會阻塞,執行一定次數之後就會升級爲重量級鎖,進入阻塞,整個過程就是鎖升級的過程。
總結
本文介紹了線程同步的兩種方式 synchronized 和 ReentrantLock,其中 ReentrantLock 使用更加靈活,效率也率高,不過 ReentrantLock 只能修飾代碼塊,使用 ReentrantLock 需要開發者手動釋放鎖,如果忘記釋放則該鎖會一直被佔用。synchronized 使用場景更廣,可以修飾普通方法、靜態方法和代碼塊等。
下一篇:java併發包
在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝我的哈
往期精選