Java中的執行緒到底有哪些安全策略

語言: CN / TW / HK

本文分享自華為雲社群《【高併發】執行緒安全策略》,作者:冰河。

一、不可變物件

不可變物件需要滿足的條件 - (1)物件建立以後其狀態就不能修改 - (2)物件所有域都是final型別 - (3)物件是正確建立的(在物件建立期間,this引用沒有溢位) 對於不可變物件,可以參見JDK中的String類

final關鍵字:類、方法、變數 - (1)修飾類:該類不能被繼承,String類,基礎型別的包裝類(比如Integer、Long等)都是final型別。final類中的成員變數可以根據需要設定為final型別,但是final類中的所有成員方法,都會被隱式的指定為final方法。 - (2)修飾方法:鎖定方法不被繼承類修改;效率。注意:一個類的private方法會被隱式的指定為final方法 - (3)修飾變數:基本資料型別變數(數值被初始化後不能再修改)、引用型別變數(初始化之後則不能再指向其他的物件)

在JDK中提供了一個Collections類,這個類中提供了很多以unmodifiable開頭的方法,如下: Collections.unmodifiableXXX: Collection、List、Set、Map… 其中Collections.unmodifiableXXX方法中的XXX可以是Collection、List、Set、Map… 此時,將我們自己建立的Collection、List、Set、Map,傳遞到Collections.unmodifiableXXX方法中,就變為不可變的了。此時,如果修改Collection、List、Set、Map中的元素就會丟擲java.lang.UnsupportedOperationException異常。

在Google的Guava中,包含了很多以Immutable開頭的類,如下: ImmutableXXX,XXX可以是Collection、List、Set、Map… 注意:使用Google的Guava,需要在Maven中新增如下依賴包:

<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency>

二、執行緒封閉

  • (1)Ad-hoc執行緒封閉:程式控制實現,最糟糕,忽略
  • (2)堆疊封閉:區域性變數,無併發問題
  • (3)ThreadLocal執行緒封閉:特別好的封閉方法

三、執行緒不安全類與寫法

1. StringBuilder -> StringBuffer

StringBuilder:執行緒不安全;

StringBuffer:執行緒不安全;

字串拼接涉及到多執行緒操作時,使用StringBuffer實現。

在一個具體的方法中,定義一個字串拼接物件,此時可以使用StringBuilder實現。因為在一個方法內部定義區域性變數進行使用時,屬於堆疊封閉,只有一個執行緒會使用變數,不涉及多執行緒對變數的操作,使用StringBuilder即可。

2. SimpleDateFormat -> JodaTime

SimpleDateFormat:執行緒不安全,可以將其物件的例項化放入到具體的時間格式化方法中,實現執行緒安全

JodaTime:執行緒安全

SimpleDateFormat執行緒不安全的程式碼示例如下:

``` package io.binghe.concurrency.example.commonunsafe; import lombok.extern.slf4j.Slf4j;

import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class DateFormatExample { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); //請求總數 public static int clientTotal = 5000; //同時併發執行的執行緒數 public static int threadTotal = 200;

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    final Semaphore semaphore = new Semaphore(threadTotal);
    final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
    for(int i = 0; i < clientTotal; i++){
        executorService.execute(() -> {
            try{
                semaphore.acquire();
                update();
                semaphore.release();
            }catch (Exception e){
                log.error("exception", e);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
}
public static void update(){
    try {
        simpleDateFormat.parse("20191024");
    } catch (ParseException e) {
        log.error("parse exception", e);
    }
}

} ```

修改成如下程式碼即可。

``` package io.binghe.concurrency.example.commonunsafe;

import lombok.extern.slf4j.Slf4j;

import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class DateFormatExample2 { //請求總數 public static int clientTotal = 5000; //同時併發執行的執行緒數 public static int threadTotal = 200;

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    final Semaphore semaphore = new Semaphore(threadTotal);
    final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
    for(int i = 0; i < clientTotal; i++){
        executorService.execute(() -> {
            try{
                semaphore.acquire();
                update();
                semaphore.release();
            }catch (Exception e){
                log.error("exception", e);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
}

public static void update(){
    try {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        simpleDateFormat.parse("20191024");
    } catch (ParseException e) {
        log.error("parse exception", e);
    }
}

} ```

