上次主要說了一個(gè)結(jié)論就是volatile,線程安全可見性的問題逢并,大部分情況下可見性都不需要管理的之剧,但是多線程編程的代碼中,我們會使用到volatile關(guān)鍵字砍聊,通過volatile關(guān)鍵字解決可見性問題背稼,一個(gè)線程對共享變量的修改,能夠及時(shí)的被其他線程看到玻蝌。只要加了volatile關(guān)鍵字蟹肘,所有對變量的讀取立刻進(jìn)行同步。volatile關(guān)鍵字的用途:禁止緩存俯树;相關(guān)的變量不做重排序帘腹。
(一)線程安全
- ① 介紹
線程安全是多線程編程時(shí)的計(jì)算機(jī)程序代碼中的一個(gè)概念。當(dāng)多個(gè)線程訪問某個(gè)方法時(shí)许饿,不管你通過怎樣的調(diào)用方式或者說這些線程如何交替的執(zhí)行阳欲,我們在主程序中不需要去做任何的同步,這個(gè)類的結(jié)果行為都是我們設(shè)想的正確行為,那么我們就可以說這個(gè)類是線程安全的胸完。
- ② 競態(tài)條件與臨界區(qū)
多線程訪問了相同的資源,向這些資源做了寫操作時(shí)翘贮,對執(zhí)行順序有要求赊窥。
臨界區(qū)
incr 方法內(nèi)部就是臨界區(qū)域,關(guān)鍵部分代碼的多線程并發(fā)執(zhí)行狸页,對會執(zhí)行結(jié)果產(chǎn)生影響锨能,下面的代碼就屬于臨界區(qū)。不見得就有一行代碼芍耘,只要對多線程并發(fā)有影響的都叫臨界區(qū)址遇。
int i = 0;
i =i +1;
x = i
競態(tài)條件
可能發(fā)生在臨界區(qū)域內(nèi)的特殊條件。觸發(fā)線程安全的環(huán)境斋竞。 上邊的代碼 x = i 就是競態(tài)條件倔约。
- ③ 問題代碼
多線程情況下,預(yù)期打印20000坝初,但是打印了13914浸剩。
public class LockDemo {
volatile int i = 0;
public void add() {
// TODO xx00
i++;// 三個(gè)步驟
}
public static void main(String[] args) throws InterruptedException {
LockDemo ld = new LockDemo();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
ld.add();
}
}).start();
}
Thread.sleep(2000L);
System.out.println(ld.i);
}
}
(二)共享資源
- 如果一段代碼是線程安全的,則它不包含競態(tài)條件鳄袍,只有當(dāng)多線程更新共享資源時(shí)绢要,才會發(fā)生競態(tài)條件。
- 棧封閉時(shí)拗小,不會在線程之間共享的變量重罪,都是線程安全的。
- 局部對象引用對象不共享哀九,但是引用了對象存儲在共享堆中剿配。如果方法內(nèi)創(chuàng)建的對象,只是在方法中傳遞勾栗,并且不對其他線程可用惨篱,那么也是線程安全的。
判定規(guī)則
如果創(chuàng)建围俘,使用和處理資源砸讳,永遠(yuǎn)不會逃脫單個(gè)線程的控制,該資源的使用線程安全的界牡。
(三)不可變對象
- ① 實(shí)例
public class Demo{
private int value = 0;
public Demo(int value){
this.value = value;
}
public int getValue(){
return this.value
}
}
方法里面沒有setValue的方法簿寂,這就是不可變的對象。
- ② 定義
創(chuàng)建不可變的共享對象來保證對象在線程共享時(shí)不會被修改宿亡,從而實(shí)現(xiàn)線程安全常遂。實(shí)例被創(chuàng)建,value變量就不能再被修改挽荠,這就是不可變性克胳。
不可變是相對的平绩,其實(shí)可以通過反射的方式進(jìn)行破壞。
(四)原子操作定義
- ① 介紹
原子操作可以是一個(gè)步驟漠另,也可以是多個(gè)操作步驟捏雌,但是其順序不可以被打亂,也不可以被分割而只執(zhí)行其中的一部分(不可中斷性)笆搓。
將整個(gè)操作視作一個(gè)整體性湿,資源在該次操作中保持一致,這是原子性的核心特性满败。
- ② 實(shí)例分析
public class Demo{
public int i = 0;
public void incr(){
i++
}
}
里面的i++ 底層運(yùn)行分為三步:加載i肤频,計(jì)算+1,賦值i 算墨,在底層被拆分了宵荒。
在多線程需要原子性操作,對修改米同,讀取骇扇,保持一致性。
(五)什么是CAS
- ① 介紹
compare and swap的縮寫面粮,中文翻譯成比較并交換少孝。屬于硬件同步原語,處理器提供了基本內(nèi)功操作的原子性保證熬苍。CAS操作需要輸入兩個(gè)數(shù)值稍走,一個(gè)舊值A(chǔ)(操作前的值)和一個(gè)新值B,在操作期間先比較下舊值有沒有發(fā)生變化柴底,如果沒有發(fā)生變化婿脸,才交換新值,發(fā)生了變化則不交換柄驻。避免硬件底層出現(xiàn)并發(fā)的操作的可能狐树。
JAVA中的sun,misc.Unsafe類,提供了compareAndSwpInt() 和 compareAndSwpLong() 等幾個(gè)方法實(shí)現(xiàn)CAS鸿脓。
- ② 演示
Unsafe 是操作c和c++底層來完成的抑钟。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class LockDemo {
volatile int value = 0;
static Unsafe unsafe; // 直接操作內(nèi)存,修改對象野哭,數(shù)組內(nèi)存....強(qiáng)大的API
private static long valueOffset;
static {
try {
// 反射技術(shù)獲取unsafe值
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
// 獲取到 value 屬性偏移量(用于定于value屬性在內(nèi)存中的具體地址)
valueOffset = unsafe.objectFieldOffset(LockDemo.class
.getDeclaredField("value"));
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void add() {
// TODO xx00
// i++;// JAVA 層面三個(gè)步驟
// CAS + 循環(huán) 重試
int current;
do {
// 操作耗時(shí)的話在塔, 那么 線程就會占用大量的CPU執(zhí)行時(shí)間
current = unsafe.getIntVolatile(this, valueOffset);
} while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
// 可能會失敗
}
public static void main(String[] args) throws InterruptedException {
LockDemo ld = new LockDemo();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
ld.add();
}
}).start();
}
Thread.sleep(2000L);
System.out.println(ld.value);
}
}
上邊的代碼太高大上了,基本都看不懂吧拨黔,下面說一個(gè)簡單的方式蛔溃。
(六)J.U.C 包內(nèi)的原子操作
- ① 介紹
java.util.concurrent(簡稱JUC)包,在此包中增加了在并發(fā)編程中很常用的工具類,用于定義類似于線程的自定義子系統(tǒng)贺待,包括線程池徽曲,異步 IO 和輕量級任務(wù)框架。還提供了設(shè)計(jì)用于多線程上下文中的 Collection 實(shí)現(xiàn)等麸塞。 rt.jar中的其實(shí)原子性疟位,jdk本身都考慮到了,定義了幾種類型喘垂。
- ② 封裝類
JDK1.8新增的原子性
原有的 Atomic系列類通過CAS來保證并發(fā)時(shí)操作的原子性,但是高并發(fā)也就意味著CAS的失敗次數(shù)會增多绍撞,失敗次數(shù)的增多會引起更多線程的重試正勒,最后導(dǎo)致AtomicLong的效率降低。新的四個(gè)類通過減少并發(fā)傻铣,將單一value的更新壓力分擔(dān)到多個(gè)value中去章贞,降低單個(gè)value的“熱度”以提高高并發(fā)情況下的吞吐量。
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
- ③ 實(shí)例分析
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: dispatch_system
* @description: ${description}
* @author: LiMing
* @create: 2019-10-31 10:57
**/
public class LockDemo {
// volatile int i = 0;
AtomicInteger i = new AtomicInteger(0);
public void add() {
// TODO xx00
// i++;// 三個(gè)步驟
i.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
LockDemo ld = new LockDemo();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
ld.add();
}
}).start();
}
Thread.sleep(2000L);
System.out.println(ld.i);
}
}
- ② LongAdder
就是嘗試使用分段CAS以及自動分段遷移的方式來大幅度提升多線程高并發(fā)執(zhí)行CAS操作的性能非洲!
在LongAdder的底層實(shí)現(xiàn)中鸭限,首先有一個(gè)base值,剛開始多線程來不停的累加數(shù)值两踏,都是對base進(jìn)行累加的败京,比如剛開始累加成了base = 5。接著如果發(fā)現(xiàn)并發(fā)更新的線程數(shù)量過多梦染,就會開始施行分段CAS的機(jī)制赡麦,也就是內(nèi)部會搞一個(gè)Cell數(shù)組,每個(gè)數(shù)組是一個(gè)數(shù)值分段帕识。這時(shí)泛粹,讓大量的線程分別去對不同Cell內(nèi)部的value值進(jìn)行CAS累加操作,這樣就把CAS計(jì)算壓力分散到了不同的Cell分段數(shù)值中了肮疗!這樣就可以大幅度的降低多線程并發(fā)更新同一個(gè)數(shù)值時(shí)出現(xiàn)的無限循環(huán)的問題晶姊,大幅度提升了多線程并發(fā)更新數(shù)值的性能和效率!而且內(nèi)部實(shí)現(xiàn)了自動分段遷移的機(jī)制伪货,也就是如果某個(gè)Cell的value執(zhí)行CAS失敗了们衙,那么就會自動去找另外一個(gè)Cell分段內(nèi)的value值進(jìn)行CAS操作。這樣也解決了線程空旋轉(zhuǎn)超歌、自旋不停等待執(zhí)行CAS操作的問題砍艾,讓一個(gè)線程過來執(zhí)行CAS時(shí)可以盡快的完成這個(gè)操作。會把base值和所有Cell分段數(shù)值加起來返回給你巍举。
計(jì)算的時(shí)候很快脆荷,取結(jié)果的是比較慢。這個(gè)思路就類似現(xiàn)在的互聯(lián)網(wǎng)分而治之的思路,量比較大蜓谋,就接很多小的管道梦皮,小管道里面慢慢的去處理,如果直接處理比較的大的比較慢桃焕,就讓小管道慢慢處理剑肯。分而治之的思路。
- ③ 示例
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
// 測試用例: 同時(shí)運(yùn)行2秒观堂,檢查誰的次數(shù)最多
public class LongAdderDemo {
private long count = 0;
// 同步代碼塊的方式
public void testSync() throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
long starttime = System.currentTimeMillis();
while (System.currentTimeMillis() - starttime < 2000) { // 運(yùn)行兩秒
synchronized (this) {
++count;
}
}
long endtime = System.currentTimeMillis();
System.out.println("SyncThread spend:" + (endtime - starttime) + "ms" + " v" + count);
}).start();
}
}
// Atomic方式
private AtomicLong acount = new AtomicLong(0L);
public void testAtomic() throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
long starttime = System.currentTimeMillis();
while (System.currentTimeMillis() - starttime < 2000) { // 運(yùn)行兩秒
acount.incrementAndGet(); // acount++;
}
long endtime = System.currentTimeMillis();
System.out.println("AtomicThread spend:" + (endtime - starttime) + "ms" + " v-" + acount.incrementAndGet());
}).start();
}
}
// LongAdder 方式
private LongAdder lacount = new LongAdder();
public void testLongAdder() throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
long starttime = System.currentTimeMillis();
while (System.currentTimeMillis() - starttime < 2000) { // 運(yùn)行兩秒
lacount.increment();
}
long endtime = System.currentTimeMillis();
System.out.println("LongAdderThread spend:" + (endtime - starttime) + "ms" + " v-" + lacount.sum());
}).start();
}
}
public static void main(String[] args) throws InterruptedException {
LongAdderDemo demo = new LongAdderDemo();
demo.testSync();
demo.testAtomic();
demo.testLongAdder();
}
}
(七)CAS三大問題
- ① ABA問題
因?yàn)镃AS需要在操作值的時(shí)候让网,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新师痕,但是如果一個(gè)值原來是A溃睹、變成了B、又變成了A胰坟,那么使用CAS進(jìn)行檢查時(shí)會發(fā)現(xiàn)它的值沒有發(fā)生變化因篇,但實(shí)際上卻變化了。
- ② 循環(huán)開銷時(shí)間長
自旋CAS如果長時(shí)間不成功笔横,會給CPU帶來非常大的執(zhí)行開銷竞滓。如果jvm能支持處理器提供的pause指令,那么效率會有一定的提升吹缔。pause指令有兩個(gè)作用:
1.它可以延遲流水線執(zhí)行指令(de-pipeline)商佑,使CPU不會消耗過多的執(zhí)行資源,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本厢塘,在一些處理器上延遲時(shí)間是零莉御。
2.它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率俗冻。
- ③ 只能保證一個(gè)共享變量的原子操作
當(dāng)對一個(gè)共享變量執(zhí)行操作時(shí)礁叔,我們可以使用循環(huán)CAS的方式來保證原子操作,但是多個(gè)共享變量操作時(shí)迄薄,循環(huán)CAS就無法保證操作的原子性琅关,這個(gè)時(shí)候就可以用鎖。還有一個(gè)方法讥蔽,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作涣易。比如,有兩個(gè)共享變量i=2,j=a合并一下ij=2a冶伞,然后用CAS來操作ij新症。從java1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性响禽,就可以把多個(gè)變量放在一個(gè)對象里來進(jìn)行CAS操作徒爹。
PS:代碼都是最終的結(jié)果荚醒,這里面涉及的思路很多,JDK已經(jīng)到13了里面的工具越來越多隆嗅。本次主要引用了原子性界阁,數(shù)據(jù)變化,保證數(shù)據(jù)的一致性胖喳,這是個(gè)本質(zhì)泡躯,希望各位老鐵參與評論,大家多交流丽焊。