成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

實現(xiàn)線程安全的11種方法,你學會了嗎?

開發(fā) 前端
盡管多線程是一個強大的特性,但它也有代價。在多線程環(huán)境中,我們需要時刻崩著線程安全這根弦,即在不同的線程可以訪問相同的資源,而不會暴露錯誤行為或產(chǎn)生不可預測的結果,這種編程方法被稱為“線程安全”。

Java原生支持多線程,意味著通過在獨立的線程中并發(fā)運行,JVM能夠提升應用程序的性能。

盡管多線程是一個強大的特性,但它也有代價。在多線程環(huán)境中,我們需要時刻崩著線程安全這根弦,即在不同的線程可以訪問相同的資源,而不會暴露錯誤行為或產(chǎn)生不可預測的結果,這種編程方法被稱為“線程安全”。

一、無狀態(tài)實現(xiàn)

在大多數(shù)情況下,多線程中的錯誤是由于多個線程之間錯誤地共享狀態(tài)導致的。

因此,我們首先要探討的方法是:使用無狀態(tài)實現(xiàn)來達到線程安全。

為了更好地理解這種方法,先創(chuàng)建一個簡單的工具類,它有一個靜態(tài)方法用于計算數(shù)字的階乘:

public class MathUtils {

    public static BigInteger factorial(int number) {
        BigInteger f = new BigInteger("1");
        for (int i = 2; i <= number; i++) {
            f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
    }
}

factorial()方法是一個無狀態(tài)的確定性函數(shù):給定特定的輸入,總是得到相同的輸出。

該方法既不依賴外部狀態(tài),也不維護狀態(tài)。因此,它被認為是線程安全的,可以同時被多個線程安全地調用。

所有線程都可以安全地調用factorial()方法,并將獲得預期的結果,而不會相互干擾,也不會改變該方法為其他線程生成的輸出。

因此,無狀態(tài)實現(xiàn)是實現(xiàn)線程安全的最簡單方法。

二、不可變實現(xiàn)

如果我們需要在不同線程之間共享狀態(tài),我們可以通過使類不可變來創(chuàng)建線程安全的類。

不可變性是一個強大的、與語言無關的概念,在Java中很容易實現(xiàn)。在函數(shù)式編程中,很重要的一個技巧就是不可變,參見什么是函數(shù)式編程?。

簡單地說,當一個類實例在構造后其內部狀態(tài)不能被修改時,它就是不可變的。

在Java中創(chuàng)建不可變類的最簡單方法是聲明所有字段為私有且為final,并且不提供設置器:

public class MessageService {

    privatefinal String message;

    public MessageService(String message) {
        this.message = message;
    }

    public String getAndPrint() {
        System.out.println(message);
        return message;
    }

    public String getMessage() {
        return message;
    }
}

一個MessageService對象,在其構造后其狀態(tài)不能改變,所以是線程安全的。

此外,如果MessageService是可變的,但多個線程對其只有只讀權限,它也是線程安全的。

如我們所見,不可變性是實現(xiàn)線程安全的另一種方式。

三、線程局部字段

在面向對象編程(OOP)中,對象實際上需要通過字段維護狀態(tài),并通過一個或多個方法實現(xiàn)行為。

如果我們確實需要維護狀態(tài),我們可以使用線程局部字段來創(chuàng)建線程安全的類,線程局部字段在線程之間就不共享狀態(tài)。

我們可以通過在Thread類中定義私有字段輕松創(chuàng)建字段為線程局部的類。

比如,我們可以定義一個Thread類,它存儲一個整數(shù)數(shù)組:

public class ThreadA extends Thread {

    private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

    @Override
    public void run() {
        numbers.forEach(System.out::println);
    }
}

另一個Thread類可能持有一個字符串數(shù)組:

public class ThreadB extends Thread {
    
    private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");
    
    @Override
    public void run() {
        letters.forEach(System.out::println);
    }
}

在這兩種實現(xiàn)中,類都有自己的狀態(tài),但不與其他線程共享。因此,這些類是線程安全的。

類似地,我們可以通過將ThreadLocal實例分配給一個字段來創(chuàng)建線程局部字段。

考慮以下StateHolder類:

public class StateHolder {
    private String state;

