真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

java高并發(fā)中線程安全性是什么

這期內(nèi)容當中小編將會給大家?guī)碛嘘P(guān)java高并發(fā)中線程安全性是什么,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供京口網(wǎng)站建設(shè)、京口做網(wǎng)站、京口網(wǎng)站設(shè)計、京口網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計與制作、京口企業(yè)網(wǎng)站模板建站服務(wù),十載京口做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。

定義:當多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式或者這些進程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行為,那么就稱這個類是線程安全的。

線程安全性體現(xiàn)在以下三個方面:

  1.  原子性:提供了互斥訪問,同一時刻只能有一個線程來對它進行操作。

  2. 可見性:一個線程對主內(nèi)存的修改可以及時的被其他線程觀察到。

  3. 有序性:一個線程觀察其他線程中的指令執(zhí)行順序,由于指令重排序的存在,該觀察結(jié)果一般雜亂無序。

原子性 Atomic包

新建一個測試類,內(nèi)容如下:

@Slf4j
@ThreadSafe
public class CountExample2 {

    // 請求總數(shù)
    public static int clientTotal = 5000;

    // 同時并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    // 工作內(nèi)存
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        //線程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定義信號量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定義計數(shù)器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    public static void add() {
        count.incrementAndGet();
    }

}

使用了AtomicInteger類,這個類的incrementAndGet方法底層使用的unsafe.getAndAddInt(this, valueOffset, 1) + 1;方法,而底層使用了this.compareAndSwapInt方法。這個compareAndSwapInt方法(CAS)是用當前值與主內(nèi)存的值進行對比,如果值相等則進行相應(yīng)的操作。

count變量就是工作內(nèi)存,它與主內(nèi)存中的數(shù)據(jù)不一定是一樣的,因此需要做同步操作才可以。 

AtomicLong與LongAdder

我們將上面的count用AtomicLong來修飾,同樣可以輸出正確的效果:

public static AtomicLong count = new AtomicLong(0);

我們?yōu)槭裁匆獑为氄f一下AtomicLong?因為JDK8中新增了一個類,與AtomicLong十分像,即LongAdder類。將上面的代碼用LongAdder實現(xiàn)一下:

public static LongAdder count = new LongAdder();

public static void add() {
        count.increment();
    }

log.info("count:{}", count);

同樣也可以輸出正確的結(jié)果。

為什么有了AtomicLong后還要新增一個LongAdder?

原因是AtomicLong底層使用CAS來保持同步,是在一個死循環(huán)內(nèi)不斷嘗試比較值,當工作內(nèi)存與主內(nèi)存數(shù)據(jù)一致的情況下才執(zhí)行后續(xù)操作,競爭不激烈的時候成功幾率高,競爭激烈時也就是并發(fā)量高時性能就會降低。對于Long和Double變量來說,jvm會將64位的Long或Double變量的讀寫操作拆分成兩個32位的讀寫操作。因此實際使用過程中可以優(yōu)先使用LongAdder,而不是繼續(xù)使用AtomicLong,當競爭比較低的時候可以繼續(xù)使用AtomicLong。

查看atomic包:

java高并發(fā)中線程安全性是什么

AtomicReference和AtomicInteger非常類似,不同之處就在于AtomicInteger是對整數(shù)的封裝,底層采用的是compareAndSwapInt實現(xiàn)CAS,比較的是數(shù)值是否相等,而AtomicReference則對應(yīng)普通的對象引用,底層使用的是compareAndSwapObject實現(xiàn)CAS,比較的是兩個對象的地址是否相等。也就是它可以保證你在修改對象引用時的線程安全性。

AtomicIntegerFieldUpdater

原子性更新某個類的實例的某個字段的值,并且這個字段必須用volatile關(guān)鍵字修飾同時不能是static修飾的。

private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
    @Getter
    public volatile int count = 100;

    public static void main(String[] args) {
        private AtomicExample5 example5 = new AtomicExample5();
        if (updater.compareAndSet(example5, 100, 120)){
            log.info("update success 1, {}", example5.getCount());
        }
        if(updater.compareAndSet(example5, 100, 120)){
            log.info("update success 2 ,{}", example5.getCount());
        }else {
            log.info("update failed, {}", example5.getCount());
        }
    }

AtomicStampReference:CAS的ABA問題

ABA問題是:在CAS操作的時候,其他線程將變量的值A(chǔ)改成了B,隨后又改成了A,CAS就會被誤導(dǎo)。所以ABA問題的解決思路就是將版本號加一,當一個變量被修改,那么這個變量的版本號就增加1,從而解決ABA問題。

AtomicBoolean

@Slf4j
@ThreadSafe
public class AtomicExample6 {

    private static AtomicBoolean isHappened = new AtomicBoolean(false);

    // 請求總數(shù)
    public static int clientTotal = 5000;

