前言
什么是線程安全?
《Java Concurrency In Partice》的作者 Brian Goetz 對 “線程安全” 有一個比較恰當的定義:“當多個線程訪問一個對象時,如果
不用考慮
這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步
,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果
,那么這個對象就是線程安全的。”。
什么情況下會出現線程安全問題
- 運行結果錯誤:a++多線程下出現消失的請求現象
- 活躍性問題:死鎖、活鎖、饑餓
- 對象發布和初始化的時候的安全問題
1.演示計數不準確(減少)
public class MultiThreadErrorExample implements Runnable{
static MultiThreadErrorExample instance = new MultiThreadErrorExample();
int index;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上結果是" + instance.index);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
}
}
表面上結果是18287
探測出錯位置
public class MultiThreadsError implements Runnable {
static MultiThreadsError instance = new MultiThreadsError();
int index = 0;
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
final boolean[] marked = new boolean[10000000];
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上結果是" + instance.index);
System.out.println("真正運行的次數" + realIndex.get());
System.out.println("錯誤次數" + wrongCount.get());
}
@Override
public void run() {
marked[0] = true;
for (int i = 0; i < 10000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realIndex.incrementAndGet();
synchronized (instance) {
if (marked[index] && marked[index - 1]) {
System.out.println("發生錯誤index" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
}
發生錯誤index11889
表面上結果是19999
真正運行的次數20000
錯誤次數1
當index發生碰撞時,當前marked[index] 由前一個線程設置為true。
2.死鎖
public class MultiThreadError implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
MultiThreadError r1 = new MultiThreadError();
MultiThreadError r2 = new MultiThreadError();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
}
flag = 1
flag = 0
發生死鎖
3.對象發布和初始化的時候的安全問題
3.1 什么是發布
https://www.cnblogs.com/CreateMyself/p/12459141.html
3.2 什么是逸出
1.方法返回一個private對象
/**
* 描述: 發布逸出
*/
public class MultiThreadsError3 {
private Map<String, String> states;
public MultiThreadsError3() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) {
MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
Map<String, String> states = multiThreadsError3.getStates();
System.out.println(states.get("1"));
states.remove("1");
System.out.println(states.get("1"));
}
}
輸出
周一
null
發現 private的states 本意是不允許被外部程序修改,卻被修改了
2.還未完成初始化,構造函數還沒完全執行完畢,就把對象提供給外界 如 :
- 在構造函數中未初始化完畢就this賦值
public class MultiThreadsError4 {
static Point point;
public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
Thread.sleep(10);
// Thread.sleep(105);
if (point != null) {
System.out.println(point);
}
}
}
class Point {
private final int x, y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
MultiThreadsError4.point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return x + "," + y;
}
}
class PointMaker extends Thread {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
main方法中 Thread.sleep(10);時 輸出結果
1,0
main方法中 Thread.sleep(105);時 輸出結果
1,1
- 隱式逸出 --- 注冊監聽事件
public class MultiThreadsError5 {
int count;
public MultiThreadsError5(MySource source) {
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
System.out.println("\n我得到的數字是" + count);
}
});
/***
* 模擬執行業務邏輯 后再賦值count
*/
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
count = 100;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}
}).start();
MultiThreadsError5 multiThreadsError5 = new MultiThreadsError5(mySource);
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("還未初始化完畢");
}
}
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
我得到的數字是0
我們期待是100 結果出現0
- 構造函數中運行線程
/**
* 描述: 構造函數中新建線程
*/
public class MultiThreadsError6 {
private Map<String, String> states;
public MultiThreadsError6() {
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
}).start();
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) throws InterruptedException {
MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
// Thread.sleep(1000);
System.out.println(multiThreadsError6.getStates().get("1"));
}
}
Exception in thread "main" java.lang.NullPointerException
at com.kpioneer.thread.background.MultiThreadsError6.main(MultiThreadsError6.java:33)
接下來 我們Thread.sleep(1000);
再去執行 System.out.println(multiThreadsError6.getStates().get("1"));
結果輸出
周一
因為調用時間不同,結果不同,這樣的程序是不安全的。
3.3 如何解決逸出
- 返回"副本" 解決
逸出行為1
public class MultiThreadsError3 {
private Map<String, String> states;
public MultiThreadsError3() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
public Map<String, String> getStatesImproved() {
return new HashMap<>(states);
}
public static void main(String[] args) {
MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
System.out.println(multiThreadsError3.getStatesImproved().get("1"));
multiThreadsError3.getStatesImproved().remove("1");
System.out.println(multiThreadsError3.getStatesImproved().get("1"));
}
}
周一
周一
- 工廠模式解決
逸出行為2
構造函數未初始化
public class MultiThreadsError7 {
int count;
private final EventListener listener;
private MultiThreadsError7(MySource source) {
listener = new EventListener() {
@Override
public void onEvent(Event e) {
System.out.println("\n我得到的數字是" + count);
}
};
/***
* 模擬執行業務邏輯 后再賦值count
*/
try {
TimeUnit.MILLISECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
count = 100;
}
/**
* 工廠方法
*
* @param source
* @return
*/
public static MultiThreadsError7 getInstance(MySource source) {
MultiThreadsError7 safeListener = new MultiThreadsError7(source);
source.registerListener(safeListener.listener);
return safeListener;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}
}).start();
MultiThreadsError7 instance = MultiThreadsError7.getInstance(mySource);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("還未初始化完畢");
}
}
}
}
還未初始化完畢
注意:在實際開發中我們不會這么明顯的犯錯,但是也可能會被動犯錯,比如調用數據庫連接池(框架會自己開啟線程初始化),如果我們過早調用就可能出錯。
4. 四種需要考慮線程安全的情況
遇到以下四種需要考慮線程安全的情況,需要注意:
- 訪問
共享
的變量或資源, 會有并發風險, 比如對象的屬性, 靜態變量, 共享緩存, 數據庫等
例如此文提到的例子, 用共享變量進行++操作 - 所有
依賴時序
的操作, 即使每一步操作都是線程安全的, 還是存在并發的問題.
read-modify-write: 先讀取, 再修改. check-then-act 先檢查, 再執行.
實際上本質是一樣的, 一個線程先獲取數據, 再進行下一步的操作. 主要可能的問題是, 數據讀取后, 還有可能被其他線程修改. 所以在這種依賴時序的情況下, 可以用synchronized鎖等操作. - 不同的數據之間存在
綁定
關系的時候
例如IP與端口號. 只要修改了IP就要修改端口號, 否則IP也是無效的. 因此遇到這種操作的時候, 要警醒原子的合并操作. 要么全部修改成功, 要么全部修改失敗. - 使用
其他類
的時候, 如果該類的注釋聲明了不是線程安全的, 那么就不應該在多線程的場景中使用, 而應該考慮其對應的線程安全的類,或者對其做一定處理保證線程安全,
例如HashMap就不是線程安全的, 而ConcurrentHashMap則是線程安全的.
悟空