JUC併發編程之:簡單概述(四)
##本章內容:
無鎖併發--樂觀鎖(非阻塞)
·CAS與volatile
·原子整數
·原子引用
·原子數組
·字段更新器
·原子累加器
·Unsafe
一、CAS與volatile
1、保護共享資源
·有一個賬戶,有1萬元,現在有1000個線程,每個線程每次取10元,最後會剩下0元
##錯誤實現 與 加鎖實現
@Slf4j
public class CasAndVolatile01 {
public static void main(String[] args) {
//unsafe實現
Account account1 = new AccountUnsafe(10000);
Account.demo(account1);
//加鎖實現
Account account2 = new AccountLock(10000);
Account.demo(account2);
}
}
//加鎖實現
class AccountLock implements Account{
Integer balance;
public AccountLock(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
synchronized (this){
return this.balance;
}
}
@Override
public void withDraw(Integer amount) {
synchronized (this){
this.balance -= amount;
}
}
}
//錯誤實現
class AccountUnsafe implements Account{
Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return this.balance;
}
@Override
public void withDraw(Integer amount) {
this.balance -= amount;
}
}
interface Account{
//獲取餘額
Integer getBalance();
//取款
void withDraw(Integer amount);
//方法內啓動1000個線程,每個線程-10元
//如果總額爲10000,那麼餘額將會爲0
static void demo(Account account){
List<Thread> ts = new ArrayList<>();
for(int i=0;i<1000;i++){
ts.add(new Thread(()->{
account.withDraw(10);
}));
}
//計算起始時間
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+" cost : "+(end-start)/1000_000+"ms");
}
}
##結果:
310 cost : 80ms ##錯誤實現
0 cost : 69ms ##加鎖實現
##無鎖實現
//無鎖實現
class AccountCas implements Account{
AtomicInteger balance;
public AccountCas(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get(); //底層是volatile
}
@Override
public void withDraw(Integer amount) {
while(true){
//餘額最新值
Integer prev = balance.get();
//修改後的餘額
Integer next = prev - amount;
//同步到主存--比較並設置
if(balance.compareAndSet(prev,next)){
break;
}
}
}
}
##結果:
160 cost : 85ms ##錯誤實現
0 cost : 73ms ##加鎖實現
0 cost : 65ms ##無鎖實現
2、CAS與volatile
CAS:
·上面AtomicInteger的解決方法,內部並沒有使用鎖來保護共享變量的線程安全,它的實現原理:
@Override
public void withDraw(Integer amount) {
while(true){
//餘額最新值
Integer prev = balance.get();
//修改後的餘額
Integer next = prev - amount;
//同步到主存---比較並設置
if(balance.compareAndSet(prev,next)){
break;
}
}
}
其中compareAndSet,它的簡稱就是CAS(也稱 compare and swap),它必須是原子操作
·CAS底層是lock cmpxchg指令(X86架構),在單個CPU和多核CPU下都能夠保證【比較-交換】的原子
性
·在多核狀態下,某個執行到帶lock的指令時,CPU會讓總線鎖住,當這個核把此指令執行完畢,再開啓
總線,這個過程中不會被線程的調度機制所打斷,保證了多個線程對內存操作的準確性,是原子的。
【CAS會在修改前判斷 自己共享變量修改之前讀到的數據和當前的數據是否一致,如果不一致則返回
·false,重新修改,如果一致則返回true修改成功】
volatile:
·獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾。
·它可以用來修飾成員變量和靜態成員變量,它可以避免線程從自己的工作緩存中查找變量的值,必須到
主存中獲取他的值,線程操作volatile變量都是直接操作主存。即一個線程對volatile變量的修改,
對另一個線程可見。
##注意:
volatile僅保證了共享變量的可見性,讓其他線程能夠看到最新值,但不能解決指令交錯問題
(不能保證原子性)
·【CAS必須藉助volatile才能讀取到共享變量的最新值來實現【比較並交換】的效果】
##AtomicInteger部分源碼:
public class AtomicInteger{
private volatile int value;
}
3、CAS效率分析
##爲什麼無鎖效率高?
·無鎖情況下,即使重試失敗,線程始終在高速運行,沒有停歇,而synchronized會讓線程在沒有獲得
鎖的時候,發生上下文切換,進入阻塞
(打個比方:線程就好像高速跑道上的賽車,高速運行時,速度超快,一旦發生上下文切換,就好比賽車減
速,熄火,等被喚醒又得重新打火、啓動、加速恢復到高速運行,代價比較大)
·但無鎖情況下因爲線程要保持運行,需要額外CPU的支持,CPU在這裏就好比高速跑道,沒有額外的跑
道,線程想高速運行也無從談起,雖然不會進入阻塞,但由於沒有分到時間片,仍然會進入可運行狀態,
還是會導致上下文切換。
4、CAS特點
·CAS結合volatile可以實現無鎖併發,適用於線程數少,多核CPU的場景下:
>CAS是基於樂觀鎖的思想:最樂觀的估計,不怕別的線程來修改共享變量,就算改了也沒關係,我喫點虧
再重試唄
>synchronized是基於悲觀鎖的思想:最悲觀的估計,得防着其他線程來修改共享變量,我上了鎖你們
都別想改,我改完了解開鎖,你們纔有機會
>CAS體現的是無鎖併發,無阻塞併發:
>>因爲沒有使用synchronized,所以縣城不會陷入阻塞,這是效率提升的因素之一
>>但如果競爭激烈,可以想到重試必然發生,反而會影響效率
二、原子整數
##JUC併發包提供了一些供CAS實現工具類:
·AtomicBoolean
·AtomicInteger
·AtomicLong
##ActomicInteger常用方法:
@Slf4j
public class ActomicIntegerTest {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
System.out.println(i.incrementAndGet());//++i
System.out.println(i.getAndIncrement());//i++
System.out.println(i.addAndGet(5));//先+5然後get
System.out.println(i.getAndAdd(5));//先get然後+5
System.out.println(i.updateAndGet(x->x*5));//先乘以5後get
System.out.println(i.getAndUpdate(x->x*5));//先get然後乘以5
System.out.println(i.get());
}
}
updateAndGet( )底層源碼:
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
三、原子引用
##由於我們的共享變量並不一定都是基本數據類型,比如說BigDecimal,我們就可以使用原子引用
保證該共享變量的線程安全
·AtomicReference
·AtomicMarkableReference
·AtomicStampedReference 有版本號的
3.1、AtomicReference
public class CasAndVolatile02 {
public static void main(String[] args) {
DecimalAccount account = new BigDecimalAccountCas(new BigDecimal("10000"));
DecimalAccount.demo(account);
}
}
class BigDecimalAccountCas implements DecimalAccount{
private AtomicReference<BigDecimal> balance;
public BigDecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withDraw(BigDecimal amount) {
while (true){
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if(balance.compareAndSet(prev,next)){
break;
}
}
}
}
interface DecimalAccount{
//獲取餘額
BigDecimal getBalance();
//取款
void withDraw(BigDecimal amount);
//方法內啓動1000個線程,每個線程-10元
//如果總額爲10000,那麼餘額將會爲0
static void demo(DecimalAccount account2){
List<Thread> ts = new ArrayList<>();
for(int i=0;i<1000;i++){
ts.add(new Thread(()->{
account2.withDraw(BigDecimal.TEN);
}));
}
//計算起始時間
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account2.getBalance()
+" cost : "+(end-start)/1000_000+"ms");
}
}
3.2、ABA問題:
##主線程將共享變量從A---修改成---->C
##但在主線程修改過程中t線程將共享變量從A---修改成---->B---又修改成--->A
##主線程還是會修改成功,但主線程是無法感知共享變量的變化的
##代碼如下:
@Slf4j
public class AtomicReferenceABA01 {
static AtomicReference<String> at = new AtomicReference("A");
public static void main(String[] args) {
log.debug("main start...");
String prev = at.get();
other();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("main change A-->C : {}",at.compareAndSet(prev,"C"));
}
private static void other(){
new Thread(()->{
log.debug("t1 change A-->B : {}",at.compareAndSet("A","B"));
},"t1").start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
log.debug("t1 change B-->A : {}",at.compareAndSet("B","A"));
},"t2").start();
}
}
##結果:
10:06:54.024 [main] DEBUG xxx - main start...
10:06:54.119 [t1] DEBUG xxx - t1 change A-->B : true
10:06:54.620 [t2] DEBUG xxx - t1 change B-->A : true
10:06:55.634 [main] DEBUG xxx - main change A-->C : true
##如何才能讓main線程感知到 共享變量被修改呢?
##只要有其他線程動過了共享變量,那麼自己的CAS就算失敗,這時僅比較值是不夠的,
需要再增加一個版本號
3.3、AtomicStampedReference
@Slf4j
public class AtomicStampedReferenceABA02 {
static AtomicStampedReference<String> asr =new AtomicStampedReference<>("A",0);
public static void main(String[] args) {
log.debug("main start....");
String prev = asr.getReference();
int stamp = asr.getStamp();
log.debug("main prev:{} , stamp:{}",prev,stamp);
other();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("main change A-->C : {}",asr.compareAndSet(prev,"C",stamp,stamp+1));
}
private static void other(){
new Thread(()->{
String prev = asr.getReference();
int stamp = asr.getStamp();
log.debug("t1 prev:{} , stamp:{}",prev,stamp);
log.debug("t1 change A-->B : {}",asr.compareAndSet(prev,"B",stamp,stamp+1));
},"t1").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
String prev = asr.getReference();
int stamp = asr.getStamp();
log.debug("t2 prev:{} , stamp:{}",prev,stamp);
log.debug("t2 change B-->A : {}",asr.compareAndSet(prev,"A",stamp,stamp+1));
},"t2").start();
}
}
##結果:
10:25:43.158 [main] DEBUG xxx - main start....
10:25:43.172 [main] DEBUG xxx - main prev:A , stamp:0
10:25:43.217 [t1] DEBUG xxx - t1 prev:A , stamp:0
10:25:43.217 [t1] DEBUG xxx - t1 change A-->B : true
10:25:44.227 [t2] DEBUG xxx - t2 prev:B , stamp:1
10:25:44.227 [t2] DEBUG xxx - t2 change B-->A : true
10:25:45.242 [main] DEBUG xxx - main change A-->C : false
3.4、AtomicMarkableReference
##AtomicStampedReference 通過版本號 可以記錄 是否被修改了,和修改了幾次
##AtomicMarkableReference 通過boolean標記 是否被修改
@Slf4j
public class AtomicMarkableReferenceTest {
public static void main(String[] args) {
GarbageBag bag = new GarbageBag("一個滿了的垃圾袋");
AtomicMarkableReference<GarbageBag> amr = new AtomicMarkableReference<>(bag,true);
log.debug("main start...");
GarbageBag prev = amr.getReference();
log.debug("main prev:{}",prev);
new Thread(()->{
log.debug("清潔阿姨 更換垃圾袋: {}",amr.compareAndSet(prev,new GarbageBag("清潔阿姨放了一個新的垃圾袋"),true,false));
},"清潔阿姨").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("main 更換垃圾袋: {}",amr.compareAndSet(prev,new GarbageBag("main放了一個新的垃圾袋"),true,false));
}
}
@Data
@ToString
@AllArgsConstructor
class GarbageBag{
private String desc;
}
##結果:
10:41:53.070 [main] DEBUG xxx - main start...
10:41:53.086 [main] DEBUG xxx - main prev:GarbageBag(desc=一個滿了的垃圾袋)
10:41:53.150 [清潔阿姨] DEBUG xxx - 清潔阿姨 更換垃圾袋: true
10:41:54.149 [main] DEBUG xxx - main 更換垃圾袋: false
四、原子數組
·AtomicIntegerArray
·AtomicLongArray
·AtomicReferenceArray
##原子數組可以在多線程中保護數組裏的元素
@Slf4j
public class AtomicIntegerArrayTest {
public static void main(String[] args) {
//創建了容量爲10個int的AtomicIntegerArray
AtomicIntegerArray array = new AtomicIntegerArray(10);
log.debug("第2個值:{}",array.get(1));
int[] array2 = new int[]{1,2,3,4,5,6,7,8,9,10};
AtomicIntegerArray array3 = new AtomicIntegerArray(array2);
log.debug("第2個值:{}",array3.get(1));
log.debug("第2個值+1:{}",array3.getAndIncrement(1));
log.debug("第2個值:{}",array3.get(1));
log.debug("第3個值+5:{}",array3.getAndAdd(2,5));
log.debug("第3個值:{}",array3.get(2));
log.debug("第4個值*7:{}",array3.getAndUpdate(3,t->t*7));
log.debug("第3個值:{}",array3.get(3));
log.debug("修改第5個值爲999");
array3.set(4,999);
log.debug("第5個值:{}",array3.get(4));
}
}
##結果:
11:45:37.301 [main] DEBUG xxx - 第2個值:0
11:45:37.316 [main] DEBUG xxx - 第2個值:2
11:45:37.317 [main] DEBUG xxx - 第2個值+1:2
11:45:37.317 [main] DEBUG xxx - 第2個值:3
11:45:37.317 [main] DEBUG xxx - 第3個值+5:3
11:45:37.317 [main] DEBUG xxx - 第3個值:8
11:45:37.361 [main] DEBUG xxx - 第4個值*7:4
11:45:37.361 [main] DEBUG xxx - 第3個值:28
11:47:45.114 [main] DEBUG xxx - 修改第5個值爲999
11:47:45.114 [main] DEBUG xxx - 第5個值:999
五、字段更新器
·AtomicReferenceFieldUpdater
·AtomicIntegerFieldUpdater
·AtomicLongFieldUpdater
##字段更新器可以在多線程中保護對象的某個屬性\成員變量
@Slf4j
public class AtomicReferenceFieldUpdaterTest {
public static void main(String[] args) {
Student st = new Student("李四");
//參數:類,要更新字段的類型,要更新字段的名稱
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
log.debug("更新名稱爲張三:{}",updater.compareAndSet(st,"李四","張三"));
log.debug("st : {}",st);
}
}
@Data
@ToString
@AllArgsConstructor
class Student{
volatile String name;
}
六、原子累加器
##ActomicLong和LongAdder累加的性能比較
@Slf4j
public class LongAdderTest {
public static void main(String[] args) {
for (int i = 0; i <5 ; i++) {
demo(
()->new AtomicLong(0),
(adder)->adder.getAndIncrement()
);
}
log.debug("-------------");
for (int i = 0; i <5 ; i++) {
demo(
()->new LongAdder(),
(adder)->adder.increment()
);
}
}
private static <T> void demo(Supplier<T> supplier, Consumer<T> consumer){
T adder = supplier.get();
List<Thread> ts = new ArrayList<>();
//4個線程,每人累加50萬
for (int i=0;i<4;i++){
ts.add( new Thread(()->{
for(int j=0;j<500000;j++){
consumer.accept(adder);
}
}));
}
long start = System.nanoTime();
ts.forEach(t->t.start());
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
log.debug("adder {}, cost : {} ms",adder,(end-start)/1000_000);
}
}
##結果:
14:57:20.170 [main] DEBUG xxx - adder 2000000, cost : 49 ms
14:57:20.228 [main] DEBUG xxx - adder 2000000, cost : 43 ms
14:57:20.265 [main] DEBUG xxx - adder 2000000, cost : 36 ms
14:57:20.303 [main] DEBUG xxx - adder 2000000, cost : 37 ms
14:57:20.344 [main] DEBUG xxx - adder 2000000, cost : 41 ms
14:57:20.344 [main] DEBUG xxx - -------------
14:57:20.359 [main] DEBUG xxx - adder 2000000, cost : 13 ms
14:57:20.364 [main] DEBUG xxx - adder 2000000, cost : 5 ms
14:57:20.370 [main] DEBUG xxx - adder 2000000, cost : 5 ms
14:57:20.377 [main] DEBUG xxx - adder 2000000, cost : 6 ms
14:57:20.382 [main] DEBUG xxx - adder 2000000, cost : 4 ms
##LongAdder性能提升的原因:
LongAdder性能提升的原因很簡單,就是在有競爭時,設置多個累加單元,Thread-0累加Cell[0]
而Thread-1累加Cell[1]...最後將結果彙總,這樣他們在累加時操作的不同的Cell變量,因此減
少了CAS重試失敗,從而提高性能
七、Unsafe
·Unsafe對象提供了非常底層的,操作內存、線程的方法
·Unsafe對象不能直接調用,只能通過反射獲得
獲取Unsafe:
@Slf4j
public class UnsafeAccessor {
public static void main(String[] args) {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
log.debug("unsafe:{}",unsafe);
}
}
Unsafe CAS操作:
/**
* 使用Unsafe線程安全的操作Teacher對象的成員變量
* (之前使用AtomicReferenceFieldUpdater)
*/
@Slf4j
public class UnsafeAccessor {
public static void main(String[] args) {
try {
//獲取Unsafe
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
log.debug("unsafe:{}",unsafe);
Teacher tc = new Teacher(1,"張三");
//獲取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
//執行CAS操作
unsafe.compareAndSwapInt(tc,idOffset,1,2);
unsafe.compareAndSwapObject(tc,nameOffset,"張三","李四");
//檢驗
log.debug("tc : {}",tc);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
@Data
@ToString
@AllArgsConstructor
class Teacher{
volatile int id;
volatile String name;
}
Unsafe模擬原子整數:
@Slf4j
public class UnsafeForAtomicInteger {
public static void main(String[] args) {
MyAtomicInteger mat = new MyAtomicInteger(10000);
List<Thread> ts = new ArrayList<>();
//1000個線程,每個線程減去10
for(int i=0;i<1000;i++){
ts.add(new Thread(()->{
mat.decrement(10);
}));
}
ts.forEach(t->t.start());
ts.forEach(t->{
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
log.debug("mat : {}",mat.get());
}
}
class MyAtomicInteger{
private volatile Integer value;
private static final long valueOffset;
private static final Unsafe UNSAFE;
static{
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException();
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
public MyAtomicInteger(Integer value) {
this.value = value;
}
public Integer get(){
return value;
}
public void decrement(Integer num){
while (true){
Integer prev = this.value;
Integer next = prev - num;
if(UNSAFE.compareAndSwapObject(this,valueOffset,prev,next)){
break;
}
}
}
}