需要回顧之前博文《多線程的問(wèn)題》
一稚新、 CPU對(duì)于兩個(gè)冒險(xiǎn)的解決辦法
- 結(jié)構(gòu)冒險(xiǎn)(CPU對(duì)某一個(gè)存儲(chǔ)器讀取資源為例):
使用同步既绩,依賴硬件提供同步指令类咧。 - 數(shù)據(jù)冒險(xiǎn):
二障簿、多線程對(duì)問(wèn)題的解決辦法
- <b>2.1 安全問(wèn)題的代碼</b>
package com.tinygao.thread.safe;
public class UnSafe {
private int value;
public int getNext() {
return value++;
}
}
- <b>2.2 為什么不安全</b>
<b>!卷谈!因?yàn)樗邆?個(gè)特性杯拐。永遠(yuǎn)記住這四點(diǎn),絕大部分只要這4點(diǎn)J勒帷端逼!</b>
- <b>有可變的狀態(tài)(以下三個(gè)地方代表類(lèi)是有狀態(tài)的特征)</b>
1、有類(lèi)變量
2 污淋、有實(shí)例變量(本例子中的value屬于這類(lèi))
3顶滩、有其他對(duì)象的引用(比如map.entry對(duì)象)
</br>
<b>復(fù)合操作(value++包含三個(gè)原子操作)</b>
1、讀取value
2寸爆、value+1計(jì)算
3礁鲁、將value寫(xiě)入主存
</br><b>順序沒(méi)有控制</b>
B線程沒(méi)有等待A操作完盐欺,就讀取了value。導(dǎo)致執(zhí)行兩次加法仅醇,但最終結(jié)果都是10
</br><b>狀態(tài)不可見(jiàn)</b>
A線程中value值的改變冗美,對(duì)B來(lái)說(shuō)不可見(jiàn)。線程棧內(nèi)存數(shù)據(jù)是互相隔離的析二,看不到粉洼!為什么不安全之提問(wèn)
1、沒(méi)有狀態(tài)的類(lèi)叶摄,是線程安全的 (√)
2属韧、只要使用線程安全的類(lèi)寫(xiě)出來(lái)的代碼塊一定是線程安全的(×)
==>多個(gè)安全類(lèi)在一起成了復(fù)合操作了,加上沒(méi)有控制順序的手段蛤吓,可能會(huì)出現(xiàn)不可預(yù)測(cè)的結(jié)果宵喂。
3、不可變對(duì)象一定是線程安全的(√)
==>什么是不可變對(duì)象会傲?
1樊破、對(duì)象創(chuàng)建之后其狀態(tài)就不能修改。
2唆铐、對(duì)象的所有狀態(tài)都是final類(lèi)型
3、對(duì)象是正確創(chuàng)建的(this引用沒(méi)有逸出)
↓對(duì)于第二問(wèn)看concurrentMap是線程安全的奔滑,用了他的代碼塊可不是線程安全的艾岂,來(lái)舉個(gè)栗子吧↓
package com.tinygao.thread.safe;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Created by gsd on 2017/1/26.
*/
@Slf4j
public class UnSafe {
private int num;
private Map<String, String> map = Maps.newConcurrentMap();
public int getNumAdd() {
return num++;
}
public String getMapValue() {
if(!map.containsKey("tinygao")) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put("tinygao", Thread.currentThread().getName());
}
return map.get("tinygao");
}
public static void main(String[] args) {
UnSafe unSafe = new UnSafe();
ExecutorService es = Executors.newFixedThreadPool(
2,
new ThreadFactoryBuilder().setNameFormat("map-%d").build());
es.submit(()->{
log.info("map {}",unSafe.getMapValue());
});
es.submit(()->{
log.info("map {}",unSafe.getMapValue());
});
}
}
- 我們的本意:當(dāng)?shù)谝粋€(gè)線程判斷不存在mapkey的時(shí)候去填充這個(gè)key的值,之后的其他線程只要從map get出來(lái)這個(gè)key就可以了朋其。
事實(shí):兩個(gè)線程都判斷了mapkey不存在王浴,都各自put了一把到map上。導(dǎo)致mapkey的值被覆蓋了梅猿。打印結(jié)果(你的可能跟我不一樣):↓
[map-1] INFO com.tinygao.thread.safe.UnSafe - map map-1
[map-0] INFO com.tinygao.thread.safe.UnSafe - map map-0<b>2.3 解決安全問(wèn)題</b>
對(duì)應(yīng)上面的四個(gè)特性取反:
<b>1氓辣、去可變狀態(tài)</b>
<b>2、復(fù)合操作改成原子操作(記住兩個(gè)常見(jiàn)的復(fù)合操作)</b>
-- if-then操作(像上面map的例子)
-- 取-讀-寫(xiě)(像上面value++的例子)
<b>3袱蚓、控制執(zhí)行順序钞啸,即保證有序性</b>
<b>4、若有狀態(tài)喇潘,則讓狀態(tài)在線程間互相可見(jiàn)</b>
</br>
<b>2.3.1体斩、怎么去可變狀態(tài)</b>
- 不再是成員變量、實(shí)例變量了颖低。比如:把他移入到方法內(nèi)部絮吵,這樣就在自己的線程棧中了
public int getNumAdd() {
int num = 0;
return num++;
}
- 不在線程之間共享該狀態(tài)(讓狀態(tài)封閉)
private ThreadLocal<Integer> num = new ThreadLocal<>();
public int getNumAdd() {
num.set(num.get()+1);
return num.get();
}
- 讓狀態(tài)做到不可變
final inti num = 1
再看一個(gè)不安全的
```
package com.tinygao.thread.safe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
//這個(gè)是不安全的
@Slf4j
public class Unfinal {
private Integer lastNumber;
private Integer currentNumber;
public void setLastNumber(Integer i) {
this.lastNumber = i;
}
public Integer getCurrentNumber(Integer i) {
if(lastNumber == null || !lastNumber.equals(i)) {
return null;
}
else {
return 1;
}
}
public static void main(String[] args) {
Unfinal unfinal = new Unfinal();
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(()->{
unfinal.setLastNumber(1);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//預(yù)期打印是1,看看實(shí)際打印多少?
log.info("get first {}", unfinal.getCurrentNumber(1));
});
es.submit(()->{
unfinal.setLastNumber(2);
//預(yù)期打印是null忱屑,看看實(shí)際打印多少?
log.info("get seconde {}", unfinal.getCurrentNumber(1));
});
es.shutdown();
}
}
看一個(gè)final安全的蹬敲,保證在構(gòu)造函數(shù)中初始化一次狀態(tài)后不可變了
package com.tinygao.thread.safe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
public class FinalClass {
private final Integer lastNumber;
private final Integer currentNumber;
public FinalClass(Integer lastNumber, Integer currentNumber) {
this.lastNumber = lastNumber;
this.currentNumber = currentNumber;
}
public Integer getCurrentNumber(Integer i) {
if(lastNumber == null || !lastNumber.equals(i)) {
return null;
}
else {
return 1;
}
}
public static void main(String[] args) {
FinalClass finalclass = new FinalClass(1, 1);
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(()->{
log.info("get first {}", finalclass.getCurrentNumber(1));
});
es.submit(()->{
log.info("get seconde {}", finalclass.getCurrentNumber(1));
});
es.shutdown();
}
}
<b> 2.3.2暇昂、怎么將復(fù)合操作變成原子操作</b>
- "讀-操作-寫(xiě)" 使用Atomic類(lèi)
private AtomicInteger num2 = new AtomicInteger(0);
public int getNumAdd2() {
return num2.incrementAndGet();
}
- "if-then" 使用java自帶的原子操作
private Map<String, String> map = Maps.newConcurrentMap();
public void safeMap() {
map.putIfAbsent("tinygao", Thread.currentThread().getName());
}
<b> 2.3.3、怎么控制執(zhí)行順序</b>
- 同步
1伴嗡、synchronized
2急波、volatile類(lèi)型的變量(但復(fù)合操作變量就有問(wèn)題了)
3、顯式鎖
4闹究、原子變量(long和double可能不是原子變量幔崖,看處理器架構(gòu))
<b> 2.3.4、怎么讓狀態(tài)在線程間可見(jiàn)</b>
- 同步
1渣淤、synchronized
2赏寇、volatile類(lèi)型的變量(但復(fù)合操作變量就有問(wèn)題了)
3、顯式鎖
4价认、原子變量(long和double可能不是原子變量嗅定,看處理器架構(gòu))
采用互斥。對(duì)于共享資源訪問(wèn)的<b>代碼片段</b>叫做臨界區(qū)
有多種方法用踩,比如鎖變量(0/1)渠退、嚴(yán)格輪換法
概念:管程、信號(hào)量脐彩、互斥量碎乃、同步、阻塞惠奸、協(xié)作梅誓、互斥。
https://zh.wikipedia.org/wiki/%E7%9B%A3%E8%A6%96%E5%99%A8_(%E7%A8%8B%E5%BA%8F%E5%90%8C%E6%AD%A5%E5%8C%96)
198.35.26.96 zh.wikipedia.org
待續(xù)~