對於JodaTime需要在Maven中新增如下依賴包:

<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9</version> </dependency>

示例程式碼如下:

``` package io.binghe.concurrency.example.commonunsafe; import lombok.extern.slf4j.Slf4j; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter;

import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore;

@Slf4j public class DateFormatExample3 { //請求總數 public static int clientTotal = 5000; //同時併發執行的執行緒數 public static int threadTotal = 200;

private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    final Semaphore semaphore = new Semaphore(threadTotal);
    final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
    for(int i = 0; i < clientTotal; i++){
        final int count = i;
        executorService.execute(() -> {
            try{
                semaphore.acquire();
                update(count);
                semaphore.release();
            }catch (Exception e){
                log.error("exception", e);
            }
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
}

public static void update(int i){
    log.info("{} - {}", i, DateTime.parse("20191024", dateTimeFormatter));
}

} ```

3. ArrayList、HashSet、HashMap等Collections集合類為執行緒不安全類

4. 先檢查再執行:if(condition(a)){handle(a);}

注意:這種寫法是執行緒不安全的!!!!!

兩個執行緒同時執行這種操作,同時對if條件進行判斷,並且a變數是執行緒共享的,如果兩個執行緒均滿足if條件,則兩個執行緒會同時執行handle(a)語句,此時,handle(a)語句就可能不是執行緒安全的。

不安全的點在於兩個操作中,即使前面的執行過程是執行緒安全的,後面的過程也是執行緒安全的,但是前後執行過程的間隙不是原子性的,因此,也會引發執行緒不安全的問題。

實際過程中,遇到if(condition(a)){handle(a);}類的處理時,考慮a是否是執行緒共享的,如果是執行緒共享的,則需要在整個執行方法上加鎖,或者保證if(condition(a)){handle(a);}的前後兩個操作(if判斷和程式碼執行)是原子性的。

四、執行緒安全-同步容器

1. ArrayList -> Vector, Stack

ArrayList:執行緒不安全;

Vector:同步操作,但是可能會出現執行緒不安全的情況,執行緒不安全的程式碼示例如下:

``` public class VectorExample {

private static Vector<Integer> vector = new Vector<>();

public static void main(String[] args) throws InterruptedException {
    while (true){
        for(int i = 0; i < 10; i++){
            vector.add(i);
        }
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < vector.size(); i++){
                    vector.remove(i);
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < vector.size(); i++){
                    vector.get(i);
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

} ```

Stack:繼承自Vector,先進後出。

2. HashMap -> HashTable(Key, Value都不能為null)

HashMap:執行緒不安全;

HashTable:執行緒安全,注意使用HashTable時,Key, Value都不能為null;

3. Collections.synchronizedXXX(List、Set、Map)

注意:在遍歷集合的時候,不要對集合進行更新操作。當需要對集合中的元素進行刪除操作時,可以遍歷集合,先對需要刪除的元素進行標記,集合遍歷結束後,再進行刪除操作。例如,下面的示例程式碼:

``` public class VectorExample3 {

//此方法丟擲:java.util.ConcurrentModificationException
private static void test1(Vector<Integer> v1){
    for(Integer i : v1){
        if(i == 3){
            v1.remove(i);
        }
    }
}
//此方法丟擲:java.util.ConcurrentModificationException
private static void test2(Vector<Integer> v1){
    Iterator<Integer> iterator = v1.iterator();
    while (iterator.hasNext()){
        Integer i = iterator.next();
        if(i == 3){
            v1.remove(i);
        }
    }
}
//正常
private static void test3(Vector<Integer> v1){
    for(int i = 0; i < v1.size(); i++){
        if(i == 3){
            v1.remove(i);
        }
    }
}
public static void main(String[] args) throws InterruptedException {
    Vector<Integer> vector = new Vector<>();
    vector.add(1);
    vector.add(2);
    vector.add(3);

    //test1(vector);
    //test2(vector);
    test3(vector);
}

} ```

五、執行緒安全-併發容器J.U.C

J.U.C表示的是java.util.concurrent報名的縮寫。