    // 同時并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    public static void main(String[] args) throws InterruptedException {
        //線程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定義信號量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定義計數(shù)器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened);
    }

    private static void test() {
        if (isHappened.compareAndSet(false, true)){
            log.info("excute");
        }
    }

}

這段代碼test()方法只會被執(zhí)行5000次而進入log.info("excute")只會被執(zhí)行一次,因為isHappened變量執(zhí)行一次之后就變?yōu)閠rue了。

這個方法可以保證變量isHappened從false變成true只會執(zhí)行一次。

這個例子可以解決讓一段代碼只執(zhí)行一次絕對不會重復(fù)。

原子性 鎖

  • synchronized:synchronized關(guān)鍵字主要是依賴JVM實現(xiàn)鎖,因此在這個關(guān)鍵字作用對象的作用范圍內(nèi)都是同一時刻只能有一個線程可以進行操作的。

  • lock:依賴特殊的CPU指令,實現(xiàn)類中比較有代表性的是ReentrantLock。

synchronized同步鎖

修飾的對象主要有一下四種:

  • 修飾代碼塊:大括號括起來的代碼,作用于調(diào)用的對象。

  • 修飾方法:整個方法,作用于調(diào)用的對象。

  • 修飾靜態(tài)方法:整個靜態(tài)方法,作用于這個類的所有對象

  • 修飾類:括號括起來的部分,作用于所有對象。

修飾代碼塊和方法

舉例如下:

@Slf4j
public class SynchronizedExample1 {

    /**
     * 修飾一個代碼塊,被修飾的代碼稱為同步語句塊,作用范圍是大括號括起來的代碼,作用的對象是調(diào)用代碼的對象
     */
    public void test1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 - {}", i);
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedExample1 synchronizedExample1 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test1();
        });
        executorService.execute(() -> {
            synchronizedExample1.test1();
        });
    }

}

為什么我們要使用線程池?如果不使用線程池的話,兩次調(diào)用了同一個方法,本身就是同步執(zhí)行的,因此是無法驗證具體的影響,而我們加上線程池之后,相當于分別啟動了兩個線程去執(zhí)行方法。

輸出結(jié)果是連續(xù)輸出兩遍test1 0-9。

如果使用synchronized修飾方法:

    /**
     * 修飾一個方法,被修飾的方法稱為同步方法,作用范圍是整個方法,作用的對象是調(diào)用方法的對象
     */
    public synchronized void test2() {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}", i);
        }
    }

輸出結(jié)果跟上面一樣,是正確的。

接下來換不同的對象,然后亂序輸出,因為同步代碼塊和同步方法作用對象是調(diào)用對象,因此使用兩個不同的對象調(diào)用不同的同步代碼塊互相是不影響的,如果我們使用線程池,example1的test1方法和example2的test1方法是交叉執(zhí)行的,而不是example1的test1執(zhí)行完然后再執(zhí)行example2的test1,代碼如下:

 /**
     * 修飾一個代碼塊,被修飾的代碼稱為同步語句塊,作用范圍是大括號括起來的代碼,作用的對象是調(diào)用代碼的對象
     */
    public void test1(int flag) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 - {}, {}", flag, i);
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 synchronizedExample1 = new SynchronizedExample1();
        SynchronizedExample1 synchronizedExample2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test1(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test1(2);
        });
    }

因此同步代碼塊作用于當前對象,不同調(diào)用對象之間是互相不影響的。

接下來測試同步方法:

/**
     * 修飾一個方法,被修飾的方法稱為同步方法,作用范圍是整個方法,作用的對象是調(diào)用方法的對象
     */
    public synchronized void test2(int flag) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}, {}", flag, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 synchronizedExample1 = new SynchronizedExample1();
        SynchronizedExample1 synchronizedExample2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test2(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test2(2);
        });
    }

如果一個方法內(nèi)部是一個完整的同步代碼塊,就像上面的test1方法一樣,那么它和用synchronized修飾的方法效果是等同的。

同時需要注意的synchronized修飾是無法繼承給子類的方法。

修飾靜態(tài)方法和類

我們先測試修飾靜態(tài)方法:

 /**
     * 修飾一個靜態(tài)方法,被修飾的方法稱為同步方法,作用范圍是整個方法,作用的對象是調(diào)用方法的對象
     */
    public static synchronized void test2(int flag) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}, {}", flag, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample2 synchronizedExample1 = new SynchronizedExample2();
        SynchronizedExample2 synchronizedExample2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test2(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test2(2);
        });
    }

修飾一個靜態(tài)方法作用于這個類的所有對象。因此我們使用不同的對象調(diào)用synchronized修飾的靜態(tài)方法時,同一時間只有一個線程在執(zhí)行。因此上面的執(zhí)行結(jié)果是:

11:31:37.447 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 0
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 1
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 2
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 3
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 4
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 5
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 6
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 7
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 8
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 9
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 0
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 1
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 2
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 3
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 4
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 5
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 6
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 7
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 8
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 9

他們不會交替執(zhí)行。

然后調(diào)用修飾類的:

