多線程

線程安全性

  • 多線程時,使用同步機制,對于可變共享對象的訪問/修改,需要保證數據的正常。- 注:訪問也是需要同步的(參考內存可見性)。
  • 線程的局部變量在獨立的線程棧上,是線程安全的。

線程原則

原子性

  • 多個關聯的原子變量/代碼塊之間,也需要確保原子性修改。

原子類AtomicXX

加內置鎖

內存可見性

  • 讀為什么也需要同步的原因:保證線程間的修改即時立刻可見。(與寫,同一個同步實現)
  • 指令的重排列,無法保證執行的時序。
  • 鎖前后,確保工作內存&主存的及時刷新。

volatile-不重排

  • 僅確保修改后,其他線程,立刻可見;不能保證修改時原子的。鎖可以可見性+原子性。
  • 當旦僅當滿足以下所有條件時,才應該使用 volatile 變量:
    1.對變量的寫人操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
    2.該變量不會與其他狀態變量一起納人不變性條件中。
    3.在訪問變量時不需要加鎖

不變性

  • 不可變對象是必然線程安全的。

不安全問題

1.競態條件(Race Condition)----原子性

  • 并發時,由于不恰當的執行時序,導致不正確的數據。e.g.a++;可變的共享變量+并發,非同步原子
    的情況下,根據多線程的調度,數據不可靠。

2.數據競爭(Data Race)

  • 未同步,導致讀寫線程的數據時不正確的

3.活躍性問題:操作無法正常執行下去。e.g.死鎖/饑餓/活鎖

4.發布和逸出

  • 發布:使得對象在當前作用域之外被使用。
  • 逸出:當不該發布的對象被發布出去。
    • this引用逸出
  • 在對象未構造完成之前,就發布該對象,破壞線程的安全性。
  • 構造函數中,發布其他對象×。super.register(new listener()),內部實例持有對象的隱含引用
  • 構造函數中,構造一個線程√,此時,線程持有當前對象this,啟動一個線程×。
  • 構造函數中,調用可被重寫的方法(final private)

5.線程封閉

  • 對象封閉在線程中,不被其他共享。

線程棧/局部變量+不逸出

ThreadLocal(全局變量)

同步機制

原子類AtomicXX

volatile

線程安全的數據結構

  • Vetor

跨線程的回調函數是如何實現的?

  • 重點在于進程間通信,本質上是如何將與線程相關的變量或者對象傳遞給別的線程,從而實現交互。
  • 對象是存在多線程共享的堆中的,不是局部變量的線程棧中,所以可以獲取。

CountDownLatch : 一個線程(或者多個), 等待另外N個線程完成某個事情之后才能執行。
CyclicBarrier : N個線程相互等待,任何一個線程完成之前,所有的線程都必須等待,類似有福同享,有難同當

因為在泛型代碼內部,無法獲取任何有關泛型參數類型的任何信息!,Java的泛型就是使用擦除來實現的
那如何獲取參數類型?——類型標簽Class< T>

typetoken獲取類型轉換?

全研成的單例內存返回(全局,Rxjava怎么會返回到第二次的?)
Rxjava如何實現線程切換,防止線程泄露?

勝瀾的命令異步回調多線程內存泄露(全局變量緩存局部變量,且沒有清空操作造成內存泄露https://blog.csdn.net/weixin_39629679/article/details/114221004

多線程各場景及解決方案


/**
 * scenario:寫寫未互斥:兩個線程同時寫同一個變量,
 * problem: 寫非原子操作,數據異常
 * solution:寫寫互斥
 */
public class MultiWriteV1 {
    private int num = 0;

    private void read() {
        System.out.println(num);
    }

    private void write(int change) {
        num += change;
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
        }).start();

        // 睡眠一秒保證線程執行完成 todo
        // 1s一定OK?
        Thread.sleep(1000);
        // 讀取結果
        read();
    }
}


