亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

Java多線程奇幻之旅——CAS算法實(shí)現(xiàn)線程安全

jasperyang / 1303人閱讀

摘要:大多數(shù)保證線程安全的方法是添加各種類型鎖,使用各種同步機(jī)制,用限制對共享的可變的類變量并發(fā)訪問的方式來保證線程安全。只有保證這兩條語句及中間語句以原子方式執(zhí)行,才能避免多線程覆蓋問題。

前言

對于線程安全,我們有說不盡的話題。大多數(shù)保證線程安全的方法是添加各種類型鎖,使用各種同步機(jī)制,用限制對共享的、可變的類變量并發(fā)訪問的方式來保證線程安全。文本從另一個(gè)角度,使用“比較交換算法”(CompareAndSwap)實(shí)現(xiàn)同樣的需求。我們實(shí)現(xiàn)一個(gè)簡單的“棧”,并逐步重構(gòu)代碼來進(jìn)行講解。
本文通俗易懂,不會涉及到過多的底層知識,適合初學(xué)者閱讀(言外之意是各位大神可以繞道了)。

旅程開始 1.先定個(gè)小目標(biāo),實(shí)現(xiàn)一個(gè)“?!?/b>

“棧”(stack)是大家經(jīng)常使用的抽象數(shù)據(jù)類型(啥?!不知道,請自行百度)?!皸!睗M足“后進(jìn)先出”特性。我們用鏈表數(shù)據(jù)結(jié)構(gòu)完成一個(gè)簡單的實(shí)現(xiàn):

public class Stack {
    //鏈表結(jié)構(gòu)頭部節(jié)點(diǎn)
    private Node head;
    /**
     * 入棧
     * @param item
     */
    public void push(E item) {
        //為新插入item創(chuàng)建一個(gè)新node
        Node newHead = new Node<>(item);
        if(head!=null){
            //將新節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)指向原來的頭部
            newHead.next = head;
        }
        //將頭部指向新的節(jié)點(diǎn)
        head=newHead;
    }
    /**
     * 出棧
     * @return
     */
    public E pop() {
        if(head==null){
            //當(dāng)前鏈表為空
            return null;
        }
        //暫存當(dāng)前節(jié)點(diǎn)。
        Node oldHead=head;
        //將當(dāng)前節(jié)點(diǎn)指向當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)
        head=head.next;
        //從暫存的當(dāng)前節(jié)點(diǎn)記錄返回?cái)?shù)據(jù)
        return oldHead.item;
    }
    /**
     * 鏈表中的節(jié)點(diǎn)
     * @param 
     */
    private static class Node {
        //節(jié)點(diǎn)保存的數(shù)據(jù)
        public final E item;
        //指向下一個(gè)鏈表中下一個(gè)節(jié)點(diǎn)
        public Node next;
        public Node(E item) {
            this.item = item;
        }
    }
}

代碼使用鏈表數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)“棧”,在Stack中維護(hù)一個(gè)鏈表的“頭部節(jié)點(diǎn)”,通過對頭部節(jié)點(diǎn)的操作完成入棧和出棧操作。
我們運(yùn)行代碼測試一下:

public static void main(String[] args) {
    Stack stack=new Stack<>();
    for (int i = 0; i < 3; i++) {
        //入棧1、2、3
        stack.push(i+1);
    }
    for (int i = 0; i < 3; i++) {
        //出棧3、2、1
        System.out.println(stack.pop());
    }
}

結(jié)果為:

3
2
1

我們使用入棧方法向Stack插入1、2、3,使用出棧方法打印為3、2、1,符合預(yù)期。

2.讓多線程搗搗亂

前面我們已經(jīng)測試過我們的方法,符合我們對Stack功能的預(yù)期,那是不是任何情況先我們的“棧”都能正常工作呢?

我們運(yùn)行如下代碼:

public static void main(String[] args) {
    Stack stack=new Stack<>();
    int max=3;
    Thread[] threads=new Thread[max];
    for (int i = 0; i < max; i++) {
        int temp=i;
        //入棧1、2、3
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                stack.push(temp+1);
            }
        });
        thread.start();
        threads[temp]=thread;
    }
    //等待所有線程完成。
    for (int i = 0; i < max; i++) {
        try {
            threads[i].join();
        } catch (InterruptedException e) {
        }
    }

    for (int i = 0; i < max; i++) {
        //出棧3、2、1
        System.out.println(stack.pop());
    }
}

