前言:上一節(jié)學(xué)習(xí)了JMM国裳、Happen Before鞠苟、可見性等等這種概念,基本都是來源于JDK的官方網(wǎng)站中熔酷,上面有所說明了孤紧,能夠追根溯源才能夠跟上技術(shù)演進(jìn)。
9.0 來自JDK官方的多線程描述
JDK官方對于多線程相關(guān)理論的說明:
https://docs.oracle.com/javase/tutorial/essential/concurrency/index.html
里面有介紹同步關(guān)鍵字拒秘、原子性号显、死鎖等等概念。(源于官方才是原汁原味)
9.1 原子性的引入
9.1.1 多線程引起的問題
下面跟上節(jié)一樣躺酒,我們先用一個簡單的程序來說明押蚤,并發(fā)產(chǎn)生的問題
package szu.vander.lock;
import java.util.concurrent.TimeUnit;
/**
* @author : Vander
* @date : 2019/08/7
* @description :
*/
public class WrongLockDemo {
volatile int i = 0;
public void add() {
i++;
}
public static void main(String[] args) throws InterruptedException {
WrongLockDemo lockDemo = new WrongLockDemo();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
lockDemo.add();
}
}).start();
}
// 讓主線程Sleep 2秒,保證有足夠的時間運行完
TimeUnit.SECONDS.sleep(2);
System.out.println(lockDemo.i);
}
}
運行結(jié)果:發(fā)現(xiàn)并不是等于20000的羹应,而且遠(yuǎn)遠(yuǎn)不夠
我們先來簡單分析一下揽碘,首先i是加了volatile的,從上一節(jié)學(xué)習(xí)中知道了,加了此關(guān)鍵字能夠保證讀取的時候是主內(nèi)存的值雳刺,所以線程1對i進(jìn)行了加1操作肯定能被線程2發(fā)現(xiàn)的劫灶。第二個就是與i相關(guān)的操作不會進(jìn)行重排序。那么此處究竟是什么導(dǎo)致了沒加成功呢掖桦。
9.1.2 解析源碼
我們可以使用javap -v反編碼WrongLockDemo.class
我們發(fā)現(xiàn)i++其實是由好幾個步驟組成的本昏,首先是獲取到i的值,然后跟變量1相加枪汪,在把相加后的結(jié)果放回去涌穆。
說白了就是三個步驟:
1)加載i
2)執(zhí)行+1
3)賦值i
所以就會出現(xiàn)以下的情況,導(dǎo)致最后累加的結(jié)果不正確:
9.1.3 相關(guān)概念
線程安全
當(dāng)多個線程訪問某個類時雀久,這個類始終都能表現(xiàn)出正確的行為蒲犬,那么就稱這個類是線程安全的。(說白了就是在多線程的情況下岸啡,能得到你想要的原叮。)
競態(tài)條件與臨界區(qū)
多個線程訪問了相同的資源,向這些資源做了寫操作時巡蘸,對執(zhí)行順序有要求奋隶。
臨界區(qū):incr方法內(nèi)部就是臨界區(qū)域,關(guān)鍵部分代碼的多線程并發(fā)執(zhí)行悦荒,會對執(zhí)行結(jié)果產(chǎn)生影響唯欣。(簡單的說,就是某個方法在單線程運行沒問題搬味,多線程運行會有問題境氢,而這個方法就是臨界區(qū))
競態(tài)條件:可能發(fā)生在臨界區(qū)域內(nèi)的特殊條件。多線程執(zhí)行incr方法中的i++關(guān)鍵代碼時碰纬,產(chǎn)生了競態(tài)條件萍聊。(引發(fā)關(guān)鍵問題的關(guān)鍵代碼)
共享資源
如果一段代碼是線程安全的,則它不包含競態(tài)條件悦析。只有當(dāng)多個線程更新共享資源時寿桨,才會發(fā)生競態(tài)條件。
棧封閉時强戴,不會在線程之間共享的變量亭螟,都是線程安全的。
局部對象引用本身不共享骑歹,但是引用的對象存儲在共享堆中预烙。如果方法內(nèi)創(chuàng)建的對象,只是在方法中傳遞道媚,并且不對其他線程可用扁掸,那么也是線程安全的欢嘿。
局部變量只能由一個線程執(zhí)行,局部變量是存放在線程棧的棧幀里的也糊,不存在變量共享的問題炼蹦,所以不會有資源競爭的可能。
/**
* 像以下代碼也是線程安全的
*/
public vold someMethod() {
LocalObject localObject = new LocalObject();
localOblect.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
判定資源是否線程安全的規(guī)則:如果創(chuàng)建狸剃、使用和處理資源掐隐,永遠(yuǎn)不會逃脫單個線程的控制,該資源的使用時線程安全的钞馁。
不可變對象
public class Demo {
private int value = 0;
public Demo(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
以上代碼沒有提供set方法虑省,一旦構(gòu)造完成,該對象中的value屬性就不會再改變僧凰,這種變量稱為不可變對象探颈。
創(chuàng)建不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現(xiàn)線程安全训措。實例被創(chuàng)建伪节,value變量就不能再被修改,這就是不可變性绩鸣。
原子操作的定義
原子操作可以是一個步驟怀大,也可以是多個操作步驟,但是其順序不可以被打亂呀闻,也不可以被切割而只執(zhí)行其中的一部分(不可中斷性)
將整個操作視為一個整體化借,資源在該次操作中保持一致,這是原子性的核心特征捡多。
上述的incr()方法中的i++蓖康,實際上執(zhí)行的是三個步驟:1)加載 2)計算 3)賦值
也就是說,這三個步驟是不可中斷的垒手,否則原子操作就不成立了蒜焊。
9.2 原子性的實現(xiàn)方式
9.2.1 硬件同步原語—Unsafe類
CAS機(jī)制
Compare and swap比較和替換,屬于硬件同步原語淫奔,處理器提供了基本內(nèi)存操作的原子性保證山涡。CAS操作需要輸入兩個數(shù)值堤结,一個舊值A(chǔ)(期望操作前的值)和一個新值B唆迁,在操作
期間先比較舊值有沒有發(fā)生變化,如果沒有發(fā)生變化竞穷,才交換成新值唐责,發(fā)生了變化則不交換。
sun.mise.Unsafe
Java中的sun.mise.Unsafe類瘾带,提供了compareAndSwapInt()和compareAndSwapLong()等幾個方法實現(xiàn)CAS鼠哥。
在硬件底層中,對同一個內(nèi)存地址同一時刻只能有一個線程去修改,假設(shè)線程1,2都先讀取到了A=1朴恳,然后線程1先去修改這個內(nèi)存的值抄罕,改成功了,然后線程B也來改成2于颖,結(jié)果發(fā)現(xiàn)原來的值已經(jīng)改變了呆贿,所以不進(jìn)行+1操作了。
示例:使用Unsafe硬件原語實現(xiàn)自增的原子性
package szu.vander.atomicity;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
/**
* @author : Vander
* @date : 2019/11/20
* @description : Unsafe中的方法都是本地方法森渐,均由C實現(xiàn)
*/
public class UnsafeLockDemo {
volatile int num;
private static Unsafe unsafe;
private static long valueOffset;// 屬性偏移量做入,用于JVM去定位屬性在內(nèi)存中的地址
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
// CAS 硬件原語 ---java語言無法直接改內(nèi)存,曲線通過對象及屬性的定位方式
valueOffset = unsafe.objectFieldOffset(UnsafeLockDemo.class.getDeclaredField("num"));
} catch (Exception e) {
e.printStackTrace();
}
}
public void add() {
boolean result;
do {
// 1)獲取當(dāng)前值
int currentNum = unsafe.getIntVolatile(this, valueOffset);
// 2)計算值
int nextNum = currentNum + 1;
// 3)寫入值同衣,若num的值被其它線程修改了竟块,則操作不成功
result = unsafe.compareAndSwapInt(this, valueOffset, currentNum, nextNum);
} while (!result);
}
public static void main(String[] args) throws InterruptedException {
UnsafeLockDemo unsafeLockDemo = new UnsafeLockDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 每個線程循環(huán)加1w次
for (int temp = 0; temp < 10000; temp++) {
unsafeLockDemo.add();
}
}).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println("累加后的結(jié)果:" + unsafeLockDemo.num);
}
}
執(zhí)行效果:
9.2.2 JDK提供的java.util.concurrent
針對原子類的實現(xiàn)i++的方式,同一時刻只有一個線程能加成功耐齐,其它的線程都失敗浪秘,這樣必定會造成CPU資源的損耗和浪費,JDK1.8又提供了LongAdder等專門用于計數(shù)的類埠况。
J.U.C包內(nèi)的原子操作封裝類
JDK1.8后又進(jìn)行了部分更新:
更新器:DoubleAccumulator秫逝、LongAccumulator
計數(shù)器:DoubleAdder、LongAdder
計數(shù)器增強(qiáng)版询枚,高井發(fā)下性能更好
基本原理:頻繁更新但不太頻繁讀取的匯總統(tǒng)計信息時违帆,使用分成多個操作單元,不同線程更新不同的單元金蜀。只有需要匯總的時候才計算所有單元的操作
使用原子類實現(xiàn)累加
package szu.vander.atomicity;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author : caiwj
* @date : 2019/11/20
* @description : 原子遞增類的使用
*/
public class AtomicAdder {
AtomicInteger num = new AtomicInteger(0);
public void add() {
num.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
AtomicAdder atomicAdder = new AtomicAdder();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10_000; j++) {
atomicAdder.add();
}
}).start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println(atomicAdder.num.get());
}
}
9.2.2 性能比較
下面是三種加的方式進(jìn)行比較:Synchronize刷后、AtomicLong、LongAdder進(jìn)行性能比較
package szu.vander.atomicity;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* @author : Vander
* @date : 2019/11/20
* @description : 測試用例: 同時運行2秒渊抄,檢查誰的次數(shù)最多
*/
public class CompareAdder {
private long syncCount = 0;
/**
* 同步代碼塊的方式
*/
public void testSync() {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 2000) { // 運行兩秒
synchronized (this) {
++syncCount;
}
}
long endTime = System.currentTimeMillis();
System.out.println("SyncThread spend:" + (endTime - startTime) + "ms" + " count:" + syncCount);
}).start();
}
}
private AtomicLong atomicLongCount = new AtomicLong(0L);
/**
* Atomic方式
*/
public void testAtomic() {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 2000) { // 運行兩秒
atomicLongCount.incrementAndGet();
}
long endTime = System.currentTimeMillis();
System.out.println("AtomicThread spend:" + (endTime - startTime) + "ms" + " count:" + atomicLongCount.incrementAndGet());
}).start();
}
}
private LongAdder longAdderCount = new LongAdder();
/**
* LongAdder 方式
*/
public void testLongAdder() {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 2000) { // 運行兩秒
longAdderCount.increment();
}
long endTime = System.currentTimeMillis();
System.out.println("LongAdderThread spend:" + (endTime - startTime) + "ms" + " count:" + longAdderCount.sum());
}).start();
}
}
public static void main(String[] args) {
CompareAdder demo = new CompareAdder();
demo.testSync();
demo.testAtomic();
demo.testLongAdder();
}
}
執(zhí)行結(jié)果:
可以發(fā)現(xiàn)JDK8新實現(xiàn)的累加器確實提高了接近一倍的性能尝胆,而原子類又會比同步關(guān)鍵字操作的累加性能提升五倍。
LongAdder實現(xiàn)思路:
思路就是不讓多個線程操作同一個變量护桦,作累加操作含衔,線程1加了X次,線程2加了y次二庵,線程3加了z次贪染,最后通過sum方法來讀取這些線程累加起來的值。
這種思路是分而治之的思路催享,不同的線程只Add屬于它自己的變量杭隙,最后通過sum累加起來。這就類似于高并發(fā)的時候因妙,使用集群來分擔(dān)壓力痰憎。
9.3 CAS機(jī)制的局限性
CAS的三個問題
1)循環(huán)+CAS票髓,自旋的實現(xiàn)讓所有線程都處于高頻運行,爭搶CPU執(zhí)行時間的狀態(tài)铣耘。如果操作長時間不成功洽沟,會帶來很大的CPU資源消耗。
2)僅針對單個變量的操作蜗细,不能用于多個變量來實現(xiàn)原子操作玲躯。
3)ABA問題。(無法體現(xiàn)出數(shù)據(jù)的變動)
針對第一點鳄乏,CAS操作適用于一些耗時較短的操作跷车,不然長時間的不成功會導(dǎo)致CPU壓力巨大,CAS實際上是使用自旋鎖來實現(xiàn)的橱野。
ABA問題
所謂的ABA問題朽缴,其實影響并不大,即線程一先修改了i的值水援,然后線程二又將值改回來密强,線程三來讀取的時候就發(fā)現(xiàn)值沒有變化,然后線程三繼續(xù)進(jìn)行操作蜗元。如果要避免這種情況或渤,只需要在每次修改都增加一個修改次數(shù)的標(biāo)識即可。
其它:
Unsafe類是沒有注釋的奕扣,要看到更詳細(xì)的需要看OpenJDK薪鹦。
OpenJDK官方網(wǎng)站:OpenJDK.java.net