並發編程與高並發學習筆記一


一,線程安全性
1.定義:
當多個線程訪問某個類時,不管運行時環境采用 任何調度方式 或者這些進程將如何交替執行,並且在主調代碼中
不需要任何額外的同步或協同,這個類都能表現出 正確的行為,那么稱這個類是線程安全的
2.線程安全性體現在三個方面:
原子性:提供了互斥訪問,同一時刻只能有一個線程來對他操作
可見性:一個線程對主內存的修改可以及時被其他線程觀察到
有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序的存在,該觀察結果一般雜亂無序
3.並發模擬代碼:
//並發模擬代碼
public class CountExample {
    //請求總數
    public static int clientTotal = 5000;
    //同時並發執行的線程數
    public static int threadTotal = 200;
    //全局變量
    public static int count = 0;
    public static void main(String[] args) {
        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 (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //閉鎖,每執行一次add()操作,請求數就減一
                countDownLatch.countDown();
            });
        }

        //等待上面的線程都執行完畢,countDown的值減為0,然后才向下執行主線程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("count:"+count);

        //關閉線程池
        executorService.shutdown();

    }

    private static void add(){
        count++;
    }
}
 
         
         
        



.原子性-Atomic包
1.AtomicInteger類中提供了incrementAndGet方法;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
2.incrementAndGet方法又調用了Unsafe類的getAndAddInt方法
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.getAndAddInt方法又是如何保證原子性的呢?該方法調用了compareAndSwapInt方法(就是我們說的CAS)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
compareAndSwapInt方法是native方法,這個方法是java底層的方法(不是通過java實現的)
4.原理解析:

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;
}
Object var1:傳進來的AtomicInteger對象
long var2:是傳進來的值,當前要進行加一的值 (比如要進行2+1的操作, var2就是2)
int var4:是傳進來的值,進行自增要加上的值 (比如要進行2+1的操作, var4就是1)
int var5:是通過調用底層的方法this.getIntVolatile(var1, var2);得到的底層當前的值
while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)):
通過do{} while()不停的將當前對象的傳進來的值和底層的值進行比較,
如果相同就將底層的值更新為:var5+var4(加一的操作),
如果不相同,就重新再從底層取一次值,然后再進行比較,這就是CAS的核心。
幫助理解:
AtomicInteger里面存的值看成是工作內存中的值
把底層的值看成是主內存中的值。在多線程中,工作內存中的值和主內存中的值會出現不一樣的情況。

線程安全的代碼:
//線程安全的並發
public class CountExample2 {
    //請求總數
    public static int clientTotal = 5000;
    //同時並發執行的線程數
    public static int threadTotal = 200;
    //全局變量
    public static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) {
        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 (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //閉鎖,每執行一次add()操作,請求數就減一
                countDownLatch.countDown();
            });
        }

        //等待上面的線程都執行完畢,countDown的值減為0,然后才向下執行主線程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("count:"+count.get());
        //關閉線程池
        executorService.shutdown();
    }
    private static void add(){
        //count++;
        count.incrementAndGet();
    }
}
 
5.還有AotimcLong和LongAddr
他倆的區別沒聽懂???????

6.CAS中的ABA問題
描述:在CAS操作時,其他線程將變量的值從A改成了B,然后又將B改回了A。
解決思路:每次變量改變時,將變量的版本號加1,只要變量被修改過,變量的版本號就會發生遞增變化
使用的類:AtomicStampedReference,
調用compareAndSet方法:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
stamp是每次更新時就維護的, 通過對比來判斷是不是一個版本號,expectedStamp == current.stamp

7.AtomicBoolean
代碼:
//讓某一段代碼只執行一次
public class CountExample3 {
    //請求總數
    public static int clientTotal = 5000;
    //同時並發執行的線程數
    public static int threadTotal = 200;
    public static AtomicBoolean isHappened = new AtomicBoolean(false);
    public static void main(String[] args) {
        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 (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //閉鎖,每執行一次add()操作,請求數就減一
                countDownLatch.countDown();
            });
        }

        //等待上面的線程都執行完畢,countDown的值減為0,然后才向下執行主線程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("isHappened:"+isHappened.get());
        //關閉線程池
        executorService.shutdown();
    }

    private static void test(){
        //如果是false,就更新為true
       if (isHappened.compareAndSet(false,true)){
           System.out.println("execute");
       }
    }
}

.原子性-鎖
1.synchronized:依賴JVM
(1).synchronized修飾的對象有四種:
修飾代碼塊:作用范圍是大括號括起來的代碼,作用於調用的對象
修飾方法:作用范圍是整個方法,作用於調用的對象
修飾靜態方法:作用范圍是整個靜態的方法,作用於這個類的所有對象
修飾類:作用范圍是synchronized括號括起來的部分,作用於這個類的所有對象