    public StateHolder(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

我們可以很容易地使其成為一個線程局部變量:

public class ThreadState {

    public static final ThreadLocal<StateHolder> statePerThread =
            ThreadLocal.withInitial(() -> new StateHolder("active"));

    public static StateHolder getState() {
        return statePerThread.get();
    }
}

線程局部字段與普通類字段非常相似,不同之處在于每個通過setter/getter訪問它們的線程,都會獲得該字段的獨立初始化副本,以便每個線程都有自己的狀態(tài)。

四、同步集合

我們可以通過使用集合框架中包含的同步包裝器輕松創(chuàng)建線程安全的集合。

比如,我們可以創(chuàng)建一個線程安全的集合:

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12)));
thread1.start();
thread2.start();

請記住,同步集合在每個方法中使用內部鎖,這意味著這些方法一次只能被一個線程訪問,而其他線程將被阻塞,直到第一個線程釋放該方法的鎖。

五、并發(fā)集合

作為同步集合的替代方案,我們可以使用并發(fā)集合來創(chuàng)建線程安全的集合。

Java提供了java.util.concurrent包,其中包含幾個并發(fā)集合,比如ConcurrentHashMap:

Map<String,String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1", "one");
concurrentMap.put("2", "two");
concurrentMap.put("3", "three");

與同步集合不同,并發(fā)集合通過將數(shù)據(jù)分割成段來實現(xiàn)線程安全。例如,在ConcurrentHashMap中,多個線程可以獲取不同映射段的鎖,因此多個線程可以同時訪問該映射。

由于并發(fā)線程訪問的固有優(yōu)勢,并發(fā)集合比同步集合性能更高。

需要注意的是,無論同步集合還是并發(fā)集合,都是集合本身線程安全,其內容并不是。

六、原子對象

我們還可以使用Java提供的原子類集合來實現(xiàn)線程安全,包括AtomicInteger、AtomicLong、AtomicBoolean和AtomicReference。

原子類允許我們執(zhí)行原子操作,這些操作是線程安全的,而無需使用同步。

為了理解這解決了什么問題,讓我們看一下以下Counter類:

public class Counter {

    private int counter = 0;

    public void incrementCounter() {
        counter += 1;
    }

    public int getCounter() {
        return counter;
    }
}

假設在一個競爭條件下,兩個線程同時訪問incrementCounter()方法。

理論上,counter字段的最終值將為2。但我們不能確定結果,因為線程同時執(zhí)行相同的代碼塊,并且遞增操作不是原子的。可以參見在多線程中使用ArrayList會發(fā)生什么?的說明。

讓我們使用AtomicInteger對象創(chuàng)建Counter類的線程安全實現(xiàn):

public class AtomicCounter {

    private final AtomicInteger counter = new AtomicInteger();

    public void incrementCounter() {
        counter.incrementAndGet();
    }

    public int getCounter() {
        return counter.get();
    }
}

這是線程安全的,因為雖然遞增操作++需要多個操作,但incrementAndGet是原子的。

七、同步方法

前面的方法對于集合和基本類型非常有用,但有時我們需要更復雜的控制邏輯。

因此,我們可以使用的另一種常見方法是:實現(xiàn)同步方法來實現(xiàn)線程安全。

簡單地說,一次只能有一個線程訪問同步方法,同時阻止其他線程訪問該方法。其他線程將保持阻塞,直到第一個線程完成或該方法拋出異常。

我們可以通過將incrementCounter()方法變?yōu)橥椒椒▉硪粤硪环N方式創(chuàng)建其線程安全版本:

public synchronized void incrementCounter() {
    counter += 1;
}

我們通過在方法簽名前加上synchronized關鍵字創(chuàng)建了一個同步方法。

由于一次只能有一個線程訪問同步方法,一個線程將執(zhí)行incrementCounter()方法,其他線程將依次執(zhí)行。不會發(fā)生任何重疊執(zhí)行。

同步方法依賴于使用“內部鎖”或“監(jiān)視器”,內部鎖是與特定類實例相關聯(lián)的隱式內部實體。具體參加synchronized 鎖同步。

在多線程上下文中,“監(jiān)視器”只是對鎖在相關對象上執(zhí)行的角色的引用,它強制對一組指定的方法或語句進行獨占訪問。

當一個線程調用同步方法時,它獲取內部鎖,線程執(zhí)行完方法后,會釋放鎖,允許其他線程獲取鎖并訪問該方法。

