Java并發編程 線程安全

前言

什么是線程安全?

《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. 四種需要考慮線程安全的情況

遇到以下四種需要考慮線程安全的情況,需要注意:

  1. 訪問共享的變量或資源, 會有并發風險, 比如對象的屬性, 靜態變量, 共享緩存, 數據庫等
    例如此文提到的例子, 用共享變量進行++操作
  2. 所有依賴時序的操作, 即使每一步操作都是線程安全的, 還是存在并發的問題.
    read-modify-write: 先讀取, 再修改. check-then-act 先檢查, 再執行.
    實際上本質是一樣的, 一個線程先獲取數據, 再進行下一步的操作. 主要可能的問題是, 數據讀取后, 還有可能被其他線程修改. 所以在這種依賴時序的情況下, 可以用synchronized鎖等操作.
  3. 不同的數據之間存在綁定關系的時候
    例如IP與端口號. 只要修改了IP就要修改端口號, 否則IP也是無效的. 因此遇到這種操作的時候, 要警醒原子的合并操作. 要么全部修改成功, 要么全部修改失敗.
  4. 使用其他類的時候, 如果該類的注釋聲明了不是線程安全的, 那么就不應該在多線程的場景中使用, 而應該考慮其對應的線程安全的類,或者對其做一定處理保證線程安全,
    例如HashMap就不是線程安全的, 而ConcurrentHashMap則是線程安全的.
    悟空
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容