隨著硬件技術的快速發展(比如多核處理器,超線程技術),我們通常會在代碼中使用多線程(比如線程池)來提高性能,但是,多線程又會帶來線程安全問題。因此,本文將深入探討Java中的線程安全問題。
首先,我們來看看維基百科對線程安全是如何描述的,如下圖:
總結一下:線程安全(Thread Safety)是指多個線程訪問共享資源時,不會破壞資源的完整性。如下圖:
請注意,導致線程安全問題一定要同時具備以下 3個條件,缺一不可:
上面從表象上說明線程安全需要具備的 3個條件,在 Java中,線程安全性通常涉及以下 3個指標:
在 Java中,造成線程安全問題的根因是硬件結構,為了消除 CPU和主內存之間的硬件速度差,通常會在兩者之間設置多級緩存(L1 ~ L3),如下圖:
Java為了適配這種多級緩存的硬件構造,設計了一套與之對應的內存模型(JMM,Java memory model,包括主內存和工作內存,如下圖:
線程對變量的所有操作(讀取、寫入)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。線程之間無法直接訪問對方的工作內存,變量的傳遞需要通過主內存來完成。
關于 Java內存模型的原理,我們會在另外的文章中單獨講解,本文只是概要性的總結。
在數據庫事務ACID中也有原子性(Atomicity)的概念,它是指一個操作是不可分割的,即要么全部執行,要么全部不執行。Java線程安全中的原子性與數據庫事務中的原子性本質是一樣的,只是它們應用的上下文和具體實現有所不同。
Java提供了多種方式來保證原子性,比如 同步塊、鎖或者原子類。
為了更好的說明原子性,我們這里以一個反例來展示不具備原子性,如下代碼:
public class AtomicityTest { private int i = 0; public void increment() { i++; }}
在上述代碼中,i++這種寫法在我們的日常開發經常使用,但它不是一個原子操作,實際上i++分為三步:
如果多個線程同時執行increment()方法,可能會導致i的值不正確,比如有 3個線程A,B,C:
3個線程都對i進行i++操作,預期i的最終值是 3,但因為i++無法保證原子性,因此,i最終的值未達到預期的值。
可見性是指一個線程對共享變量的修改,其他線程能立刻看到。在Java中,volatile關鍵字可以保證變量的可見性。
為了更好的說明可見性,我們這里以一個示例進行分析,如下代碼:
public class VisibilityTest { private boolean running = true; public void stop() { running = false; } public void run() { while (running) { // do something } }}
在上述代碼中,變量running是一個全局變量,如果沒有使用volatile關鍵字,running 變量的修改可能不會被其他線程立即看到。
有序性是指程序代碼的執行順序。在單線程環境中,代碼的執行順序通常是按照代碼的書寫順序執行的。然而,在多線程環境中,編譯器、JVM和CPU可能會為了優化性能進行指令重排序(Instruction Reordering),這可能會導致代碼的執行順序與預期不一致。
Java內存模型(Java Memory Model, JMM)允許編譯器和處理器進行指令重排序,但會保證單線程內的執行結果和多線程內的同步結果是正確的。
這里以一個反例來展示不具備有序性,如下代碼:
public class ReorderingExample {private int x = 0;private boolean flag = false; public void writer() { x = 42; flag = true; } public void read() { if (flag) { System.out.println(x); // 可能輸出0 } }}
在上述代碼中,read()方法可能會看到flag=true,但x仍然為 0,因為編譯器或CPU可能對指令進行重排序。
在 Java中,通常可以通過以下幾個方式來保證線程安全。
(1) synchronized關鍵字
synchronized是Java的一個原語關鍵字,它可以保證方法或代碼塊在同一時刻只能被一個線程執行,從而確保原子性和可見性。
下面的代碼是synchronized關鍵字的簡單使用:
public class SynchronizedTest {private int i = 0; public synchronized void increment() { i++; } public synchronized int getCount() { return i; }}
(2) Lock 接口
Lock接口提供了比synchronized更靈活的鎖機制,常用的實現類有 ReentrantLock 可重入鎖。
下面的代碼是Lock關鍵字的簡單使用:
import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class LockCounter {private int count = 0;private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } }}
(3) 原子類
Java提供了一些原子類,如 AtomicInteger、AtomicLong 和 AtomicReference,它們通過CAS(Compare-And-Swap)操作實現了非阻塞的線程安全。
下面的代碼是AtomicInteger原子類的簡單使用:
import java.util.concurrent.atomic.AtomicInteger;public class AtomicTest {private AtomicInteger atomic = new AtomicInteger(); public void increment() { atomic.incrementAndGet(); } public int getCount() { return atomic.get(); }}
(4) ThreadLocal 類
ThreadLocal類提供了線程局部變量,每個線程都有自己獨立的變量副本,從而避免了共享數據的競爭。
下面的代碼是ThreadLocal類的簡單使用:
public class ThreadLocalExample {private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1); public int getValue() { return threadLocal.get(); } public void setValue(int value) { threadLocal.set(value); }}
(5) 分布式鎖
Redis 分布式鎖 或者 Zookeeper分布式鎖是分布式環境下保證線程安全的常用方法。關于兩種分布式鎖的原理,會在其他的文章詳細分析。
線程安全是 Java多線程編程中很重要的一部分,本文講解了什么是線程安全以及產生線程安全問題的根因,并且通過原子性,有序性,可見性對線程安全進行了分析。
本文鏈接:http://www.tebozhan.com/showinfo-26-89393-0.html到底什么是線程安全? 如何保證線程安全?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com