第十章、核心8:線程安全-多線程會導致的問題

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併發核心知識體系精講》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章