1、線程安全
1.1 什麼是線程安全
- 當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的————《Java併發編程實戰》
1.2 線程不安全:get同時set
- 全都線程安全?:運行速度、設計成本、trade off
- 完全不用於多線程的代碼:不過度設計
1.3 什麼情況下會出現線程安全問題,怎麼避免?
1.3.1 運行結果錯誤:a++多線程下出現消失的請求現象
/**
* MultiThreadsError
*
* @author venlenter
* @Description: 普通a++會導致count疊加錯誤,以下程序已優化處理
* @since unknown, 2020-05-07
*/
public class MultiThreadsError3 implements Runnable {
int index = 0;
final boolean[] marked = new boolean[10000000];
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static MultiThreadsError3 instance = new MultiThreadsError3();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
@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
synchronized (instance) {
if (marked[index] && marked[index - 1]) {
System.out.println("發生錯誤:" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
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());
}
}
//輸出結果
表面上結果是:20000
真正運行的次數:20000
錯誤次數:0
1.3.2 活躍性問題:死鎖、活鎖、飢餓
/**
* MultiThreadError
*
* @author venlenter
* @Description: 第二章線程安全問題,演示死鎖
* @since unknown, 2020-05-11
*/
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
//程序一直等待不結束
1.3.3 對象發佈和初始化時的安全問題
什麼是發佈?
- public、return都算是獲得對象,發佈了該對象出去
什麼是溢出?
- 1.方法返回一個private對象(定義了private對象的getXX()方法)(private的本意是不讓外部訪問)
- 2.還未完成初始化(構造函數沒完全執行完畢)就把對象提供給外界,比如
(1)在構造函數中未初始化完畢就把this賦值出去了
/**
* MultiThreadsError4
*
* @author venlenter
* @Description: 初始化未完畢,就this賦值
* @since unknown, 2020-05-12
*/
public class MultiThreadsError4 {
static Point point;
public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
Thread.sleep(105);
if (point != null) {
System.out.println(point);
}
}
}
class Point {
private final int x, y;
Point(int x, int y) throws InterruptedException {
this.x = x;
//這裏先行給point賦值this,此時外部拿到point對象只有x,沒有y的值
MultiThreadsError4.point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
class PointMaker extends Thread {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//輸出結果
//可能
Point{x=1, y=1}
//也可能
Point{x=1, y=0}
(2)隱式逸出————註冊監聽事件
/**
* MultiThreadsError5
*
* @author venlenter
* @Description: 觀察者模式
* @since unknown, 2020-05-12
*/
public class MultiThreadsError5 {
int count;
public MultiThreadsError5(MySource source) {
source.registerListener(new EventListener() {
@Override
//這裏EventListener是一個匿名內部類,實際上也用了count這個外部引用變量,當count未初始化完成,拿到的值就還是0
public void onEvent(Event e) {
System.out.println("\n我得到的數字是:" + count);
}
});
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
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 {
}
}
//輸出結果
012345678910...
我得到的數字是:0
...
(3)構造函數中運行線程
/**
* MultiThreadsError6
*
* @author venlenter
* @Description: 構造函數中新建線程
* @since unknown, 2020-05-14
*/
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) {
MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
//在構造函數中states還未初始化完成,就get
System.out.println(multiThreadsError6.getStates().get("1"));
}
}
//輸出結果
Exception in thread "main" java.lang.NullPointerException
at ConcurrenceFolder.mooc.threadConcurrencyCore.background.MultiThreadsError6.main(MultiThreadsError6.java:34)
1.4 如何解決逸出
- 返回“副本”(返回對象的deepCopy)--對應解決(1.方法返回了private對象)
- 工廠模式--對應解決(2.還沒初始化就吧對象提供給外界)
2、各種需要考慮線程安全的情況
- 訪問共享的變量或資源,會有併發風險,比如對象的屬性、靜態變量、共享緩存、數據庫等
- 所有依賴時序的操作,即使每一步操作都是線程安全的,還是存在併發問題:read-modify-write、check-then-act(a++問題)
- 不同的數據之間存在捆綁關係的時候(原子操作:要麼全部執行,要麼全部不執行)
- 我們使用其他類的時候,如果對方沒有聲明自己是線程安全的,則我們需要做相應的處理邏輯
3、雙刃劍:多線程會導致的問題
3.1 性能問題有哪些體現、什麼是性能問題
- 服務響應慢、吞吐量低、資源消耗(例如內存)過高等
- 雖然不是結果錯誤,但仍然危害巨大
- 引入多線程不能本末倒置
3.2 爲什麼多線程會帶來性能問題
(1)調度:上下文切換
- 什麼是上下文?:線程A執行到某個地方,然後要切換到另一個線程B的時候,CPU會保存當前的線程A在CPU中的狀態(上下文)到內存中的某處,等線程B執行完成後,回到線程A需要還原線程A之前保存的狀態(這種切換需要耗時)
- 緩存開銷(考慮緩存失效):多線程切換,從線程A切換到線程B,線程A的緩存就失效了,需要重新加載
- 何時會導致密集的上下文切換:搶鎖、IO
(2)協作:內存同步
- 爲了數據的正確性,同步手段往往會使用禁止編譯器優化、使CPU內的緩存失效(java內存模型)
4、常見面試問題
(1)你知道有哪些線程不安全的情況
- 運行結果錯誤:a++多線程下出現消失的請求現象
- 活躍性問題:死鎖、活鎖、飢餓
- 對象發佈和初始化時的安全問題
(2)平時哪些情況下需要額外注意線程安全問題?
(3)什麼是多線程的上下文切換?
筆記來源:慕課網悟空老師視頻《Java併發核心知識體系精講》