概述
前面幾篇博客我們介紹了 java 代碼中如何創建並啓動線程。而多線程併發在提高效率的同時也帶來了線程安全性問題,本篇博客我們主要整理一下java對象和線程安全性問題的聯繫。
Java對象與線程安全
本篇博客從以下四個模塊展開:
- 線程安全問題產生的原因
- 線程安全與全局變量
- 線程安全與局部變量
- 線程安全與其他資源
1、線程安全問題產生的原因
之前的多線程博客中我們提到:如果存在多個線程同時寫一塊內存空間,就可能產生線程安全問題。對應到 java 代碼中,也就是說:如果存在多個線程同時寫某個對象值,就可能產生線程安全問題。
下面我們看一個簡單的代碼:
public class Demo {
private int value = 0;
public void addValue(int num) {
this.value = this.value + num;
}
}
在上述代碼中,如果存在多個線程同時調用 addValue() 方法,我們就無法確定結果具體的值,可能是線程1操作的結果,也可以是線程2操作的結果,還可能是兩個線程共同操作的結果。我們的目標應該是兩個線程共同操作的結果,但現在結果無法確定,也就是說可能造成線程安全問題。
看到這裏大家可能會有疑問:簡單的一行代碼,爲什麼也存在線程安全問題呢?
原因是這樣的:JAVA 不同於C語言編譯之後就可以被計算機執行,JAVA 代碼首先需要通過編譯器編譯爲 .class 類型,然後被 jvm 解釋爲當前操作系統能夠理解的機器語言,也就是說:操作系統層面執行的指令和我們寫的代碼是完全不同的,簡單的一行 java 代碼最終在操作系統層面可能需要運行多條指令。
回到上述代碼,addValue() 方法中的代碼 this.value = this.value + num 實際上在操作系統中被分解爲以下三步:
從內存中讀取 value 值存放在寄存器
將寄存器中的值加 num
將新值寫回內存
因此,當兩個線程同時調用 addValue() 方法時,可能存在以下執行順序:
線程A 讀取 value 值存在到寄存器
線程B 讀取 value 值存在到寄存器
線程A 進行加法運算
線程B 進線加法運算
線程A 將新值寫回內存
線程B 將新值寫回內存
從運行結果可以看出,線程B 的執行結果覆蓋了線程A 的執行結果造成線程安全問題。交替執行 也是多線程模式下產生線程安全問題的根本原因。一般我們稱這種對同一資源訪問順序敏感的情況爲 競態條件。產生競態條件的代碼塊我們稱爲 臨界區。
2、線程安全與全局變量
全局變量無論類對象還是普通常量都可能產生線程安全問題。因爲無論何種類型的變量,如果存在方法修改這部分數據,都會產生競態條件。
在實際開發過程中,一般使用類本身就線程安全的對象作爲全局變量。即使使用非線程安全對象,也一般通過各種同步手段保證線程安全性問題。如:synchronized、ReentrantLock等
需要注意的一點是,當兩個線程操作的全局變量分別在兩個不同的對象時,不會產生線程安全性問題。
3、線程安全與局部變量
局部常量存儲在線程自己的棧中,也就是說不存在多個線程同時操作的情況,因此 局部常量 永遠是線程安全的。 我們舉一個簡單例子:
public int methodName(){
int num = 1;
return num++;
}
在上述代碼中,無論多少個線程併發執行,返回結果都是1,不存在線程安全性問題。
而局部變量中的類對象就不一定了,類對象只有 引用 保存在棧中,對象還是保存在所有線程共享的堆區,而且存在逃逸的可能性,也就是說局部對象可能存在線程安全問題。
下面我們通過代碼簡單的介紹一種可能產生線程安全性問題的場景:
public class ErrorTest {
private class Node {
int num = 0;
private void add() {
num = num + 1;
}
}
private class Worker implements Runnable {
Node node = null;
public Worker(Node node) {
this.node = node;
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
node.add();
}
}
}
@Test
public void test() throws InterruptedException {
Node node = new Node();
new Thread(new Worker(node)).start();
new Thread(new Worker(node)).start();
Thread.sleep(3000);
System.out.println(node.num);
}
}
執行結果:
151491
通過結果我們可以看出,雖然 node 對象是方法 test() 中創建的局部變量,輸出結果仍然存在線程安全性問題。導致線程安全問題產生的原因是我們將該對象作爲參數創建兩個線程對象。也就是說該局部變量從方法中逃逸出去了,並且在兩個不同的線程中併發執行。
判斷一個局部變量是否線程安全的主要方式是判斷該對象從創建到被回收,是否只在一個線程中執行。如果滿足該條件,即使逃逸到其他方法中,也不會存在線程安全性問題。
4、線程安全與其他資源
即使線程中所有對象都保證線程安全,如果系統中包含其他資源(數據庫資源、文件資源),那麼整個系統也可能是線程不安全的。
舉個簡單的例子:線程A 和 線程B 都執行如下入庫邏輯:
判斷數據庫數據是否爲空,如果爲空就寫入新數據
即使兩個線程使用兩個不同的數據庫連接,並且兩個數據庫連接都是線程安全的。在執行上述邏輯時,也可能存在 線程A 和線程B 都入庫的場景:
線程A 數據庫連接判斷數據庫數據爲空
線程B 數據庫連接判斷數據庫數據爲空
線程A 通過數據庫連接插入新的數據
線程B 通過數據庫連接插入新的數據
除了數據庫場景,文件資源也可能存在上述問題。