這期內(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)在以下三個方面:
原子性:提供了互斥訪問,同一時刻只能有一個線程來對它進行操作。
可見性:一個線程對主內(nèi)存的修改可以及時的被其他線程觀察到。
有序性:一個線程觀察其他線程中的指令執(zhí)行順序,由于指令重排序的存在,該觀察結(jié)果一般雜亂無序。
新建一個測試類,內(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ù)不一定是一樣的,因此需要做同步操作才可以。
我們將上面的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包:
AtomicReference和AtomicInteger非常類似,不同之處就在于AtomicInteger是對整數(shù)的封裝,底層采用的是compareAndSwapInt實現(xiàn)CAS,比較的是數(shù)值是否相等,而AtomicReference則對應(yīng)普通的對象引用,底層使用的是compareAndSwapObject實現(xiàn)CAS,比較的是兩個對象的地址是否相等。也就是它可以保證你在修改對象引用時的線程安全性。
原子性更新某個類的實例的某個字段的值,并且這個字段必須用volatile關(guān)鍵字修飾同時不能是static修飾的。
private static AtomicIntegerFieldUpdaterupdater = 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()); } }
ABA問題是:在CAS操作的時候,其他線程將變量的值A(chǔ)改成了B,隨后又改成了A,CAS就會被誤導(dǎo)。所以ABA問題的解決思路就是將版本號加一,當一個變量被修改,那么這個變量的版本號就增加1,從而解決ABA問題。
@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。
修飾的對象主要有一下四種:
修飾代碼塊:大括號括起來的代碼,作用于調(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)方法,被修飾的方法稱為同步方法,作用范圍是整個方法,作用的對象是調(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)存中重新讀取最新的值(注意,加鎖與解鎖是同一把鎖)
通過加入內(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í)行的正確性。
程序次序規(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è)資訊頻道。