<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    Java并發:五種線程安全類型、線程安全的實現、枚舉類型

    VSole2021-11-05 16:29:15

    1. Java中的線程安全

    • Java線程安全:狹義地認為是多線程之間共享數據的訪問。
    • Java語言中各種操作共享的數據有5種類型:不可變、絕對線程安全、相對線程安全、線程兼容、線程獨立

    ① 不可變

    • 不可變(Immutable) 的對象一定是線程安全的,不需要再采取任何的線程安全保障措施。
    • 只要能正確構建一個不可變對象,該對象永遠不會在多個線程之間出現不一致的狀態。
    • 多線程環境下,應當盡量使對象成為不可變,來滿足線程安全。

    如何實現不可變?

    • 如果共享數據是基本數據類型,使用final關鍵字對其進行修飾,就可以保證它是不可變的。
    • 如果共享數據是一個對象,要保證對象的行為不會對其狀態產生任何影響。
    • String是不可變的,對其進行substring()、replace()、concat()等操作,返回的是新的String對象,原始的String對象的值不受影響。而如果對StringBuffer或者StringBuilder對象進行substring()、replace()、append()等操作,直接對原對象的值進行改變。
    • 要構建不可變對象,需要將內部狀態變量定義為final類型。如java.lang.Integer類中將value定義為final類型。
    private final int value;
    

    常見的不可變的類型:

    • final關鍵字修飾的基本數據類型
    • 枚舉類型、String類型
    • 常見的包裝類型:Short、Integer、Long、Float、Double、Byte、Character等
    • 大數據類型:BigInteger、BigDecimal
    注意:原子類 AtomicInteger 和 AtomicLong 則是可變的。

    對于集合類型,可以使用 Collections.unmodifiableXXX() 方法來獲取一個不可變的集合。

    • 通過Collections.unmodifiableMap(map)獲的一個不可變的Map類型。
    • Collections.unmodifiableXXX() 先對原始的集合進行拷貝,需要對集合進行修改的方法都直接拋出異常。

    例如,如果獲得的不可變map對象進行put()、remove()、clear()操作,則會拋出UnsupportedOperationException異常。

    ② 絕對線程安全

    絕對線程安全的實現,通常需要付出很大的、甚至不切實際的代價。

    Java API中提供的線程安全,大多數都不是絕對線程安全。

    例如,對于數組集合Vector的操作,如get()、add()、remove()都是有synchronized關鍵字修飾。有時調用時也需要手動添加同步手段,保證多線程的安全。

    下面的代碼看似不需要同步,實際運行過程中會報錯。

    import java.util.Vector;
    
    /**
     * @Author: lucy
     * @Version 1.0
     */
    public class VectorTest {
        public static void main(String[] args) {
            Vector vector = new Vector<>();
            while(true){
                for (int i = 0; i < 10; i++) {
                    vector.add(i);
                }
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < vector.size(); i++) {
                            System.out.println("獲取vector的第" + i + "個元素: " + vector.get(i));
                        }
                    }
                }).start();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i=0;i                        System.out.println("刪除vector中的第" + i+"個元素");
                            vector.remove(i);
                        }
                    }
                }).start();
                while (Thread.activeCount()>20)
                    return;
            }
        }
    }
    

    出現ArrayIndexOutOfBoundsException異常,原因:某個線程恰好刪除了元素i,使得當前線程無法訪問元素i。

    Exception in thread "Thread-1109" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1
     at java.util.Vector.remove(Vector.java:831)
     at VectorTest$2.run(VectorTest.java:28)
     at java.lang.Thread.run(Thread.java:745)
    

    需要將對元素的get和remove構造成同步代碼塊:

    synchronized (vector){
        for (int i = 0; i < vector.size(); i++) {
            System.out.println("獲取vector的第" + i + "個元素: " + vector.get(i));
        }
    }
    synchronized (vector){
        for (int i=0;i        System.out.println("刪除vector中的第" + i+"個元素");
            vector.remove(i);
        }
    }
    

    ③ 相對線程安全

    • 相對線程安全需要保證對該對象的單個操作是線程安全的,在必要的時候可以使用同步措施實現線程安全。
    • 大部分的線程安全類都屬于相對線程安全,如Java容器中的Vector、HashTable、通過Collections.synchronizedXXX()方法包裝的集合。

    ④ 線程兼容

    • Java中大部分的類都是線程兼容的,通過添加同步措施,可以保證在多線程環境中安全使用這些類的對象。
    • 如常見的ArrayList、HashTableMap都是線程兼容的。

    ⑤ 線程對立

    • 線程對立是指:無法通過添加同步措施,實現多線程中的安全使用。
    • 線程對立的常見操作有:Thread類的suspend()和resume()(已經被JDK聲明廢除),System.setIn()System.setOut()等。

    2. Java的枚舉類型

    通過enum關鍵字修飾的數據類型,叫枚舉類型。

    • 枚舉類型的每個元素都有自己的序號,通常從0開始編號。
    • 可以通過values()方法遍歷枚舉類型,通過name()或者toString()獲取枚舉類型的名稱
    • 通過ordinal()方法獲取枚舉類型中元素的序號
    public class EnumData {
        public static void main(String[] args) {
            for (Family family : Family.values()) {
                System.out.println(family.name() + ":" + family.ordinal());
            }
        }
    }
    
    enum Family {
        GRADMOTHER, GRANDFATHER, MOTHER, FATHER, DAUGHTER, SON;
    }
    

    可以將枚舉類型看做普通的class,在里面定義final類型的成員變量,便可以為枚舉類型中的元素賦初值。

    要想獲取枚舉類型中元素實際值,需要為成員變量添加getter方法。

    雖然枚舉類型的元素有了自己的實際值,但是通過ordinal()方法獲取的元素序號不會發生改變。

    public class EnumData {
        public static void main(String[] args) {
            for (Family family : Family.values()) {
                System.out.println(family.name() + ":實際值" + family.getValue() +
                        ", 實際序號" + family.ordinal());
            }
        }
    }
    enum Family {
        GRADMOTHER(3), GRANDFATHER(4), MOTHER(1), FATHER(2), DAUGHTER(5), SON(6);
        private final int value;
        Family(int value) {
            this.value = value;
        }
        public int getValue() {
            return value;
        }
    }
    

    3. Java線程安全的實現

    ① 互斥同步

    互斥同步(Mutex Exclusion & Synchronization)是一種常見的并發正確性保障手段。

    • 同步:多個線程并發訪問共享數據,保證共享數據同一時刻只被一個(或者一些,使用信號量)線程使用。
    • 互斥:互斥是實現同步的一種手段,主要的互斥實現方式:臨界區(Critical Section)、互斥量(Mutex)、信號量(Semaphore)。

    同步與互斥的關系:

    • 互斥是原因,同步是結果。
    • 同步是目的,互斥是方法。

    Java中,最基本的實現互斥同步的手段是synchronized關鍵字,其次是JUC包中的ReentrantLock。

    關于synchronized關鍵字:

    • 編譯后的同步塊,開始處會添加monitorenter指令,結束處或異常處會添加monitorexit指令。
    • monitorenter和monitorexit指令中都包含一個引用類型的參數,分別指向加鎖或解鎖的對象。如果是同步代碼塊,則為synchronized括號中明確指定的對象;如果為普通方法,則為當前實例對象;如果為靜態方法,則為類對應的class對象。
    • JVM執行monitorenter指令時,要先嘗試獲取鎖:如果對象沒被鎖定或者當前線程已經擁有該對象的鎖,則鎖計數器加1;否則獲取鎖失敗,進入阻塞狀態,等待持有鎖的線程釋放鎖。
    • JVM執行monitorexit指令時,鎖計數器減1,直到計數器的值為0,鎖被釋放。(synchronized是支持重進入的)
    • 由于阻塞或者喚醒線程都需要從用戶態(User Mode)切換到核心態(Kernel Mode),有時鎖只會被持有很短的時間,沒有必要進行狀態轉換。可以讓線程在阻塞之前先自旋等待一段時間,超時未獲取到鎖才進入阻塞狀態,這樣可以避免頻繁的切入到核心態。其實,就是后面自旋鎖的思想。

    關于ReentrantLock:

    • 與synchronized關鍵字相比,它是API層面的互斥鎖(lock()、unlock()、try...finally)。
    • 與synchronized關鍵字相比,具有可中斷、支持公平與非公平性、可綁定多個Condition對象的高級功能。
    • 由于synchronized關鍵字被優化,二者的性能差異并不是很大,如果不是想使用ReentrantLock的高級功能,優先考慮使用synchronized關鍵字。

    ② 非阻塞同步

    (1)CAS概述

    互斥同步最大的性能問題是線程的阻塞和喚醒,因此又叫阻塞同步。

    互斥同步采用悲觀并發策略:

    • 多線程并發訪問共享數據時,總是認為只要不加正確的同步措施,肯定會出現問題。
    • 無論共享數據是否存在競爭,都會執行加鎖、用戶態和心態的切換、維護鎖計數器、檢查是否有被阻塞的線程需要喚醒等操作。

    隨著硬件指令集的發展,我們可以采用基于沖突檢測的樂觀并發策略:

    • 先進行操作,如果不存在沖突(即沒有其他線程爭用共享數據),則操作成功。
    • 如果有其他線程爭用共享數據,產生了沖突,使用其他的補償措施。
    • 常見的補償措施:不斷嘗試,直到成功為止,比如循環的CAS操作。

    樂觀并發策略的許多實現都不需要將線程阻塞,這種同步操作叫做非阻塞同步。

    非阻塞同步依靠的硬件指令集:前三條是比較久遠的指令,后兩條是現代處理器新增的。

    • 測試和設置(Test and Set)
    • 獲取并增加(Fetch and Increment)
    • 交換(Swap)
    • 比較并交換(Compare and Swap,即CAS)
    • 加載鏈接/條件存儲(Load Linked/ Store Conditional,即LL/SC)

    什么是CAS?

    • CAS,即Compare and Swap,需要借助處理器的cmpxchg指令完成。
    • CAS指令需要三個操作數:內存位置V(Java中可以簡單的理解為變量的內存地址)、舊的期待值A、新值B。
    • CAS指令執行時,當且僅當V符合舊的預期值A,處理器才用新值B更新V的值;否則,不執行更新。
    • 不管是否更新V的值,都返回V的舊值,整個處理過程是一個原子操作。

    原子操作:所謂的原子操作是指一個或一系列不可被中斷的操作。

    Java中的CAS操作:

    • Java中的CAS操作由sun.misc.Unsafe中的compareAndSwapInt()、compareAndSwapLong()等幾個方法包裝提供。實際無法調用這些方法,需要采用反射機制才能使用。
    • 在實際的開發過程中,一般通過其他的Java API調用它們,如JUC包原子類中的compareAndSet(expect, update) 、getAndIncrement()等方法。這些方法內部都使用了Unsafe類的CAS操作。
    • Unsafe類的CAS操作,通過JVM的即時編譯器編譯后,是一條與平臺相關的CAS指令。

    除了偏向鎖,Java中其他鎖的實現方式都是用了循環的CAS操作。學習資料:Java進階視頻資源

    (2)通過循環的CAS實現原子操作

    通過++i或者i++可以實現計數器的自增,在多線程環境下,這樣使用是非線程安全的。

    public class UnsafeCount {
        private int i = 0;
        private static final int THREADS_COUNT = 200;
    
        public static void main(String[] args) {
            Thread[] threads = new Thread[THREADS_COUNT];
            UnsafeCount counter = new UnsafeCount();
            for (int i = 0; i < THREADS_COUNT; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 10000; j++) {
                            counter.count();
                        }
                    }
                });
                threads[i].start();
            }
            while (Thread.activeCount() > 1) {
                Thread.yield();
            }
            System.out.println("多線程調用計數器i,運行后的值為: " + counter.i);
        }
    
        public void count() {
            i++;
        }
    }
    

    運行以上的代碼發現:當線程數量增加,每個線程調用計數器的次數變大時,每次運行的結果是錯誤且不固定的。

    為了實現實在一個多線程環境下、線程安全的計數器,需要使用AtomicInteger的原子自增運算。

    import java.util.concurrent.atomic.AtomicInteger;
    public class SafeCount {
        private AtomicInteger atomic = new AtomicInteger(0);
        private static final int THREAD_COUNT = 200;
        public static void main(String[] args) {
            SafeCount counter = new SafeCount();
            Thread[] threads = new Thread[THREAD_COUNT];
            for (int i = 0; i < THREAD_COUNT; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j=0;j<10000;j++){
                            counter.count();
                        }
                    }
                });
                threads[i].start();
            }
            while (Thread.activeCount()>1){
                Thread.yield();
            }
            System.out.println("多線程調用線程安全的計數器atomic:"+counter.atomic);
        }
        public void count() {
            // 調用compareAnSet方法,使用循環的CAS操作實現計數器的原子自增
            for (; ; ) {
                int expect = atomic.get();
                int curVal = expect + 1;
                if (atomic.compareAndSet(expect, curVal)) {
                    break;
                }
            }
        }
    }
    

    與非線程安全的計數器相比,線程安全的計數器有以下特點:

    • 將int類型的計數器變量i,更換成具有CAS操作的AtomicInteger類型的計數器變量atomic。
    • 進行自增運算時,通過循環的CAS操作實現atomic的原子自增。
    • 先通過atomic.get()獲取expect的值,將expect加一得到新值,然后通過atomic.compareAndSet(expect, curVal)這一方法實現CAS操作。
    • 其中compareAndSet()返回的true或者false,表示此次CAS操作是否成功。如果返回false,則不停地重復執行CAS操作,直到操作成功。

    上面的count方法實現的AtomicInteger原子自增,可以只需要調用incrementAndGet()一個方法就能實現。

    public void count() {
        // 調用incrementAndGet方法,實現AtomicInteger的原子自增
        atomic.incrementAndGet();
    }
    

    因為incrementAndGet()方法,封裝了通過循環的CAS操作實現AtomicInteger原子自增的代碼。

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
    
    (3)CAS操作存在的問題

    1. ABA問題

    • 在執行CAS操作更新共享變量的值時,如果一個值原來是A,被其他線程改成了B,然后又改回成了A。對于該CAS操作來說,它完全感受不到共享變量值的變化。這種操作漏洞稱為CAS操作的ABA問題。
    • 解決該問題的思路是,為變量添加版本號,每次更新時版本號遞增。這種場景下就成了1A --> 2B --> 3A。CAS操作就能檢測到共享變量的ABA問題了。
    • JUC包中,也提供了相應的帶標記的原子引用類AtomicStampedReference來解決ABA問題。
    • AtomicStampedReference的compareAndSet()方法會首先比較期待的引用是否等于當前引用,然后檢查期待的標記是否等于當前標記。如果全部相等,則以原子操作的方式將新的引用和新的標記更新到當前值中。
    • 但是AtomicStampedReference目前比較雞肋,如果想解決AB問題,可以使用鎖。

    2. 循環時間過長,開銷大

    循環的CAS操作如果長時間不成功,會給CPU帶來非常大的執行開銷。

    3. 只能保證一個共享變量的原子操作

    • 只對一個共享變量執行操作時,可以通過循環的CAS操作實現。如果是多個共享變量,循環的CAS操作無法保證操作的原子性。
    • 取巧的操作:將多個共享變量合為一個變量進行CAS操作。JDK1.5開始,提供了AtomicReference類保證引用對象之間的原子性,可以將多個變量放在一個對象中進行CAS操作。

    ③ 無同步方案

    同步只是保證共享數據爭用時正確性的一種手段,如果不存在共享數據,自然無須任何同步措施。

    (1)棧封閉

    多個線程訪問同一個方法的局部變量時,不會出現線程安全問題。

    因為方法中的局部變量不會逃出該方法而被其他線程訪問,因此可以看做JVM棧中數據,屬于線程私有。

    (2)可重入代碼(Reentrant Code)

    可重入代碼又叫純代碼(Pure Code),可在代碼執行的任何時候中斷他它,轉去執行另外一段代碼(包括遞歸調用它本身),控制權返回后,原來的程序不會出現任何錯誤。

    所有可重入的代碼都是線程安全,并非所有線程安全的代碼都是可重入的。

    可重入代碼的共同特征:

    • 不依賴存儲在堆上的數據和公用的系統資源
    • 用到的狀態量都由參數中傳入
    • 不調用非可重用的方法

    如何判斷代碼是否具備可重入性?如果一個方法,它的返回結果是可預測的。只要輸入了相同的數據,就都能返回相同的結果,那它就滿足可重入性,當然也就是線程安全的。

    (3)線程本地存儲(TLS)

    線程本地存儲(Thread Local Storage):

    • 如果一段代碼中所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行。
    • 如果能保證,我們就可以把共享數據的可見范圍限制在同一個線程內。
    • 這樣,無須同步也能保證線程之間不出現數據爭用的問題。

    TLS的重要應用實例:經典的Web交互模型中,一個請求對應一個服務器線程,使得Web服務器應用可以使用。學習資料:Java進階視頻資源

    Java中沒有關鍵字可以將一個變量定義為線程所獨享,但是Java中創建了java.lang.ThreadLocal類提供線程本地存儲功能。

    • 每一個線程內部都包含一個ThreadLocalMap對象,該對象將ThreadLocal對象的hashCode值作為key,即ThreadLocal.threadLocalHashCode,將本地線程變量作為value,構成鍵值對。
    • ThreadLocal對象是當前線程ThreadLocalMap對象的訪問入口,通過threadLocal.set()為本地線程添加獨享變量;通過threadLocal.get()獲取本地線程獨享變量的值。
    • ThreadLocal、ThreadLocalMap、Thread的關系:Thread對象中包含ThreadLocalMap對象,ThreadLocalMap對象中包含多個鍵值對,每個鍵值對的key是ThreadLocal對象的hashCode,value是本地線程變量。

    ThreadLocal的編程實例:

    • 想為某個線程添加本地線程變量,必須通過ThreadLocal對象在該線程中進行添加,構造出的鍵值對自動存入該線程的map中;
    • 想要獲取某個線程的本地線程變量,必須在該線程中獲取,會自動查詢該線程的map,獲得ThreadLocal對象對應的value。
    • 通過ThreadLocal對象重復為某個線程添加鍵值對,會覆蓋之前的value。
    public class TLS {
        public static void main(String[] args) {
            ThreadLocal threadLocal1 = new ThreadLocal<>();
            ThreadLocal threadLocal2 = new ThreadLocal<>();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 設置當前線程的本地線程變量
                    threadLocal1.set("thread1");
                    threadLocal2.set(1);
                    System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
                    // 使用完畢后要刪除,避免內存泄露
                    threadLocal1.remove();
                    threadLocal2.remove();
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    threadLocal1.set("thread2");
                    threadLocal2.set(2);
                    System.out.println(threadLocal1.get() + ": " + threadLocal2.get());
                    threadLocal1.remove();
                    threadLocal2.remove();
                }
            });
            thread1.start();
            thread2.start();
            // 沒有通過ThreadLocal為主線程添加過本地線程變量,獲取到的內容都是null
            System.out.println(threadLocal1.get()+": "+threadLocal2.get());
        }
    }
    

    對ThreadLocal的正確理解:

    • ThreadLocal適用于線程需要有自己的實例變量,該實例變量可以在多個方法中被使用,但是不能被其他線程共享的場景。
    • 由于不存在數據共享,何談同步?因此ThreadLocal 從理論上講,不是用來解決多線程并發問題的。

    ThreadLocal的實現:

    最原始的想法:ThreadLocal維護線程與實例的映射。既然通過ThreadLocal對象為線程添加本地線程變量,那就將ThreadLocalMap放在ThreadLocal中。

    原始想法存在的缺陷:多線程并發訪問ThreadLocal中的Map,需要添加鎖。這是, JDK 未采用該方案的一個原因。

    優化后的方法:Thread維護ThreadLocal與實例的映射。Map是每個線程所私有,只能在當前線程通過ThreadLocal對象訪問自身的Map。不存在多線程并發訪問同一個Map的情況,也就不需要鎖。

    優化后存在內存泄露的情況:JDK1.8中,ThreadLocalMap每個Entry對ThreadLocal對象是弱引用,對每個實例是強引用。當ThreadLocal對象被回收后,該Entry的鍵變成null,但Entry無法被移除。使得實例被Entry引用無法回收,造成內存泄露。

    線程安全vector
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    本篇是《Android逆向入門教程》的第三章第7節,更多章節詳細內容及實驗材料可通過加入底部免費的【Android逆向成長計劃】星球獲得!
    Java線程安全:狹義地認為是多線程之間共享數據的訪問。 Java語言中各種操作共享的數據有5種類型:不可變、絕對線程安全、相對線程安全線程兼容、線程獨立
    Java并發隊列與容器
    2022-07-29 10:03:36
    所謂“阻塞”是指在某些情況下線程被掛起,當滿足一定條件時會被自動喚醒,可以通過API進行控制。生產和消費數據時,直接將枚舉對象插入或刪除,不會產生或銷毀額外的對象實例。
    前言Scala是以JVM為運行環境的面向對象的函數式編程語言,它可以直接訪問Java類庫并且與Java框架進行交互操作。正如之前所介紹,Spark是用Scala語言編寫的,Kafka server端也是,那么深入學習Scala對掌握Spark、Kafka是必備掌握技能。
    如果指定了一個類為final,則該類所有的方法都是final的。此舉能夠使性能平均提高50% 。因為對這些大對象的操作會造成系統大的開銷,稍有不慎,將會導致嚴重的后果。
    摘要:數字基礎設施的發展加速了個人隱私數據在機器學習中的應用。隨著機器學習即服務的市場規模逐步擴大,服務提供商和用戶在雙向獲利的同時也面臨著嚴重的隱私泄露風險。因此,安全推理作為隱私保護機器學習的一個分支,成為科學界和工業界的研究熱點。安全多方計算是安全推理最重要的密碼學工具。從機器學習推理中潛在的隱私問題出發,引入安全多方計算技術,進一步對基于安全多方計算實現的安全推理框架進行分析研究,重點分析
    安全服務器應用領域,基于對稱算法的密碼芯片需要讀寫訪問大量、離散的密鑰和初始向量(Intial Vector,IV)數據。利用密鑰和 IV 數據的時間局部性和空間局部性,提出了一種預讀與高速緩沖存儲器(cache)相結合的雙倍速率同步動態隨機存取存儲(Double Data Rate Synchronous Dynamic Random Access Memory,DDR)加速器,來解決密鑰和
    在某些情況下,具有高完整性或系統完整性的進程會向權限進程/線程/令牌請求句柄,然后生成低完整性進程。如果這些句柄足夠強大,且類型正確,并且由子進程繼承,我們可以從另一個進程復制它們,然后濫用它們來升級權限或繞過UAC。在這篇文章中,我們將介紹如何尋找和濫用這種漏洞。
    從代碼視角來分析漏洞問題
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类