/**
 * scenario:寫寫互斥:兩個線程同時寫同一個變量,
 * problem: 寫寫互斥
 * solution:寫線程,對象鎖,
 */
public class MultiWriteV2 {
    private int num = 0;
    private final Object lock = new Object();

    private void read() {
        System.out.println(num);
    }

    private void write(int change) {
        synchronized (lock) {
            num += change;
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
        }).start();


        // 睡眠一秒保證線程執行完成 todo
        // 1s一定OK?
        Thread.sleep(1000);
        // 讀取結果
        read();
    }
}

/**
 * scenario:邊讀邊寫
 * solution:寫寫互斥,讀寫不互斥,讀讀不互斥
 * problem: 讀到的不是最新主內存中的數據
 */
public class MultiReadWriteV1 {
    private int num = 0;
    private final Object lock = new Object();

    private void read() {
        System.out.println("read " + num);
    }

    private void write(int change) {
        synchronized (lock) {
            num += change;
            System.out.println("write " + num);
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();


    }
}


/**
 * scenario:邊讀邊寫
 * solution:寫寫互斥,讀寫互斥,讀讀互斥:讀寫互斥都加同一把鎖
 * problem: 讀到的是最新主內存中的數據,滿足邊讀邊寫,但效率低,所有操作都互斥
 */
public class MultiReadWriteV2 {
    private int num = 0;
    private final Object lock = new Object();

    private void read() {
        synchronized (lock) {
            //所以我要給 read 中的代碼也加上判斷,它也要拿到鑰匙后才能讀取,
            // 這樣就能保證讀取時不會有寫操作,寫的時候也沒有讀取操作了
            //更常見的需求是寫入全部完成后,再去讀取值。
            System.out.println("read " + num);
        }
    }

    private void write(int change) {
        synchronized (lock) {
            num += change;
            System.out.println("write " + num);
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();


    }
}

/**
 * scenario:邊讀邊寫
 * solution:寫寫互斥,讀寫不互斥,讀讀不互斥,volatile
 * problem: 無效 讀到的不是最新的
 */
public class MultiReadWriteV3 {
    private volatile int num = 0;
    private final Object lock = new Object();

    private void read() {
        System.out.println("read " + num);
    }

    private void write(int change) {
        synchronized (lock) {
            num += change;
            System.out.println("write " + num);
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
        }).start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();
    }
    
}

/**
 * scenario:寫完再讀
 * solution:寫寫互斥,讀寫互斥,讀讀互斥,加一個寫線程是否結束的標志位,讀線程循環檢查標準位
 * problem: 一旦讀在寫完之前取拿到了鎖,死循環在讀線程while,一直打印waiting,寫線程無法拿到鎖,無法修改度的條件標志位;所有都互斥,效率低
 */
public class MultiReadWriteV4 {
    private int num = 0;
    private final Object lock = new Object();
    private boolean isWriteFinished1 = false;
    private boolean isWriteFinished2 = false;

    private void read() {
        synchronized (lock) {
            //所以我要給 read 中的代碼也加上判斷,它也要拿到鑰匙后才能讀取,
            // 這樣就能保證讀取時不會有寫操作,寫的時候也沒有讀取操作了
            //更常見的需求是寫入全部完成后,再去讀取值。

            //這不能實現10000次讀取
//            if (isWriteFinished1 && isWriteFinished2) {
//                System.out.println("read " + num);
//            } else {
//                System.out.println("writing");
//            }

            while (!isWriteFinished1 || !isWriteFinished2) {
                System.out.println("writing");
            }
            System.out.println("read " + num);
        }
    }

    private void write(int change) {
        synchronized (lock) {
            num += change;
            System.out.println("write " + num);
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
            isWriteFinished1 = true;
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
            isWriteFinished2 = true;
        }).start();


        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();
    }
}
/**
 * scenario:寫完再讀
 * solution:寫寫互斥,讀寫互斥,讀讀互斥,不阻塞,等待/喚醒機制(synchronized (lock) {lock.wait、lock.notify}))
 * 寫入操作不受限制;如果寫入還沒有完成,read 方法先進入等待狀態。write 方法寫入完成后,通知 read 開始讀取
 * problem: 讀讀互斥,效率低
 */
