1、先來了解一下:為什么多線程並發是不安全的?
在操作系統中,線程是不擁有資源的,進程是擁有資源的。而線程是由進程創建的,一個進程可以創建多個線程,這些線程共享着進程中的資源。所以,當線程一起並發運行時,同時對一個數據進行修改,就可能會造成數據的不一致性,看下面的例子:
假設一個簡單的int字段被定義和初始化:
int counter = 0;
該counter字段在兩個線程A和B之間共享。假設線程A、線程B同時對counter進行計算,遞增運算:
counter ++;
那么計算的結果應該是 2 。但是真實的結果卻是 1 ,這是因為:線程A得到的運算結果是1,線程B的運算結果也是1,當線程A將結果寫回到內存中的 count 后,線程B也將結果寫回到內存中去,這就會把線程A的計算結果給覆蓋了。
上面僅僅是一種簡單的情況,還有更復雜的情況,本文不深入去了解。
2、多線程並發不安全的原因已經知道,那么針對這個種情況,java中有兩種解決思路:
給共享的資源加把鎖,保證每個資源變量每時每刻至多被一個線程占用。
讓線程也擁有資源,不用去共享進程中的資源。
3、基於上面的兩種思路,下面便是3種實施方案:
1. 多實例、或者是多副本(ThreadLocal):對應着思路2,ThreadLocal可以為每個線程的維護一個私有的本地變量,可參考java線程副本–ThreadLocal;
2. 使用鎖機制 synchronize、lock方式:為資源加鎖,可參考我寫的一系列文章;
3. 使用 java.util.concurrent 下面的類庫:有JDK提供的線程安全的集合類
可能說的還不太清楚,更新一下,以及給出一個線程安全模擬的例子:
上面說了,多線程之所以不安全,是因為共享着資源(如果沒有資源變量共享,那么多線程一定是安全的)。比如,存在共享變量a,線程A在使用變量a時進行計算時,因為時間片的到來,導致線程不得不由運行中狀態進入就緒狀態,暫停運行。等該線程A又重新被調度,得以繼續執行時,得到了最終的結果。但是此時內存中的變量a可能已經被其他線程改變了,但線程A的結果再寫回到內存中時,就會覆蓋了其他線程的計算結果,這就是多線程不安全的原理。
下面給出線程安全模擬的例子的思路:1、讓三個線程瞬間同時並發(不得不用到鎖,wait/notify機制,如果不懂,只要知道這是 等待/通知 便可,下面有注釋);2、模擬3個線程共享着一個變量,使用變量進行計算的過程 與 將計算結果分成兩次執行。
下面是沒有進行同步,也就是線程不安全的情況:
CountMoney countMoney = new CountMoney();
String obj="";
//創建啟動3個線程
for(int i=0;i<3;i++){
Thread t1 = new Thread(){
@Override
public void run() {
//用鎖來讓線程第一次運行時,進入等待狀態,直到被通知來了才繼續往下運行
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//通知來了后,執行addMoney的方法
countMoney .addMoney(1);
}
};
//線程啟動
t1.start();
//確保創建的線程的優先級一樣
t1.setPriority(Thread.NORM_PRIORITY);
}
try {
//確保創建的3個線程已經運行了一次,進入等待狀態
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
//瞬間喚醒3個線程
obj.notifyAll();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CountMoney 類
public class CountMoney {
//線程共享着CountMoney對象中的money變量
volatile long money = 0;
public long getMoney() {
return money;
}
public void setMoney(long money) {
this.money = money;
}
public void addMoney(long a) {//synchronized
//1、取得變量money的值,計算出結果
a = getMoney() + a;
//線程完成第一步后,讓出CPU;目的是:模擬1、2兩行代碼是分成兩次執行的,不是一次性執行的
Thread.yield();
//2、將計算結果寫回到變量money中
setMoney(a);
System.out.println("線程"+Thread.currentThread().getName()+"的計算結果"+getMoney());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
運行結果:
線程Thread-2的計算結果1
線程Thread-1的計算結果1
線程Thread-0的計算結果1
我們再來看一下,加鎖后的 addMoney()方法,也就是進行同步后:
public synchronized void addMoney(long a) {//加了synchronized 修飾
//1、取得變量money的值,計算出結果
a = getMoney() + a;
//線程完成第一步后,讓出CPU;目的是:模擬1、2兩行代碼是分成兩次執行的,不是一次性執行的
Thread.yield();
//2、將計算結果寫回到變量money中
setMoney(a);
System.out.println("線程"+Thread.currentThread().getName()+"的計算結果"+getMoney());
}
1
2
3
4
5
6
7
8
9
運行結果:
線程Thread-2的計算結果1
線程Thread-1的計算結果2
線程Thread-0的計算結果3
---------------------
作者:jinggod
來源:CSDN
原文:https://blog.csdn.net/jinggod/article/details/78275763
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!