你可能運(yùn)行了很多次,每次運(yùn)行時(shí)除了打印順序(3、2、1或2、3、1或1、2、3)有變化之外也沒有發(fā)現(xiàn)其他異常,你可能會說打印順序變化很正常呀,因?yàn)槲覀兊膶⑷霔2僮鞣诺疆惒骄€程中操作,三個(gè)線程的執(zhí)行過程由系統(tǒng)調(diào)度,所以入棧操作的內(nèi)容自然每次都有可能不同。
好吧,你說的沒錯(cuò),至少從大量運(yùn)行的結(jié)果上看是這樣的,但是這就是多線程編程的奇(tao)幻(yan)之處,也許你運(yùn)行一次沒有問題,兩次沒有問題,一萬次也沒有問題,但是終有一次你會得到那個(gè)意想不到的結(jié)果(你也不想得到,因?yàn)槟鞘莃ug)。這就像一個(gè)“黑天鵝事件”,小概率但是一定會發(fā)生,且發(fā)生后對你的系統(tǒng)影響不堪設(shè)想。
下面讓我?guī)憧纯慈绾蔚玫揭饬现獾慕Y(jié)果:

我們使用調(diào)試模式運(yùn)行上面的程序在Stack中push()方法第一行打一個(gè)斷點(diǎn),然后按照表格中的順序切換不同的線程以單步調(diào)試(step over)方式運(yùn)行run方法中的每一步,直到遇到Resume。

執(zhí)行順序 thread-0 thread-1 thread-2
1 Node newHead = new Node<>(item); -- --
2 head=newHead; -- --
3 (Resume) -- --
4 -- Node newHead = new Node<>(item); --
5 -- -- Node newHead = new Node<>(item);
6 -- newHead.next = head; --
7 -- -- newHead.next = head;
8 -- head=newHead; --
9 -- -- head=newHead;
10 -- (Resume)
11 -- -- (Resume)

當(dāng)你再次看到打印結(jié)果,你會發(fā)現(xiàn)結(jié)果為3、1、null,“黑天鵝”出現(xiàn)了。

異常結(jié)果是如何產(chǎn)生的?
當(dāng)thread-0執(zhí)行到順序3時(shí),head表示的鏈表為node(1)。
當(dāng)thread-1執(zhí)行到順序10時(shí),head表示的鏈表為node(2)->node(1)。
當(dāng)thread-2執(zhí)行到順序11時(shí),head表示的鏈表為node(3)->node(1)。
當(dāng)三個(gè)線程都執(zhí)行完畢之后,head的最終表示為node(3)->node(1),也就是說thread-2將thread-1的執(zhí)行結(jié)果覆蓋了。

語句newHead.next = head;是對頭部節(jié)點(diǎn)的讀取。語句head=newHead;是對頭部節(jié)點(diǎn)的寫入操作。這兩條語句組成了一個(gè)“讀取——設(shè)置——寫入”語句模式(就像n=n+1)。
如果一個(gè)線程執(zhí)行了共享頭部變量讀取語句,切換其他線程執(zhí)行了修改共享變量的值,再切回到第一個(gè)線程后,第一個(gè)線程中修改頭部結(jié)點(diǎn)的數(shù)據(jù)就不是最新的數(shù)據(jù)為依據(jù)的,所以修改之后其他線程的修改就被覆蓋了。
只有保證這兩條語句及中間語句以原子方式執(zhí)行,才能避免多線程覆蓋問題。
大家可以任意調(diào)整代碼中讀取頭部節(jié)點(diǎn)和寫入頭部節(jié)點(diǎn)的調(diào)試順序,制造多線程交錯(cuò)讀寫觀察不同的異常結(jié)果。

為什么我們直接執(zhí)行無法看到異常結(jié)果呢?
因?yàn)槲覀兊膔un方法很簡單,在CPU分配的時(shí)間片內(nèi)能運(yùn)行完,沒有出現(xiàn)在不同的運(yùn)行周期中交錯(cuò)運(yùn)行的狀態(tài)。所以我們才要用調(diào)試模式這種交錯(cuò)運(yùn)行。

為什么上文中我說過這種異常一定會發(fā)生?
原因在于我們在Stack類中對共享的、可變的變量head進(jìn)行的多線程讀寫操作。

怎么才能保證類Stack在多線程情況下運(yùn)行正確?
引用一段《JAVA并發(fā)編程實(shí)踐》中的話:

無論何時(shí),只要有多于一個(gè)的線程訪問給定的狀態(tài)變量,而且其中某個(gè)線程會寫入該變量,此時(shí)必須使用同步來協(xié)調(diào)線程對該變量的訪問。