1. ArrayList -> CopyOnWriteArrayList

ArrayList:執行緒不安全;

CopyOnWriteArrayList:執行緒安全;

寫操作時複製,當有新元素新增到CopyOnWriteArrayList陣列時,先從原有的陣列中拷貝一份出來,然後在新的陣列中進行寫操作,寫完之後再將原來的陣列指向到新的陣列。整個操作都是在鎖的保護下進行的。

CopyOnWriteArrayList缺點:

  • (1)每次寫操作都需要複製一份,消耗記憶體,如果元素特別多,可能導致GC;
  • (2)不能用於實時讀的場景,適合讀多寫少的場景; CopyOnWriteArrayList設計思想:
  • (1)讀寫分離
  • (2)最終一致性
  • (3)使用時另外開闢空間,解決併發衝突

注意:CopyOnWriteArrayList讀操作時,都是在原陣列上進行的,不需要加鎖,寫操作時複製,當有新元素新增到CopyOnWriteArrayList陣列時,先從原有的集合中拷貝一份出來,然後在新的陣列中進行寫操作,寫完之後再將原來的陣列指向到新的陣列。整個操作都是在鎖的保護下進行的。

2.HashSet、TreeSet -> CopyOnWriteArraySet、ConcurrentSkipListSet

CopyOnWriteArraySet:執行緒安全的,底層實現使用了CopyOnWriteArrayList。

ConcurrentSkipListSet:JDK6新增的類,支援排序。可以在構造時,自定義比較器,基於Map集合。在多執行緒環境下,ConcurrentSkipListSet中的contains()方法、add()、remove()、retain()等操作,都是執行緒安全的。但是,批量操作,比如:containsAll()、addAll()、removeAll()、retainAll()等操作,並不保證整體一定是原子操作,只能保證批量操作中的每次操作是原子性的,因為批量操作中是以迴圈的形式呼叫的單步操作,比如removeAll()操作下以迴圈的方式呼叫remove()操作。如下程式碼所示:

//ConcurrentSkipListSet型別中的removeAll()方法的原始碼 public boolean removeAll(Collection<?> c) { // Override AbstractSet version to avoid unnecessary call to size() boolean modified = false; for (Object e : c) if (remove(e)) modified = true; return modified; }

所以,在執行ConcurrentSkipListSet中的批量操作時,需要考慮加鎖問題。

注意:ConcurrentSkipListSet類不允許使用空元素(null)。

3. HashMap、TreeMap -> ConcurrentHashMap、ConcurrentSkipListMap

ConcurrentHashMap:執行緒安全,不允許空值

ConcurrentSkipListMap:是TreeMap的執行緒安全版本,內部是使用SkipList跳錶結構實現

4.ConcurrentSkipListMap與ConcurrentHashMap對比如下

  • (1)ConcurrentSkipListMap中的Key是有序的,ConcurrentHashMap中的Key是無序的;
  • (2)ConcurrentSkipListMap支援更高的併發,對資料的存取時間和執行緒數幾乎無關,也就是說,在資料量一定的情況下,併發的執行緒數越多,ConcurrentSkipListMap越能體現出它的優勢。

注意:在非對執行緒下儘量使用TreeMap,另外,對於併發數相對較低的並行程式,可以使用Collections.synchronizedSortedMap,將TreeMap進行包裝;對於高併發程式,使用ConcurrentSkipListMap提供更高的併發度;在多執行緒高併發環境中,需要對Map的鍵值對進行排序,儘量使用ConcurrentSkipListMap。

六、安全共享物件的策略-總結

  • (1)執行緒限制:一個被執行緒限制的物件,由執行緒獨佔,並且只能被佔有它的執行緒修改
  • (2)共享只讀:一個共享只讀的物件,在沒有額外同步的情況下,可以被多個執行緒併發訪問,但是任何執行緒都不能修改它。
  • (3)執行緒安全物件:一個執行緒安全的物件或者容器,在內部通過同步機制來保證執行緒安全,所以其他執行緒無需額外的同步就可以通過公共介面隨意訪問它
  • (4)被守護物件:被守護物件只能通過獲取特定的鎖來訪問

點選關注,第一時間瞭解華為雲新鮮技術~