/**
     * 修飾一個類,被修飾的代碼稱為同步語句塊,作用范圍是大括號括起來的代碼,作用的對象是調(diào)用代碼的對象
     */
    public static void test1(int flag) {
        synchronized (SynchronizedExample2.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 - {}, {}", flag, i);
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedExample2 synchronizedExample1 = new SynchronizedExample2();
        SynchronizedExample2 synchronizedExample2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test1(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test1(2);
        });
    }

運行結(jié)果跟上面是一致的。

同樣的如果一個方法內(nèi)部被synchronized修飾的一個類是一個完整的同步代碼塊,就像上面的test1方法一樣,那么它和用synchronized修飾的靜態(tài)方法效果是等同的。

synchronized:不可中斷鎖,適合競爭不激烈,可讀性好。

lock:可中斷鎖,多樣化同步,競爭激烈時能維持常態(tài)。

Atomic:競爭激烈時能維持常態(tài),比lock性能好;只能同步一個值。

可見性

可見性是指線程對主內(nèi)存的修改可以及時的被其他線程觀察到。說起可見性,我們常常去向什么時候不可見,下面介紹一下共享變量在線程間不可見的原因。

  • 線程交叉執(zhí)行

  • 重排序結(jié)合線程交叉執(zhí)行

  • 共享變量更新后的值沒有在工作內(nèi)存與主內(nèi)存間及時更新。

JMM關(guān)于synchronized的兩條規(guī)定:

  • 線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存。

  • 線程加鎖時,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時需要從主內(nèi)存中重新讀取最新的值(注意,加鎖與解鎖是同一把鎖) 

 可見性 - volatile

通過加入內(nèi)存屏障和禁止重排序優(yōu)化來實現(xiàn):

  • 對volatile變量寫操作時,會在寫操作后加入一條store屏障命令,將本地內(nèi)存中的共享變量值刷新到主內(nèi)存。

  • 對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內(nèi)存中讀取共享變量。

volatile變量在每次對線程訪問時,都強迫從主內(nèi)存中讀取該變量的值,而當該變量發(fā)生改變時,又會強迫線程將最新的值刷新到主內(nèi)存,這樣任何時候不同的線程總能看到該變量的最新值。 下面舉例說明:

@Slf4j
@NotThreadSafe
public class CountExample4 {

    // 請求總數(shù)
    public static int clientTotal = 5000;

    // 同時并發(fā)執(zhí)行的線程數(shù)
    public static int threadTotal = 200;

    public static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        //線程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定義信號量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定義計數(shù)器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    public static void add() {

        // 使用volatile修飾,可以保證count是主內(nèi)存中的值
        count++;
    }

}

運行結(jié)果依然無法保證線程安全。為什么呢?

原因是當我們執(zhí)行count++的時候呢,它其實是分了三步,1.從主內(nèi)存中取出count值,這時的count值是最新的,2給count執(zhí)行+1操作,3.將count值寫回主內(nèi)存。當多線程同時讀取到count的值并且給count值+1,這樣就會出現(xiàn)線程不安全的情況。

因此通過使用volatile修飾變量不是線程安全的。同時也說明volatile不具有原子性。

既然volatile不適合計數(shù)的場景,那么適合什么場景呢?

通常來說使用volatile必須具備 對變量的寫操作不依賴與當前值。

原子性 有序性

java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。

有序性 - happens-before原則

  • 程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫后面的操作。對于程序次序規(guī)則來說,一段程序代碼的執(zhí)行在單個線程中看起來是有序的(注意,雖然在這條規(guī)則中提到書寫在前面的操作先行發(fā)生于書寫后面的操作,這是程序看起來執(zhí)行順序是按照代碼書寫的順序執(zhí)行的,而虛擬機會對程序代碼進行指令重排序,雖然進行了重排序,但是最終執(zhí)行的結(jié)果是與程序順序執(zhí)行的結(jié)果是一樣的, 它只會對不存在數(shù)據(jù)依賴行的指令進行重排序,因此在單個線程中,程序看起來是有序執(zhí)行的),事實上這個規(guī)則是用來保證程序在單線程中執(zhí)行結(jié)果的正確性。但無法保證程序在多線程中執(zhí)行的正確性。

  • 鎖定規(guī)則:一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。也就是說無論在單線程中還是多線程中,同一個鎖如果處于被鎖定狀態(tài),那么必須先對鎖進行釋放操作,后面才能繼續(xù)進行l(wèi)ock操作。

  • volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作。如果一個線程先去寫一個變量,然后一個線程進行讀取,那么寫入操作肯定先行發(fā)生于讀操作。

  • 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C。

  • 線程啟動原則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作。

  • 線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生。

  • 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行。

  • 對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始。

上述就是小編為大家分享的java高并發(fā)中線程安全性是什么了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。


當前名稱:java高并發(fā)中線程安全性是什么
鏈接URL:http://weahome.cn/article/jchigj.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部