需要回顧之前博文《多線程的問題》
一、 CPU對于兩個冒險的解決辦法
- 結(jié)構(gòu)冒險(CPU對某一個存儲器讀取資源為例):
使用同步,依賴硬件提供同步指令。 - 數(shù)據(jù)冒險:
二、多線程對問題的解決辦法
- <b>2.1 安全問題的代碼</b>
package com.tinygao.thread.safe;
public class UnSafe {
private int value;
public int getNext() {
return value++;
}
}
- <b>2.2 為什么不安全</b>
A/B代表兩個不同線程,不安全執(zhí)行錯誤情況
<b>??!因為它具備4個特性。永遠記住這四點,絕大部分只要這4點??!</b>
- <b>有可變的狀態(tài)(以下三個地方代表類是有狀態(tài)的特征)</b>
1、有類變量
2 、有實例變量(本例子中的value屬于這類)
3、有其他對象的引用(比如map.entry對象)
</br>
<b>復合操作(value++包含三個原子操作)</b>
1、讀取value
2、value+1計算
3、將value寫入主存
</br><b>順序沒有控制</b>
B線程沒有等待A操作完,就讀取了value。導致執(zhí)行兩次加法,但最終結(jié)果都是10
</br><b>狀態(tài)不可見</b>
A線程中value值的改變,對B來說不可見。線程棧內(nèi)存數(shù)據(jù)是互相隔離的,看不到!為什么不安全之提問
1、沒有狀態(tài)的類,是線程安全的 (√)
2、只要使用線程安全的類寫出來的代碼塊一定是線程安全的(×)
==>多個安全類在一起成了復合操作了,加上沒有控制順序的手段,可能會出現(xiàn)不可預測的結(jié)果。
3、不可變對象一定是線程安全的(√)
==>什么是不可變對象?
1、對象創(chuàng)建之后其狀態(tài)就不能修改。
2、對象的所有狀態(tài)都是final類型
3、對象是正確創(chuàng)建的(this引用沒有逸出)
↓對于第二問看concurrentMap是線程安全的,用了他的代碼塊可不是線程安全的,來舉個栗子吧↓
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());
});
}
}
- 我們的本意:當?shù)谝粋€線程判斷不存在mapkey的時候去填充這個key的值,之后的其他線程只要從map get出來這個key就可以了。
事實:兩個線程都判斷了mapkey不存在,都各自put了一把到map上。導致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 解決安全問題</b>
對應上面的四個特性取反:
<b>1、去可變狀態(tài)</b>
<b>2、復合操作改成原子操作(記住兩個常見的復合操作)</b>
-- if-then操作(像上面map的例子)
-- 取-讀-寫(像上面value++的例子)
<b>3、控制執(zhí)行順序,即保證有序性</b>
<b>4、若有狀態(tài),則讓狀態(tài)在線程間互相可見</b>
</br>
<b>2.3.1、怎么去可變狀態(tài)</b>
- 不再是成員變量、實例變量了。比如:把他移入到方法內(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
再看一個不安全的
```
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 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();
}
//預期打印是1,看看實際打印多少?
log.info("get first {}", unfinal.getCurrentNumber(1));
});
es.submit(()->{
unfinal.setLastNumber(2);
//預期打印是null,看看實際打印多少?
log.info("get seconde {}", unfinal.getCurrentNumber(1));
});
es.shutdown();
}
}
看一個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、怎么將復合操作變成原子操作</b>
- "讀-操作-寫" 使用Atomic類
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類型的變量(但復合操作變量就有問題了)
3、顯式鎖
4、原子變量(long和double可能不是原子變量,看處理器架構(gòu))
<b> 2.3.4、怎么讓狀態(tài)在線程間可見</b>
- 同步
1、synchronized
2、volatile類型的變量(但復合操作變量就有問題了)
3、顯式鎖
4、原子變量(long和double可能不是原子變量,看處理器架構(gòu))
采用互斥。對于共享資源訪問的<b>代碼片段</b>叫做臨界區(qū)
有多種方法,比如鎖變量(0/1)、嚴格輪換法
概念:管程、信號量、互斥量、同步、阻塞、協(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ù)~
