一、什麼是線程共享模型?
在前面的章節中,我們介紹了計算機的共享模型,和java的線程共享模型:
1)計算機共享模型
2)java線程共享模型
如上所示,無論是哪種模型,都有線程或cpu自己的運行時緩存或內存,同時都有主內存。
二、線程共享模型存在什麼問題?
首先看下面的代碼,兩個線程,每個線程分別對i進行++操作,加100000次,結果會得到200000嗎:
/**
* @description: 線程共享模型問題
* @author:weirx
* @date:2021/11/25 9:48
* @version:3.0
*/
public class ThreadSharedModelProblems {
static int i = 0;
/**
* 兩個長度的門閂
*/
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
i++;
}
// 減少門閂數
countDownLatch.countDown();
});
t1.start();
Thread t2 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
i++;
}
// 減少門閂數
countDownLatch.countDown();
});
t2.start();
//阻塞等待門閂數降爲0
countDownLatch.await();
System.out.println("i = " + i);
}
結果:
i = 143188
產生的原因呢?主要是因爲i++並不是一個原子性操作。i++操作的JVM字節碼如下:
getstatic #2 // 獲取靜態變量i
iconst_1 // 定義局部變量1
iadd // 執行自加1操作
putstatic #2 // 將自加1後的值賦給靜態變量i
return
那麼結合上面的例子和線程共享模型就會是如下模式:
線程t1和t2同時去主內存獲取獲取i的值,並進行自加1的操作,然後再將值賦回給主線程,因爲這兩個線程之間是沒有順序的,且沒有任何的關聯,勢必會造成線程t1,剛寫入主內存的值,被t2覆蓋,而t1再次取值,就不是上次的值了。
以上呢就是共享資源所導致的問題。
一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊爲臨界區。
多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了競態條件。
三、解決方案
爲了避免臨界區的競態條件發生,有多種手段可以達到目的。
- 阻塞式的解決方案:synchronized,Lock
- 非阻塞式的解決方案:原子變量
下文重點講解使用synchronized解決上面的問題。
3.1 synchronized對象鎖
對象鎖:它採用互斥的方式讓同一時刻至多隻有一個線程能持有對象鎖,其它線程再想獲取這個對象鎖時就會阻塞住。
可以理解這個對象爲一個房間,這個房間一次只能有一個人進入,代碼如下:
public class ThreadSharedModelProblems {
static int i = 0;
/**
* 兩個長度的門閂
*/
static CountDownLatch countDownLatch = new CountDownLatch(2);
/**
* 定義一個不可變的對象,此處可以理解成一個房間
*/
static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
// 爭奪進入房間的機會
synchronized (obj){
i++;
}
}
// 減少門閂數
countDownLatch.countDown();
});
t1.start();
Thread t2 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
// 爭奪進入房間的機會
synchronized (obj){
i++;
}
}
// 減少門閂數
countDownLatch.countDown();
});
t2.start();
//阻塞等待門閂數降爲0
countDownLatch.await();
System.out.println("i = " + i);
}
}
synchronized 實際是用對象鎖保證了臨界區內代碼的原子性。臨界區內的代碼對外是不可分割的,不會被線程切換所打斷。
如何理解上面這句話的後半句?cpu在運行時,會發生線程上下文的切換,假設t1正持有對象,及在房間內進行++操作,如果此時cpu時間片用完了,這個t1就會釋放佔用的cpu資源,但是對象鎖仍然被其持有,t2仍然不能獲得對象鎖。只有當cpu在給t1分配時間片,並完成此次循環操作後,t2纔有機會去獲得對象鎖。
3.2 對象鎖的優化
java是一門面向對象的語言,所以像上一章節的對象鎖不是好的實現方式,我們應該將其放在對象當中。
寫一個Room對象,將++操作和對象鎖放在其中,代碼如下所示:
Room:
public class Room {
int i = 0;
public int getI() {
synchronized (this) {
return i;
}
}
public void add() {
synchronized (this) {
i++;
}
}
}
main方法:
/**
* @description: 線程共享模型問題
* @author:weirx
* @date:2021/11/25 9:48
* @version:3.0
*/
public class ThreadSharedModelProblems {
/**
* 兩個長度的門閂
*/
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
room.add();
}
// 減少門閂數
countDownLatch.countDown();
});
t1.start();
Thread t2 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
room.add();
}
// 減少門閂數
countDownLatch.countDown();
});
t2.start();
//阻塞等待門閂數降爲0
countDownLatch.await();
System.out.println("i = " + room.getI());
}
}
synchronized (this)當中的this是什麼呢?其實就是Room這個對象本身,如下所示:
3.3 方法上的synchronized
1)普通方法上的synchronized,等同於加在當前對象上,如下面代碼,test1等同於test2
2)靜態方法上的synchronized,等同於加在類上,如下面代碼,test3等同於test4
public class MethodSynchronized {
public synchronized void test1() {
System.out.println("this is test1");
}
public void test2() {
synchronized (this) {
System.out.println("this is test2");
}
}
public static synchronized void test3() {
System.out.println("this is test3");
}
public void test4() {
synchronized (MethodSynchronized.class) {
System.out.println("this is test4");
}
}
}
3.4 何謂“線程八鎖”?
其實就是考察 synchronized 鎖住的是哪個對象,我們主要要記住以下兩點:
- 普通方法鎖住的是this(當前對象),而靜態方法鎖住的是類(class)
- 同一時刻,只有一個線程能夠持有鎖
所謂線程八鎖,就是八種不同鎖的情況,下面我就不舉例了,但是要能夠分析,基本在以下幾種類型中:
- 同一個對象,內部無論幾個非靜態方法有鎖,都是互斥的
- 同一個類的不同對象,鎖不互斥
- 對象鎖,即this,與類鎖(class),是不互斥的
- 同一個類的內部兩個靜態方法的鎖,是互斥的
四、變量的安全分析
-
成員變量與靜態變量是線程安全的嗎?
如果它們沒有共享,則線程安全。
如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況:
- 如果只有讀操作,則線程安全
- 如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全
-
局部變量是線程安全的嗎?
局部變量是線程安全的。但局部變量引用的對象則未必
- 如果該對象沒有逃離方法的作用訪問,它是線程安全的
- 如果該對象逃離方法的作用範圍,需要考慮線程安全。(比如由於內部類重寫方法,該方法使用了修改了局部變量,且該方法被共享了,則會導致該變量的不安全,可以對這種方法時使用final,或設置爲pravite)。
五、常見的線程安全類
常見的線程安全類其實也分爲兩個方面:
-
使用鎖(synchronized,Lock,CAS)
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的類需要注意的是,上面舉例的類,他們的方法都是原子性的,但是組合使用後並不能保證原子性,需要我們自己進行控制。
-
不可變類(final)
String
IntegerString、Integer 等都是不可變類,因爲其內部的狀態不可以改變,因此它們的方法都是線程安全的
關於線程共享模型以及synchronized的簡單使用就介紹到這裏了,有幫助的話點個贊吧。。