我們可以在實例方法、靜態(tài)方法和語句(同步語句)中實現(xiàn)同步。

八、同步語句

有時,如果我們只需要使方法的一部分線程安全,同步整個方法可能有些過度。

我們再重構incrementCounter()方法:

public void incrementCounter() {
    // 其他未同步的操作
    synchronized(this) {
        counter += 1; 
    }
}

假設該方法現(xiàn)在執(zhí)行一些其他不需要同步的操作,我們通過將相關的狀態(tài)修改部分包裝在同步塊中來僅同步這部分。

與同步方法不同,同步語句必須指定提供內部鎖的對象,通常是this引用。

同步是有代價的,通過同步代碼塊,能夠僅同步方法的相關部分。

(一)其他對象作為鎖

我們可以通過利用另一個對象作為監(jiān)視器鎖(而不是this)來稍微改進Counter類的線程安全實現(xiàn)。

這不僅在多線程環(huán)境中為共享資源提供了協(xié)調訪問,還使用外部實體來強制對資源的獨占訪問:

public class ObjectLockCounter {

    privateint counter = 0;
    privatefinal Object lock = new Object();

    public void incrementCounter() {
        synchronized (lock) {
            counter += 1;
        }
    }

    public int getCounter() {
        return counter;
    }
}

我們使用一個普通的Object實例來實現(xiàn)互斥,它提高了鎖級別的安全性。

當使用this進行內部鎖時,攻擊者可以通過獲取內部鎖并觸發(fā)拒絕服務(DoS)條件來導致死鎖。

相反,當使用其他對象時,無法從外部訪問這個私有對象,攻擊者很難獲取鎖并導致死鎖。

(二)注意事項

盡管我們可以使用任何Java對象作為內部鎖,但我們應該避免使用String進行鎖定:

public class Class1 {
    privatestaticfinal String LOCK  = "Lock";

    // 使用LOCK作為內部鎖
}

publicclass Class2 {
    privatestaticfinal String LOCK  = "Lock";

    // 使用LOCK作為內部鎖
}

乍一看,似乎這兩個類使用了兩個不同的對象作為它們的鎖。然而,由于字符串駐留,這兩個“Lock”值實際上可能在字符串池中引用同一個對象。也就是說,Class1和Class2共享同一個鎖!

除了String,我們應該避免使用任何可緩存或可重用的對象作為內部鎖。比如,Integer.valueOf()方法緩存小數(shù)字。因此,即使在不同的類中調用Integer.valueOf(1)也會返回同一個對象。

九、易失性字段

同步方法和塊對于解決線程之間的變量可見性問題很方便,即便如此,常規(guī)類字段的值可能會被CPU緩存。因此,即使對特定字段進行了同步更新,其他線程可能也看不到這些更新。

為了防止這種情況,我們可以使用易失性類字段(通過volatile關鍵字標記):

public class Counter {

    private volatile int counter;

    // 標準的構造函數(shù)/獲取器
}

通過使用volatile關鍵字,我們指示JVM和編譯器將counter變量存儲在主內存中。這樣,我們確保每次JVM讀取counter變量的值時,它實際上是從主內存中讀取,而不是從CPU緩存中讀取。同樣,每次JVM寫入counter變量時,值將被寫入主內存。

此外,使用易失性變量確保給定線程可見的所有變量也將從主內存中讀取。

比如:

public class User {

    private String name;
    private volatile int age;

    // 標準的構造函數(shù)/獲取器
}

在這種情況下,每次JVM將age易失性變量寫入主內存時,它也會將非易失性name變量寫入主內存。這確保了兩個變量的最新值都存儲在主內存中,因此對變量的后續(xù)更新將自動對其他線程可見。

類似地,如果一個線程讀取易失性變量的值,該線程可見的所有變量也將從主內存中讀取。

易失性變量提供的這種擴展保證被稱為完全易失性可見性保證。

十、可重入鎖

Java提供了一組改進的鎖實現(xiàn),其行為比上面討論的內部鎖稍微復雜一些。

對于內部鎖,鎖獲取模型相當嚴格:一個線程獲取鎖,然后執(zhí)行一個方法或代碼塊,最后釋放鎖,以便其他線程可以獲取它并訪問該方法。內部鎖沒有實現(xiàn)檢查排隊的線程并優(yōu)先訪問等待時間最長的線程,即屬于非公平鎖。