好吧,看來我們必須采用“同步”方法了,來保障我們的Stack類在多線程并行和單線程串行的情況下都有正確的結(jié)果,也就是說將Stack變成一個(gè)線程安全的類。

3.讓你搗亂,請家長!

既然多線程總來搗亂,我們就請他的家長,讓家長管管他,守守規(guī)矩,不在搗亂。
我們已經(jīng)知道了Stack類問什么不能再多線程下正確的運(yùn)行的原因,所有我們要限制多線程對Stack類中head變量的并發(fā)寫入,Stack方法中push()和pop()方法都會對head進(jìn)行寫操作,所以要限制這兩個(gè)方法不能多線程并發(fā)訪問,所以我們想到了synchronized關(guān)鍵字。

程序重構(gòu):

public class SynchronizedStack {
    //鏈表結(jié)構(gòu)頭部節(jié)點(diǎn)
    private Node head;
    /**
     * 入棧
     * @param item
     */
    public synchronized void push(E item) {
        //為新插入item創(chuàng)建一個(gè)新node
        Node newHead = new Node<>(item);
        if(head!=null){
            //將新節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)指向原來的頭部
            newHead.next = head;
        }
        //將頭部指向新的節(jié)點(diǎn)
        head=newHead;
    }
    /**
     * 出棧
     * @return
     */
    public synchronized E pop() {
        if(head==null){
            //當(dāng)前鏈表為空
            return null;
        }
        //暫存當(dāng)前節(jié)點(diǎn)。
        Node oldHead=head;
        //將當(dāng)前節(jié)點(diǎn)指向當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)
        head=head.next;
        //從暫存的當(dāng)前節(jié)點(diǎn)記錄返回?cái)?shù)據(jù)
        return oldHead.item;
    }
    /**
     * 鏈表中的節(jié)點(diǎn)
     * @param 
     */
    private static class Node {
        //節(jié)點(diǎn)保存的數(shù)據(jù)
        public final E item;
        //指向下一個(gè)鏈表中下一個(gè)節(jié)點(diǎn)
        public Node next;
        public Node(E item) {
            this.item = item;
        }
    }
}

Stack類替換為SynchronizedStack類的測試方法。

 public static void main(String[] args) {
        SynchronizedStack stack=new SynchronizedStack<>();
        int max=3;
        Thread[] threads=new Thread[max];
        for (int i = 0; i < max; i++) {
            int temp=i;
            //入棧1、2、3
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    stack.push(temp+1);
                }
            });
            thread.start();
            threads[temp]=thread;
        }
        //等待所有線程完成。
        for (int i = 0; i < max; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
            }
        }

        for (int i = 0; i < max; i++) {
            //出棧3、2、1
            System.out.println(stack.pop());
        }
    }

我們再次運(yùn)行第二章為多線程準(zhǔn)備的測試方法,發(fā)現(xiàn)當(dāng)執(zhí)行一個(gè)線程的方法時(shí),其他線程的方法均被阻塞,只能等到第一個(gè)線程方法執(zhí)行完成之后才能執(zhí)行其他線程方法。
我們只不過是在push()pop()方法上加入了synchronized 關(guān)鍵字,就將這兩個(gè)方法編程了同步方法,在多線程并發(fā)的情況下也如同單線程串行調(diào)用一般,方法再不能在線程間交替運(yùn)行,也就不能對head變量做并發(fā)更改了,這樣修改的Stack類就是線程安全的了。

除了synchronized關(guān)鍵字,還有其他的方式實(shí)現(xiàn)加鎖嗎?
除了synchronized關(guān)鍵字還可以使用java.util.concurrent.locks包中各種鎖來保證同步,但是大概思路都是相同的,都是使用阻塞其他線程的方式在達(dá)到防止并發(fā)寫入的目的。

阻塞線程是否會影響執(zhí)行效率?
如果和不加通過的“?!鳖愊啾?,在多線程執(zhí)行的之后效率一定會有影響,因?yàn)橥椒椒ㄏ拗屏司€程之間的并發(fā)性,但是為了保證“?!鳖惖脑诙嗑€程環(huán)境時(shí)功能正確,我們不得不做出效率和正確性的權(quán)衡。

必須要對整個(gè)方法加上鎖嗎?
我們上面已經(jīng)分析了需要加鎖的范圍,只要保證讀取頭部節(jié)點(diǎn)和寫入頭部節(jié)點(diǎn)之間的語句原子性就可以。所以我們可以這樣執(zhí)行。