public class MultiReadWriteV5 {
    private int num = 0;
    private final Object lock = new Object();
    private boolean isWriteFinished1 = false;
    private boolean isWriteFinished2 = false;

    private void read() {
        synchronized (lock) {
            while (!isWriteFinished1 || !isWriteFinished2) {
                // 等待,并且不要阻塞寫入
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 只會在上述的lock被notify,往下走后,打印兩次
                System.out.println("writing");
            }
            System.out.println("read " + num);
        }
    }

    private void write(int change) {
        synchronized (lock) {
            num += change;
            System.out.println("write " + num);
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
            isWriteFinished1 = true;
            //寫線程寫完后,喚醒讀取線程繼續讀取。
            synchronized (lock) {
                lock.notify();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
            isWriteFinished2 = true;
            //寫線程寫完后,喚醒讀取線程繼續讀取。
            synchronized (lock) {
                lock.notify();
            }
        }).start();


        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();
    }
}
/**
 * scenario:寫完再讀
 * solution:寫寫互斥,讀寫互斥,不阻塞,等待/喚醒機制(ReentrantLock+Condition 類替代synchronized (lock) {lock.wait、lock.notify}))
 * ReentrantLock可以設置嘗試獲取鎖的等待時間+condition可以設置自喚醒時間
 * 寫入操作不受限制;如果寫入還沒有完成,read 方法先進入等待狀態。write 方法寫入完成后,通知 read 開始讀取
 * problem:讀讀互斥,效率低
 */
public class MultiReadWriteV6 {
    private int num = 0;

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    private boolean isWriteFinished1 = false;
    private boolean isWriteFinished2 = false;

    private void read() {
        lock.lock();

        while (!isWriteFinished1 || !isWriteFinished2) {
            // 等待,并且不要阻塞寫入
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 只會在上述的lock被notify,往下走后,打印兩次
            System.out.println("writing");

//            if (condition.await(1, TimeUnit.SECOND)) {
//                // 1 秒內被 signal 喚醒
//            } else {
//                // 1 秒內沒有被喚醒,自己醒來
//            }


        }
        System.out.println("read " + num);

        lock.unlock();
    }

    private void write(int change) {
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) {
                num += change;
                System.out.println("write " + num);

                lock.unlock();
            } else {
                System.out.println("1 秒內沒有獲取到鎖,不再等待。不執行");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
            isWriteFinished1 = true;

            //寫線程寫完后,喚醒讀取線程繼續讀取。
            // 寫入完成,喚醒讀取線程,await/signal 操作必須在 lock 時執行。
            lock.lock();
            condition.signal();
            lock.unlock();

        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
            isWriteFinished2 = true;
            //寫線程寫完后,喚醒讀取線程繼續讀取。
            // 寫入完成,喚醒讀取線程,await/signal 操作必須在 lock 時執行。
            lock.lock();
            condition.signal();
            lock.unlock();
        }).start();


        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();
    }
}
/**
 * scenario:寫完再讀
 * solution:寫寫互斥,讀寫互斥,讀讀互斥(ReadWriteLock優化讀讀互斥,本身也支持不阻塞的喚醒通知機制)
 * problem:ReadWriteLock 會導致寫線程必須等待讀線程完成后才能寫(悲觀鎖)
 */
public class MultiReadWriteV7 {
    private int num = 0;

    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock writeLock = lock.writeLock();
    private final Lock readLock = lock.readLock();
    private final Condition condition = writeLock.newCondition();

    private boolean isWriteFinished1 = false;
    private boolean isWriteFinished2 = false;

    private void read() {
        readLock.lock();

        while (!isWriteFinished1 || !isWriteFinished2) {
            // 等待,并且不要阻塞寫入
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 只會在上述的lock被notify,往下走后,打印兩次
            System.out.println("writing");

//            if (condition.await(1, TimeUnit.SECOND)) {
//                // 1 秒內被 signal 喚醒
//            } else {
//                // 1 秒內沒有被喚醒,自己醒來
//            }


        }
        System.out.println("read " + num);

        readLock.unlock();
    }

    private void write(int change) {
        try {
            if (writeLock.tryLock(1, TimeUnit.SECONDS)) {
                num += change;
                System.out.println("write " + num);

                writeLock.unlock();
            } else {
                System.out.println("1 秒內沒有獲取到鎖,不再等待。不執行");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
            isWriteFinished1 = true;

            //寫線程寫完后,喚醒讀取線程繼續讀取。
            // 寫入完成,喚醒讀取線程,await/signal 操作必須在 lock 時執行。
            writeLock.lock();
            condition.signal();
            writeLock.unlock();

        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
            isWriteFinished2 = true;
            //寫線程寫完后,喚醒讀取線程繼續讀取。
            // 寫入完成,喚醒讀取線程,await/signal 操作必須在 lock 時執行。
            writeLock.lock();
            condition.signal();
            writeLock.unlock();
        }).start();


        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();
    }
}
/**
 * scenario:寫完再讀
 * solution:寫寫互斥,讀寫不互斥( StampedLock樂觀鎖,讀的過程中也允許寫。通過版本對比判斷讀的過程中是否有寫入發生+回退悲觀鎖的方案,確保能讀到主內存中最新的值)
 * problem:
 */
public class MultiReadWriteV8 {
    private int num = 0;
    private final StampedLock lock = new StampedLock();

    private void read() {
        long stamp = lock.tryOptimisticRead();
        int readNumber = num;
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            System.out.println("樂觀讀取到的 number " + readNumber + " 有誤,換用悲觀鎖重新讀取:number = " + num);
            lock.unlockRead(stamp);
        }
    }

    private void write(int change) {
        long stamp = lock.writeLock();
        num += change;
        System.out.println("write " + num);
        lock.unlockWrite(stamp);
    }

    public void readAndWrite() throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(1);
            }
            System.out.println("+ 10000 finish");
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                write(-1);
            }
            System.out.println("- 10000 finish");
        }).start();


        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                read();
            }
            System.out.println("read finish");
        }).start();
    }
}

ReentrantLock
公平鎖&非公平鎖:搶鎖直接排隊還是先去搶失敗再排隊,ReentrantLock默認非公平
偏向鎖、輕量鎖等
樂觀鎖(無鎖算法CAS,先干起來,寫的時候檢查是否被其他線程修改,修改的話就;AtomicInteger Java原子類中的遞增操作就通過Compare And Swap(比較與交換自旋實現)&悲觀鎖(鎖多寫場景,,synchronized關鍵字和Lock的實現類):鎖還是不鎖同步資源
自旋鎖&適應性自旋:搶鎖失敗了,阻塞這個線程還是讓他自旋:多個處理器,CPU切換+恢復線程線程的消耗>自旋*N次的消耗。因為切換線程阻塞掛起喚醒等需要切CPU,保存/恢復現場,消耗大。如果同步資源獲取后,線程處理邏輯簡單,其實是比阻塞線程開銷少。如果有多個處理器,可以讓后來的線程自及忙一會,等會再來檢查同步資源,到時候直接獲取,避免線程切換。
可重入鎖:同一線程能否多次獲取同一把鎖,ReentrantLock和synchronized都是重入鎖
共享鎖(大家都可讀)&排他/獨享鎖(我自己可以讀可以寫):多個線程能否共享一把鎖?那還鎖啥鎖?
https://tech.meituan.com/2018/11/15/java-lock.html
https://segmentfault.com/a/1190000023735772

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容