ReentrantLock實例允許我們做到這一點,防止排隊的線程遭受某些類型的資源饑餓:

public class ReentrantLockCounter {

    privateint counter;
    privatefinal ReentrantLock reLock = new ReentrantLock(true);

    public void incrementCounter() {
        reLock.lock();
        try {
            counter += 1;
        } finally {
            reLock.unlock();
        }
    }

    public int getCounter() {
        return counter;
    }
}

ReentrantLock構造函數(shù)接受一個可選的公平性布爾參數(shù),當設置為true且多個線程試圖獲取鎖時,JVM將優(yōu)先考慮等待時間最長的線程并授予其訪問鎖的權限,即實現(xiàn)公平鎖。

十一、讀寫鎖

我們可以使用讀寫鎖實現(xiàn)來實現(xiàn)線程安全。讀寫鎖實際上使用一對相關聯(lián)的鎖,一個用于只讀操作,另一個用于寫入操作。

因此,只要沒有線程正在寫入資源,就可以有多個線程讀取該資源。此外,寫入資源的線程將阻止其他線程讀取它。

以下是我們如何使用讀寫鎖:

public class ReentrantReadWriteLockCounter {

    privateint counter;
    privatefinal ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    privatefinal Lock readLock = rwLock.readLock();
    privatefinal Lock writeLock = rwLock.writeLock();

    public void incrementCounter() {
        writeLock.lock();
        try {
            counter += 1;
        } finally {
            writeLock.unlock();
        }
    }

    public int getCounter() {
        readLock.lock();
        try {
            return counter;
        } finally {
            readLock.unlock();
        }
    }
}

文末總結

在本文中,我們了解了Java中的線程安全是什么,并深入研究了實現(xiàn)線程安全的11種方法。

責任編輯:武曉燕 來源: 看山的小屋
相關推薦

2024-01-10 07:38:08

2022-06-16 07:50:35

數(shù)據(jù)結構鏈表

2023-12-11 08:03:01

Java線程線程組

2023-10-30 11:40:36

OOM線程池單線程

2023-01-28 09:50:17

java多線程代碼

2024-03-12 08:37:32

asyncawaitJavaScript

2023-12-07 12:29:49

Nginx負載均衡策略

2024-10-06 08:20:53

鎖定機制編程

2024-02-02 11:03:11

React數(shù)據(jù)Ref

2023-08-01 12:51:18

WebGPT機器學習模型

2024-01-02 12:05:26

Java并發(fā)編程

2024-07-29 10:35:44

KubernetesCSI存儲

2024-10-31 09:15:09

2024-02-29 13:12:30

2022-10-13 08:02:13

死鎖運算系統(tǒng)

2022-09-22 12:03:14

網(wǎng)絡安全事件

2023-07-31 07:33:05

JVM調優(yōu)Full GC

2022-03-05 23:29:18

LibuvwatchdogNode.js

2025-06-20 09:57:42

2024-08-12 08:12:38

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 四虎永久在线精品免费一区二 | 麻豆久久久 | 亚洲一区二区三区四区五区午夜 | 中文字幕在线观 | 国产视频久久久 | 久久久久久久久久久久久9999 | 91国内精品久久 | 91看片| 91久久精品一区二区二区 | 日日操日日干 | 欧美久久一区二区三区 | 国产精品亚洲一区二区三区在线 | 国产精品一区2区 | 欧美在线a| 九九久久免费视频 | 亚洲成人在线免费 | 欧美在线视频不卡 | 亚洲精品久久久久久久久久久 | 中文字幕 欧美 日韩 | 日本不卡一区 | 欧美精品日韩精品 | 国产999精品久久久影片官网 | 国产成人精品一区二三区在线观看 | 成人精品国产 | 精品一区二区三区四区 | 国产线视频精品免费观看视频 | 操久久 | av中文天堂 | 中文字幕一区二区三区四区五区 | 国产视频中文字幕 | 欧美一区二 | 日本久久久久久久久 | 天天色综 | 免费久久久 | 99视频在线看| 天天艹日日干 | 玖玖综合在线 | 欧美成人黄色小说 | 一区二区三区视频在线观看 | 国产精品中文字幕在线播放 | 北条麻妃av一区二区三区 |