2.Lock:依賴特殊的cpu指令,代碼實現,ReentrantLock

3.原子性-對比
synchronized:不可中斷鎖,適合競爭不激烈,可讀性好.(競爭激烈時性能下降非常快)
Lock:可中斷鎖(調用unlock即可),多樣化同步,競爭激烈時能維持常態
Atomic:競爭激烈時呢能維持常態,比Lock性能好,只能同步一個值

.可見性
1.定義:一個線程對主內存的修改可以及時被其他線程觀察到
2.導致共享變量在線程間不可見的原因:
線程交叉執行
重排序結合交叉執行
共享變量更新后的值沒有在工作內存與主內存之間及時更新
3.可見性-synchronized
java內存模型(JMM)關於synchronized的兩條規定:
A.線程解鎖前,必須把共享變量的最新值刷新到主內存
B.線程加鎖時,將清空工作內存中的共享變量的值,從而在使用共享變量時,需要從主內存中重新讀取最新的值(注意:
加鎖和解鎖是同一把鎖)
4.可見性-volatile
(1).通過加入內存屏障和禁止重排序優化來實現
A.對volatile變量寫操作時,會在寫操作后加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存
B.對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量
通俗地說就是,volatile變量每次被線程訪問時,都強迫從主內存讀取該變量的值,而當該變量發生變化時,又會強迫線程
將最新的值刷新到主內存,這樣,任何時候不同的線程總能看到該變量的最新值
(2).內存屏障是如何禁止重排序的呢?
圖示:
(3).注意:volatile可不具有原子性,用它修飾變量是沒有用的
比如:public static volatile int count = 0;這樣的變量雖然用volatile修飾了,但是並不是線程安全的
//volatile並不保證原子性
public class VolatileExample {

    //請求總數
    public static int clientTotal = 5000;
    //同時並發執行的線程數
    public static int threadTotal = 200;
    //全局變量
    public static volatile int count = 0;

    public static void main(String[] args) {
        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 (InterruptedException e){
                    System.out.println("exception");
                    e.printStackTrace();
                }
                //閉鎖,每執行一次add()操作,請求數就減一
                countDownLatch.countDown();
            });
        }

        //等待上面的線程都執行完畢,countDown的值減為0,然后才向下執行主線程
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //打印count的值
        System.out.println("count:"+count);

        //關閉線程池
        executorService.shutdown();

    }

    private static void add(){
        count++;
        /*
        count++操作其實有三步:
            獲取count
            +1操作
            將count刷新到主內存
        當有多個線程同時執行count++時,因為上面有三步,所以保證不了線程的安全性
         */
    }
}
 
        

 


(4).volatile適用場景一:
使用volatile必須具備兩個條件:
A.對變量的寫操作,不依賴於當前值
B.該變量沒有包含在具有其他變量的式子中
所以,volatile特別適合作為狀態標記量
例子:
volatile boolean inited = false; //inited用來標識線程初始化是否完成
//線程一: 線程一負責初始化
context = loadContext(); //初始化操作
init = true;//初始化完成后修改狀態

//線程二: 線程二必須保證初始化完成才能執行
while(!inited){//所以線程二不斷的判斷是否為inited是否true,只有為true時,線程二才開始執行
sleep();
}
doSomethingWithConfig(context);
(5).volatile適用場景二:
用來雙重檢測,單例模式中的雙重檢測機制。
.有序性
1.定義:
java內存模型中,允許編譯器和處理器對指令進行重排序,重排序過程是不會影響到單線程程序的執行的,但是會影響到
多線程並發執行的正確性
2.保證有序性的方式有哪些?
volatile,synchronized,Lock
3.happens-before原則 (即先行發生原則),共有8個規則:
程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在后面的操作
鎖定規則:一個unlock操作先行發生於后面對同一個鎖的lock操作
volatile規則L:對一個變量的寫操作先行發生於后面對這個變量的讀操作
傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
后四個是顯而易見的,重點是前四個
線程啟動規則:Thread對象的start()方法先行發生於此線程的每一個動作
線程中規則:對線程interrupt()方法的調用先行發生於 被中斷線程的代碼檢測到 中斷事件的發生
線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束,
Thread.isAlive()的返回值手段檢測到線程已經終止執行
對象終結規則:一個對象的初始化完成先發生於他的finalize()方法的開始

所以:如果兩個操作的執行順序無法通過happen-before的8個原則推斷出來,那么就不能保證他們的有序性。
虛擬機就可以隨意的對他們進行重排序

 

 
        
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM