Java 中的線程安全是一個非常重要的主題。Java 通過 Java Threads 提供多線程環境支持,我們知道來自同一個對象的多個線程共享對象變量,這可能導致數據不一致性,當這些線程用於讀取和更新共享數據時。
線程安全
數據不一致性的原因是因為更新任何字段值不是一個原子過程,它需要三個步驟;首先讀取當前值,其次進行必要的操作以獲取更新後的值,最後將更新後的值分配給字段引用。讓我們通過一個簡單的程序來檢查這一點,其中多個線程正在更新共享數據。
package com.journaldev.threads;
public class ThreadSafety {
public static void main(String[] args) throws InterruptedException {
ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
//等待線程完成處理
t1.join();
t2.join();
System.out.println("Processing count="+pt.getCount());
}
}
class ProcessingThread implements Runnable{
private int count;
@Override
public void run() {
for(int i=1; i < 5; i++){
processSomething(i);
count++;
}
}
public int getCount() {
return this.count;
}
private void processSomething(int i) {
//處理一些工作
try {
Thread.sleep(i*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的程序的 for 循環中,count 被增加 1 四次,由於有兩個線程,它的值應該在兩個線程都執行完畢後為 8。但是,當你多次運行上面的程序時,你會注意到 count 的值在 6、7、8 之間變化。這是因為即使count++ 看似是一個原子操作,它實際上不是,導致數據損壞。
Java 中的線程安全性
Java 中的線程安全性是確保我們的程式在多線程環境中安全使用的過程,有多種方式可以確保我們的程式是線程安全的。
- 同步化是在 Java 中實現線程安全性的最簡單且最廣泛使用的工具。
- 使用來自 java.util.concurrent.atomic 套件的 Atomic Wrapper 類別。例如 AtomicInteger。
- 使用來自 java.util.concurrent.locks 套件的鎖定機制。
- 使用線程安全的集合類別,可以查看此文章以了解 ConcurrentHashMap 的使用方法來實現線程安全性。
- 使用 volatile 關鍵字與變數,以便讓每個線程從記憶體中讀取資料,而不是從線程快取中讀取。
Java 同步化
同步化是我們可以實現線程安全性的工具,JVM 保證同步化的程式碼將僅由一個線程同時執行。Java 關鍵字 synchronized 用於創建同步化的程式碼,內部使用對象或類別上的鎖定來確保僅有一個線程執行同步化的程式碼。
- Java同步是基于资源的锁定和解锁工作的,任何线程进入同步代码之前,都必须获取对象上的锁,当代码执行结束时,它会解锁资源,其他线程可以锁定该资源。与此同时,其他线程处于等待状态以锁定同步资源。
- 我们可以使用`synchronized`关键字有两种方式,一种是使整个方法同步,另一种是创建同步块。
- 当一个方法被同步时,它锁定对象,如果方法是静态的,则锁定类,因此最好的做法是使用同步块来锁定需要同步的方法的部分。
- 在创建同步块时,我们需要提供将获得锁的资源,它可以是`XYZ.class`或类的任何对象字段。
synchronized(this)
将在进入同步块之前锁定对象。- 应该使用最低级别的锁定,例如,如果在类中有多个同步块,其中一个锁定了对象,那么其他同步块也将不可供其他线程执行。当我们锁定一个对象时,它会锁定对象的所有字段。
- Java同步在性能成本上提供了数据完整性,因此只有在绝对必要时才应使用。
- Java同步仅在同一JVM中工作,因此如果您需要在多个JVM环境中锁定某些资源,则它将无法正常工作,您可能需要考虑一些全局锁定机制。
- Java同步可能导致死锁,请查看有关Java死锁及如何避免的帖子。Java中的死锁及其避免方法。
- Java的synchronized关键字不能用于构造函数和变量。
- 最好创建一个虚拟的私有Object来用于synchronized块,以便其他代码无法更改它的引用。例如,如果您有一个用于同步的Object的setter方法,它的引用可能会被其他代码更改,导致同步块的并行执行。
- 我们不应该使用任何在常量池中维护的对象,例如String不应该用于同步,因为如果其他代码也在同一个String上加锁,它将尝试获取来自字符串池的相同引用对象的锁,即使这两段代码没有关联,它们也会互相锁定。
下面是我们需要在上面的程序中进行的代码更改,以使其线程安全。
//用于同步的虚拟对象变量
private Object mutex=new Object();
...
//使用同步块来同步读取、增加和更新计数值
synchronized (mutex) {
count++;
}
让我们看一些同步的示例以及我们可以从中学到什么。
public class MyObject {
//锁定对象的监视器
public synchronized void doSomething() {
// ...
}
}
//黑客的代码
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
//无限延迟myObject
Thread.sleep(Integer.MAX_VALUE);
}
}
請注意,駭客的代碼正在試圖鎖定myObject實例,一旦獲得鎖定,就永遠不會釋放它,導致doSomething()方法在等待鎖定時被阻塞,這將導致系統陷入死鎖並引發拒絕服務(DoS)攻擊。
public class MyObject {
public Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// ...
}
}
}
//不受信任的代碼
MyObject myObject = new MyObject();
//更改鎖定對象引用
myObject.lock = new Object();
請注意,鎖定對象是公共的,通過更改其引用,我們可以在多個線程中並行執行同步塊。如果您擁有私有對象但具有更改其引用的setter方法,則情況類似。
public class MyObject {
//鎖定類對象的監視器
public static synchronized void doSomething() {
// ...
}
}
//駭客代碼
synchronized (MyObject.class) {
while (true) {
Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
}
}
請注意,駭客代碼正在獲取類監視器的鎖定並且不釋放它,這將導致系統死鎖和DoS。這裡是另一個例子,多個線程正在處理相同的字符串數組,一旦處理完成,將線程名稱附加到數組值上。
package com.journaldev.threads;
import java.util.Arrays;
public class SyncronizedMethod {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1","2","3","4","5","6"};
HashMapProcessor hmp = new HashMapProcessor(arr);
Thread t1=new Thread(hmp, "t1");
Thread t2=new Thread(hmp, "t2");
Thread t3=new Thread(hmp, "t3");
long start = System.currentTimeMillis();
//啟動所有線程
t1.start();t2.start();t3.start();
//等待線程完成
t1.join();t2.join();t3.join();
System.out.println("Time taken= "+(System.currentTimeMillis()-start));
//現在檢查共享變量值
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class HashMapProcessor implements Runnable{
private String[] strArr = null;
public HashMapProcessor(String[] m){
this.strArr=m;
}
public String[] getMap() {
return strArr;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
private void processArr(String name) {
for(int i=0; i < strArr.length; i++){
//處理數據並附加線程名稱
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +":"+name;
}
private void processSomething(int index) {
//處理一些工作
try {
Thread.sleep(index*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
當我運行上述程序時,這是輸出。
Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]
字符串數組的值已損壞,因為存在共享數據且未同步。這是我們如何更改addThreadName()方法以使我們的程序線程安全的方法。
private Object lock = new Object();
private void addThreadName(int i, String name) {
synchronized(lock){
strArr[i] = strArr[i] +":"+name;
}
}
在這個更改之後,我們的程序運行正常,這是程序的正確輸出。
Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]
那就是 Java 中關於線程安全的全部內容,希望你了解了關於線程安全編程和使用 synchronized 關鍵字。
Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java