/**
     * 入棧
     *
     * @param item
     */
    public void push(E item) {
        //為新插入item創(chuàng)建一個(gè)新node
        Node newHead = new Node<>(item);
        synchronized (this) {
            if (head != null) {
                //將新節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)指向原來的頭部
                newHead.next = head;
            }
            //將頭部指向新的節(jié)點(diǎn)
            head = newHead;
        }
    }

    /**
     * 出棧
     *
     * @return
     */
    public E pop() {
        synchronized (this) {
            if (head == null) {
                //當(dāng)前鏈表為空
                return null;
            }
            //暫存當(dāng)前節(jié)點(diǎn)。
            Node oldHead = head;
            //將當(dāng)前節(jié)點(diǎn)指向當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)
            head = head.next;
            //從暫存的當(dāng)前節(jié)點(diǎn)記錄返回?cái)?shù)據(jù)
            return oldHead.item;
        }
    }

通過synchronized 塊實(shí)現(xiàn),因?yàn)榉椒ū容^簡單,所以也沒有很明顯的縮小加鎖范圍。

除了加鎖的方式,是否還有其他方式?
當(dāng)然,我們還有無鎖化編程來解決線程之間同步的問題。這就是下面要介紹的比較交換算法。

4.換個(gè)思路,樂觀一點(diǎn)

加鎖實(shí)現(xiàn)線程同步的方式是預(yù)防性方式。無論共享變量是否會被并發(fā)修改,我們都只允許同一時(shí)刻只有一個(gè)線程運(yùn)行方法來阻止并發(fā)發(fā)生。這就相當(dāng)于我們假設(shè)并發(fā)一定會發(fā)生,所以比較悲觀。
現(xiàn)在我們換一種思路,樂觀一點(diǎn),不要假設(shè)對變量的并發(fā)修改一定發(fā)生,這樣也就不用對方法加鎖阻止多線程并行運(yùn)行方法了。但是一旦發(fā)生了并發(fā)修改,我們想法發(fā)解決就是了,解決的方法就是將這個(gè)操作重試一下。

繼續(xù)重構(gòu)“?!贝a:

public class TreiberStack {
    private AtomicReference> headNode = new AtomicReference<>();
    public void push(E item) {
        Node newHead = new Node<>(item);
        Node oldHead;
        do {
            oldHead = headNode.get();
            newHead.next = oldHead;
        } while (!headNode.compareAndSet(oldHead, newHead));
    }
    public E pop() {
        Node oldHead;
        Node newHead;
        do {
            oldHead = headNode.get();
            if (oldHead == null)
                return null;
            newHead = oldHead.next;
        } while (!headNode.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }
    private static class Node {
        public final E item;
        public Node next;

        public Node(E item) {
            this.item = item;
        }
    }
}

這個(gè)就是大名鼎鼎的Treiber Stack,我也只是做了一次代碼的搬運(yùn)工。
我們來看看TreiberStack和我們前面的Stack有什么不同。

首先關(guān)注第一行:

private AtomicReference> headNode = new AtomicReference<>();

我們用了一個(gè)AtomicReference類存儲鏈表的頭部節(jié)點(diǎn),這個(gè)類可以獲取存儲對象的最新值,并且在修改存儲值時(shí)候采用比較交換算法保證原子操作,具體大家可以自行百度。

然后重點(diǎn)關(guān)注pop()push()方法中都有的一個(gè)代碼結(jié)構(gòu):

//略...
do {
    oldHead = headNode.get();
    //略...
     
} while (!headNode.compareAndSet(oldHead, newHead));
//略...

我們AtomicReferenceget()方法最新的獲取頭部節(jié)點(diǎn),然后調(diào)用AtomicReferencecompareAndSet()將設(shè)置新頭部節(jié)點(diǎn),如果當(dāng)前線程執(zhí)行這兩端代碼的時(shí)候如果有其他已經(jīng)修改了頭部節(jié)點(diǎn)的值,"compareAndSet()"方法返回false ,表明修改失敗,循環(huán)繼續(xù),否則修改成功,跳出循環(huán)。

這樣一個(gè)代碼結(jié)構(gòu)和synchronized關(guān)鍵字修飾的方法一樣,都保證了對于頭部節(jié)點(diǎn)的讀取和寫入操作及中間代碼在一個(gè)線程下原子執(zhí)行,前者是通過其他線程修改過就重試的方式,后者通過阻塞其他線程的方式,一個(gè)是樂觀的方式,一個(gè)是悲觀的方式。

大家可以按照前面的例子自己寫測試方法測試。

后記

我們通過對“?!钡囊徊揭徊酱a重構(gòu),逐步介紹了什么是線程安全及保證線程安全的各種方法。這里需要說明一點(diǎn),對于一個(gè)類來說,是否需要支持線程安全是由類的使用場景決定,不是有類所提供的功能決定的,如果一個(gè)類不會被應(yīng)用于多線程的情況下也就無需將他轉(zhuǎn)化為線程安全的類。

關(guān)于CAS特點(diǎn)等更多內(nèi)容鑒于本文篇幅有限,我會另文再續(xù)。

參考

《JAVA并發(fā)編程實(shí)踐》

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/68934.html

相關(guān)文章

