1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        面試官:Java中如何保證線(xiàn)程安全性

        共 17553字,需瀏覽 36分鐘

         ·

        2021-09-17 13:11

        來(lái)源:blog.csdn.net/weixin_40459875/

        article/details/80290875

        一、線(xiàn)程安全在三個(gè)方面體現(xiàn)

        1.原子性:提供互斥訪問(wèn),同一時(shí)刻只能有一個(gè)線(xiàn)程對(duì)數(shù)據(jù)進(jìn)行操作,(atomic,synchronized);

        2.可見(jiàn)性:一個(gè)線(xiàn)程對(duì)主內(nèi)存的修改可以及時(shí)地被其他線(xiàn)程看到,(synchronized,volatile);

        3.有序性:一個(gè)線(xiàn)程觀察其他線(xiàn)程中的指令執(zhí)行順序,由于指令重排序,該觀察結(jié)果一般雜亂無(wú)序,(happens-before原則)。

        接下來(lái),依次分析。

        二、原子性---atomic

        JDK里面提供了很多atomic類(lèi),AtomicInteger,AtomicLong,AtomicBoolean等等。

        它們是通過(guò)CAS完成原子性。

        我們一次來(lái)看AtomicIntegerAtomicStampedReference,AtomicLongArrayAtomicBoolean。

        (1)AtomicInteger

        先來(lái)看一個(gè)AtomicInteger例子:

        public class AtomicIntegerExample1 {
            // 請(qǐng)求總數(shù)
            public static int clientTotal = 5000;
            // 同時(shí)并發(fā)執(zhí)行的線(xiàn)程數(shù)
            public static int threadTotal = 200;
         
            public static AtomicInteger count = new AtomicInteger(0);
         
            public static void main(String[] args) throws Exception {
                ExecutorService executorService = Executors.newCachedThreadPool();//獲取線(xiàn)程池
                final Semaphore semaphore = new Semaphore(threadTotal);//定義信號(hào)量
                final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
                for (int i = 0; i < clientTotal ; i++) {
                    executorService.execute(() -> {
                        try {
                            semaphore.acquire();
                            add();
                            semaphore.release();
                        } catch (Exception e) {
                            log.error("exception", e);
                        }
                        countDownLatch.countDown();
                    });
                }
                countDownLatch.await();
                executorService.shutdown();
                log.info("count:{}", count.get());
            }
         
            private static void add() {
                count.incrementAndGet();
            }
        }

        我們可以執(zhí)行看到最后結(jié)果是5000是線(xiàn)程安全的。

        那么看AtomicInteger的incrementAndGet()方法:

        再看getAndAddInt()方法:

        這里面調(diào)用了compareAndSwapInt()方法:

        它是native修飾的,代表是java底層的方法,不是通過(guò)java實(shí)現(xiàn)的 。

        再重新看getAndAddInt(),傳來(lái)第一個(gè)值是當(dāng)前的一個(gè)對(duì)象 ,比如是count.incrementAndGet(),那么在getAndAddInt()中,var1就是count,而var2第二個(gè)值是當(dāng)前的值,比如想執(zhí)行的是2+1=3操作,那么第二個(gè)參數(shù)是2,第三個(gè)參數(shù)是1 。

        變量5(var5)是我們調(diào)用底層的方法而得到的底層當(dāng)前的值,如果沒(méi)有別的線(xiàn)程過(guò)來(lái)處理我們count變量的時(shí)候,那么它正常返回值是2。

        因此傳到compareAndSwapInt方法里的參數(shù)是(count對(duì)象,當(dāng)前值2,當(dāng)前從底層傳過(guò)來(lái)的2,從底層取出來(lái)的值加上改變量var4)。

        compareAndSwapInt()希望達(dá)到的目標(biāo)是對(duì)于var1對(duì)象,如果當(dāng)前的值var2和底層的值var5相等,那么把它更新成后面的值(var5+var4).

        compareAndSwapInt核心就是CAS核心。

        關(guān)于count值為什么和底層值不一樣:count里面的值相當(dāng)于存在于工作內(nèi)存的值,底層就是主內(nèi)存。

        (2)AtomicStampedReference

        接下來(lái)我們看一下AtomicStampedReference。

        關(guān)于CAS有一個(gè)ABA問(wèn)題:開(kāi)始是A,后來(lái)改為B,現(xiàn)在又改為A。解決辦法就是:每次變量改變的時(shí)候,把變量的版本號(hào)加1。

        這就用到了AtomicStampedReference。

        我們來(lái)看AtomicStampedReference里的compareAndSet()實(shí)現(xiàn):

        而在AtomicInteger里compareAndSet()實(shí)現(xiàn):

        可以看到AtomicStampedReference里的compareAndSet()中多了 一個(gè)stamp比較(也就是版本),這個(gè)值是由每次更新時(shí)來(lái)維護(hù)的。

        (3)AtomicLongArray

        這種維護(hù)數(shù)組的atomic類(lèi),我們可以選擇性地更新其中某一個(gè)索引對(duì)應(yīng)的值,也是進(jìn)行原子性操作。這種對(duì)數(shù)組的操作的各種方法,會(huì)多處一個(gè)索引。

        比如,我們看一下compareAndSet()

        (4)AtomicBoolean

        看一段代碼:

        public class AtomicBooleanExample {
         
            private static AtomicBoolean isHappened = new AtomicBoolean(false);
         
            // 請(qǐng)求總數(shù)
            public static int clientTotal = 5000;
            // 同時(shí)并發(fā)執(zhí)行的線(xiàn)程數(shù)
            public static int threadTotal = 200;
            public static void main(String[] args) throws Exception {
                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();
                            test();
                            semaphore.release();
                        } catch (Exception e) {
                            log.error("exception", e);
                        }
                        countDownLatch.countDown();
                    });
                }
                countDownLatch.await();
                executorService.shutdown();
                log.info("isHappened:{}", isHappened.get());
            }
            private static void test() {
                if (isHappened.compareAndSet(falsetrue)) {
                    log.info("execute");
                }
            }
        }

        執(zhí)行之后發(fā)現(xiàn),log.info("execute");只執(zhí)行了一次,且isHappend值為true。

        原因就是當(dāng)它第一次compareAndSet()之后,isHappend變?yōu)閠rue,沒(méi)有別的線(xiàn)程干擾。

        通過(guò)使用AtomicBoolean,我們可以使某段代碼只執(zhí)行一次。

        三、原子性---synchronized

        synchronized是一種同步鎖,通過(guò)鎖實(shí)現(xiàn)原子操作。

        JDK提供鎖分兩種:一種是synchronized,依賴(lài)JVM實(shí)現(xiàn)鎖,因此在這個(gè)關(guān)鍵字作用對(duì)象的作用范圍內(nèi)是同一時(shí)刻只能有一個(gè)線(xiàn)程進(jìn)行操作;另一種是LOCK,是JDK提供的代碼層面的鎖,依賴(lài)CPU指令,代表性的是ReentrantLock。

        synchronized修飾的對(duì)象有四種:

        • 修飾代碼塊,作用于調(diào)用的對(duì)象;
        • 修飾方法,作用于調(diào)用的對(duì)象;
        • 修飾靜態(tài)方法,作用于所有對(duì)象;
        • 修飾類(lèi),作用于所有對(duì)象。

        修飾代碼塊和方法:

        @Slf4j
        public class SynchronizedExample1 {
         
            // 修飾一個(gè)代碼塊
            public void test1(int j) {
                synchronized (this) {
                    for (int i = 0; i < 10; i++) {
                        log.info("test1 {} - {}", j, i);
                    }
                }
            }
         
            // 修飾一個(gè)方法
            public synchronized void test2(int j) {
                for (int i = 0; i < 10; i++) {
                    log.info("test2 {} - {}", j, i);
                }
            }
         
            public static void main(String[] args) {
                SynchronizedExample1 example1 = new SynchronizedExample1();
                SynchronizedExample1 example2 = new SynchronizedExample1();
                ExecutorService executorService = Executors.newCachedThreadPool();
                //一
                executorService.execute(() -> {
                    example1.test1(1);
                });
                executorService.execute(() -> {
                    example1.test1(2);
                });
                //二
                executorService.execute(() -> {
                    example2.test2(1);
                });
                executorService.execute(() -> {
                    example2.test2(2);
                });
                //三
                executorService.execute(() -> {
                    example1.test1(1);
                });
                executorService.execute(() -> {
                    example2.test1(2);
                });
            }
        }

        執(zhí)行后可以看到對(duì)于情況一,test1內(nèi)部方法塊作用于example1,先執(zhí)行完一次0-9輸出,再執(zhí)行下一次0-9輸出;情況二,同情況一類(lèi)似,作用于example2;情況三,可以看到交叉執(zhí)行,test1分別獨(dú)立作用于example1和example2,互不影響。

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

        @Slf4j
        public class SynchronizedExample2 {
         
            // 修飾一個(gè)類(lèi)
            public static void test1(int j) {
                synchronized (SynchronizedExample2.class{
                    for (int i = 0; i < 10; i++) {
                        log.info("test1 {} - {}", j, i);
                    }
                }
            }
         
            // 修飾一個(gè)靜態(tài)方法
            public static synchronized void test2(int j) {
                for (int i = 0; i < 10; i++) {
                    log.info("test2 {} - {}", j, i);
                }
            }
         
            public static void main(String[] args) {
                SynchronizedExample2 example1 = new SynchronizedExample2();
                SynchronizedExample2 example2 = new SynchronizedExample2();
                ExecutorService executorService = Executors.newCachedThreadPool();
                executorService.execute(() -> {
                    example1.test1(1);
                });
                executorService.execute(() -> {
                    example2.test1(2);
                });
            }
        }

        test1和test2會(huì)鎖定調(diào)用它們的對(duì)象所屬的類(lèi),同一個(gè)時(shí)間只有一個(gè)對(duì)象在執(zhí)行。

        四、可見(jiàn)性---volatile

        對(duì)于可見(jiàn)性,JVM提供了synchronized和volatile。這里我們看volatile。

        (1)volatile的可見(jiàn)性是通過(guò)內(nèi)存屏障和禁止重排序?qū)崿F(xiàn)的

        volatile會(huì)在寫(xiě)操作時(shí),會(huì)在寫(xiě)操作后加一條store屏障指令,將本地內(nèi)存中的共享變量值刷新到主內(nèi)存:

        volatile在進(jìn)行讀操作時(shí),會(huì)在讀操作前加一條load指令,從內(nèi)存中讀取共享變量:

        (2)但是volatile不是原子性的,進(jìn)行++操作不是安全的

        @Slf4j
        public class VolatileExample {
         
            // 請(qǐng)求總數(shù)
            public static int clientTotal = 5000;
         
            // 同時(shí)并發(fā)執(zhí)行的線(xiàn)程數(shù)
            public static int threadTotal = 200;
         
            public static volatile int count = 0;
         
            public static void main(String[] args) throws Exception {
                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();
                            add();
                            semaphore.release();
                        } catch (Exception e) {
                            log.error("exception", e);
                        }
                        countDownLatch.countDown();
                    });
                }
                countDownLatch.await();
                executorService.shutdown();
                log.info("count:{}", count);
            }
         
            private static void add() {
                count++;
            }
        }

        執(zhí)行后發(fā)現(xiàn)線(xiàn)程不安全,原因是 執(zhí)行conut++ 時(shí)分成了三步,第一步是取出當(dāng)前內(nèi)存 count 值,這時(shí) count 值時(shí)最新的,接下來(lái)執(zhí)行了兩步操作,分別是 +1 和重新寫(xiě)回主存。假設(shè)有兩個(gè)線(xiàn)程同時(shí)在執(zhí)行 count++ ,兩個(gè)內(nèi)存都執(zhí)行了第一步,比如當(dāng)前 count 值為 5 ,它們都讀到了,然后兩個(gè)線(xiàn)程分別執(zhí)行了 +1 ,并寫(xiě)回主存,這樣就丟掉了一次加一的操作。

        (3)volatile適用的場(chǎng)景

        既然volatile不適用于計(jì)數(shù),那么volatile適用于哪些場(chǎng)景呢:

        1. 對(duì)變量的寫(xiě)操作不依賴(lài)于當(dāng)前值
        2. 該變量沒(méi)有包含在具有其他變量不變的式子中

        因此,volatile適用于狀態(tài)標(biāo)記量:

        線(xiàn)程1負(fù)責(zé)初始化,線(xiàn)程2不斷查詢(xún)inited值,當(dāng)線(xiàn)程1初始化完成后,線(xiàn)程2就可以檢測(cè)到inited為true了。

        五、有序性

        有序性是指,在JMM中,允許編譯器和處理器對(duì)指令進(jìn)行重排序,但是重排序過(guò)程不會(huì)影響到單線(xiàn)程程序的執(zhí)行,卻會(huì)影響到多線(xiàn)程并發(fā)執(zhí)行的正確性。

        可以通過(guò)volatile、synchronized、lock保證有序性。

        另外,JMM具有先天的有序性,即不需要通過(guò)任何手段就可以得到保證的有序性。這稱(chēng)為happens-before原則。

        如果兩個(gè)操作的執(zhí)行次序無(wú)法從happens-before原則推導(dǎo)出來(lái),那么它們就不能保證它們的有序性。虛擬機(jī)可以隨意地對(duì)它們進(jìn)行重排序。

        happens-before原則:

        1. 程序次序規(guī)則:在一個(gè)單獨(dú)的線(xiàn)程中,按照程序代碼書(shū)寫(xiě)的順序執(zhí)行。
        2. 鎖定規(guī)則:一個(gè)unlock操作happen—before后面對(duì)同一個(gè)鎖的lock操作。
        3. volatile變量規(guī)則:對(duì)一個(gè)volatile變量的寫(xiě)操作happen—before后面對(duì)該變量的讀操作。
        4. 線(xiàn)程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法happen—before此線(xiàn)程的每一個(gè)動(dòng)作。
        5. 線(xiàn)程終止規(guī)則:線(xiàn)程的所有操作都happen—before對(duì)此線(xiàn)程的終止檢測(cè),可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測(cè)到線(xiàn)程已經(jīng)終止執(zhí)行。
        6. 線(xiàn)程中斷規(guī)則:對(duì)線(xiàn)程interrupt()方法的調(diào)用happen—before發(fā)生于被中斷線(xiàn)程的代碼檢測(cè)到中斷時(shí)事件的發(fā)生。
        7. 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)happen—before它的finalize()方法的開(kāi)始。
        8. 傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

        程序汪資料鏈接

        程序汪接的7個(gè)私活都在這里,經(jīng)驗(yàn)整理

        Java項(xiàng)目分享  最新整理全集,找項(xiàng)目不累啦 04版

        堪稱(chēng)神級(jí)的Spring Boot手冊(cè),從基礎(chǔ)入門(mén)到實(shí)戰(zhàn)進(jìn)階

        臥槽!字節(jié)跳動(dòng)《算法中文手冊(cè)》火了,完整版 PDF 開(kāi)放下載!

        臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開(kāi)放下載!

        字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開(kāi)放下載!

        歡迎添加程序汪個(gè)人微信 itwang008  進(jìn)粉絲群或圍觀朋友圈

        瀏覽 56
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            大香蕉淫人 | 成人在线视频免费观看 | 国产www在线观看 | 《女兵性史》未删减 | 操逼网站直接看 | 国产激情综合 | 天天插天天射天天操 | 高清无码视屏 | 国内精品成人 | 丁香婷婷五月激情综合 |