  • Java線程奇幻之旅——Synchronized方式和CAS方式實(shí)現(xiàn)線程安全性能思考

    摘要:前言在上一篇文章中多線程奇幻之旅算法實(shí)現(xiàn)線程安全,我們介紹了和方式實(shí)現(xiàn)線程安全類的方法,兩種方式一個(gè)是鎖定阻塞方式,一個(gè)是非阻塞方式。 前言 在上一篇文章中《Java多線程奇幻之旅——CAS算法實(shí)現(xiàn)線程安全》,我們介紹了Synchronized和CAS方式實(shí)現(xiàn)線程安全類的方法,兩種方式一個(gè)是鎖定阻塞方式,一個(gè)是非阻塞方式。本文專注于兩種實(shí)現(xiàn)方式效率問題。本文是上篇文章的延續(xù),會借用到上...

    Chaz 評論0 收藏0
  • Week 1 - Java 線程 - CAS

    摘要:前言學(xué)習(xí)情況記錄時(shí)間子目標(biāo)多線程記錄在學(xué)習(xí)線程安全知識點(diǎn)中,關(guān)于的有關(guān)知識點(diǎn)。對于資源競爭嚴(yán)重線程沖突嚴(yán)重的情況,自旋的概率會比較大,從而浪費(fèi)更多的資源,效率低于。 前言 學(xué)習(xí)情況記錄 時(shí)間:week 1 SMART子目標(biāo) :Java 多線程 記錄在學(xué)習(xí)線程安全知識點(diǎn)中,關(guān)于CAS的有關(guān)知識點(diǎn)。 線程安全是指:多個(gè)線程不管以何種方式訪問某個(gè)類,并且在主調(diào)代碼中不需要進(jìn)行同步,都能表...

    ZweiZhao 評論0 收藏0
  • Java并發(fā)編程-原子操作

    摘要:這個(gè)規(guī)則比較好理解,無論是在單線程環(huán)境還是多線程環(huán)境,一個(gè)鎖處于被鎖定狀態(tài),那么必須先執(zhí)行操作后面才能進(jìn)行操作。線程啟動規(guī)則獨(dú)享的方法先行于此線程的每一個(gè)動作。 1. 指令重排序 關(guān)于指令重排序的概念,比較復(fù)雜,不好理解。我們從一個(gè)例子分析: public class SimpleHappenBefore { /** 這是一個(gè)驗(yàn)證結(jié)果的變量 */ private st...

    SillyMonkey 評論0 收藏0
  • i++ 是線程安全的嗎?

    摘要:例子先來看下面的示例來驗(yàn)證下到底是不是線程安全的。上面的例子我們期望的結(jié)果應(yīng)該是,但運(yùn)行遍,你會發(fā)現(xiàn)總是不為,至少你現(xiàn)在知道了操作它不是線程安全的了。它的性能比較好也是因?yàn)楸苊饬耸咕€程進(jìn)入內(nèi)核態(tài)的阻塞狀態(tài)。 例子 先來看下面的示例來驗(yàn)證下 i++ 到底是不是線程安全的。 1000個(gè)線程,每個(gè)線程對共享變量 count 進(jìn)行 1000 次 ++ 操作。 showImg(https://s...

    RyanQ 評論0 收藏0
  • ConcurrentHashMap基于JDK1.8源碼剖析

    摘要:下面我來簡單總結(jié)一下的核心要點(diǎn)底層結(jié)構(gòu)是散列表數(shù)組鏈表紅黑樹,這一點(diǎn)和是一樣的。是將所有的方法進(jìn)行同步,效率低下。而作為一個(gè)高并發(fā)的容器,它是通過部分鎖定算法來進(jìn)行實(shí)現(xiàn)線程安全的。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡單【源碼剖析】 LinkedHas...

    sanyang 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<