出現線程安全問題的主要原因:
多線程搶占式執行
導致線程安全問題的第一大因素就是多線程搶占式執行,想象一下,如果是單線程執行,或者是多線程有序執行,那就不會出現混亂的情況了,不出現混亂的情況,自然就不會出現非線程安全的問題了。
多個線程同時操作一個變量
如果是多線程同時修改不同的變量(每個線程只修改自己的變量),也是不會出現非線程安全的問題了,比如以下代碼,線程 1 修改 number1 變量,而線程 2 修改 number2 變量,最終兩個線程執行完之后的結果如下:
public class ThreadSafe { // 全局變量 private static int number = 0; // 循環次數(100W) private static final int COUNT = 1_000_000; // 線程 1 操作的變量 number1 private static int number1 = 0; // 線程 2 操作的變量 number2 private static int number2 = 0; public static void main(String[] args) throws InterruptedException { // 線程1:執行 100W 次 number+1 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { number1++; } }); t1.start(); // 線程2:執行 100W 次 number-1 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { number2--; } }); t2.start(); // 等待線程 1 和線程 2,執行完,打印 number 最終的結果 t1.join(); t2.join(); number = number1 + number2; System.out.println("number=number1+number2 最終結果:" + number); }
以上程序的執行結果如下圖所示:
非原子性操作
原子性操作是指操作不能再被分隔就叫原子性操作。比如人類吸氣或者是呼氣這個動作,它是一瞬間一次性完成的,你不可能先吸一半(氣),停下來玩會手機,再吸一半(氣),這種操作就是原子性操作。而非原子性操作是我現在要去睡覺,但睡覺之前要先上床,再拉被子,再躺下、再入睡等一系列的操作綜合在一起組成的,這就是非原子性操作。非原子性操作是有可以被分隔和打斷的,比如要上床之前,發現時間還在,先刷個劇、刷會手機、再玩會游戲,甚至是再吃點小燒烤等等,所以非原子性操作有很多不確定性,而這些不確定性就會造成線程安全問題問題。像 i++ 和 i– 這種操作就是非原子的,它在 +1 或 -1 之前,先要查詢原變量的值,并不是一次性完成的,所以就會導致線程安全問題。
內存不可見
所謂內存不可見性,就是線程對某個共享變量在線程自己的緩沖中存在副本的時候對主內存中共享變量的值是不可見的,看不見主存中的值。所以就會導致線程安全問題。
指令重排序(編譯器優化)
指令重排序是指 Java 程序為了提高程序的執行速度,所以會對一下操作進行合并和優化的操作。比如說,張三要去圖書館還書,舍友又讓張三幫忙借書,那么程序的執行思維是,張三先去圖書館把自己的書還了,再去一趟圖書館幫舍友把書借回來。而指令重排序之后,把兩次執行合并了,張三帶著自己的書去圖書館把書先還了,再幫舍友把書借出來,整個流程就執行完了,這是正常情況下的指令重排序的好處。但是指令重排序也有“副作用”,而“副作用”是發生在多線程執行中的,還是以張三借書和幫舍友還書為例,如果張三是一件事做完再做另一件事是沒有問題的(也就是單線程執行是沒有問題的),但如果是多線程執行,就是兩件事由多個人混合著做,比如張三在圖書館遇到了自己的多個同學,于是就把任務分派給多個人一起執行,有人借了幾本書、有人借了還了幾本書、有人再借了幾本書、有人再借了還了幾本書,執行的很混亂沒有明確的目標,到最后悲劇就發生了,這就是在指令重排序帶來的線程安全問題。
在 Java 中,解決線程安全問題的手段有 3 種:
使用線程安全的類,如 AtomicInteger 類。
AtomicInteger 是線程安全的類,使用它可以將 ++ 操作和 – 操作,變成一個原子性操作,這樣就能解決非線程安全的問題了。
使用鎖 synchronized 或 ReentrantLock 加鎖排隊執行。
同步鎖synchronized:synchronized 是 JVM 層面實現的自動加鎖和自動釋放鎖的同步鎖。
可重入鎖ReentrantLock:ReentrantLock 可重入鎖需要程序員自己加鎖和釋放鎖。
使用線程本地變量 ThreadLocal 來處理。
使用 ThreadLocal 線程本地變量也可以解決線程安全問題,它是給每個線程獨自創建了一份屬于自己的私有變量,不同的線程操作的是不同的變量,所以也不會存在非線程安全的問題。
回答所涉及的環境:聯想天逸510S、Windows 10。
出現線程安全問題的主要原因:
多線程搶占式執行
導致線程安全問題的第一大因素就是多線程搶占式執行,想象一下,如果是單線程執行,或者是多線程有序執行,那就不會出現混亂的情況了,不出現混亂的情況,自然就不會出現非線程安全的問題了。
多個線程同時操作一個變量
如果是多線程同時修改不同的變量(每個線程只修改自己的變量),也是不會出現非線程安全的問題了,比如以下代碼,線程 1 修改 number1 變量,而線程 2 修改 number2 變量,最終兩個線程執行完之后的結果如下:
以上程序的執行結果如下圖所示:
非原子性操作
原子性操作是指操作不能再被分隔就叫原子性操作。比如人類吸氣或者是呼氣這個動作,它是一瞬間一次性完成的,你不可能先吸一半(氣),停下來玩會手機,再吸一半(氣),這種操作就是原子性操作。而非原子性操作是我現在要去睡覺,但睡覺之前要先上床,再拉被子,再躺下、再入睡等一系列的操作綜合在一起組成的,這就是非原子性操作。非原子性操作是有可以被分隔和打斷的,比如要上床之前,發現時間還在,先刷個劇、刷會手機、再玩會游戲,甚至是再吃點小燒烤等等,所以非原子性操作有很多不確定性,而這些不確定性就會造成線程安全問題問題。像 i++ 和 i– 這種操作就是非原子的,它在 +1 或 -1 之前,先要查詢原變量的值,并不是一次性完成的,所以就會導致線程安全問題。
內存不可見
所謂內存不可見性,就是線程對某個共享變量在線程自己的緩沖中存在副本的時候對主內存中共享變量的值是不可見的,看不見主存中的值。所以就會導致線程安全問題。
指令重排序(編譯器優化)
指令重排序是指 Java 程序為了提高程序的執行速度,所以會對一下操作進行合并和優化的操作。比如說,張三要去圖書館還書,舍友又讓張三幫忙借書,那么程序的執行思維是,張三先去圖書館把自己的書還了,再去一趟圖書館幫舍友把書借回來。而指令重排序之后,把兩次執行合并了,張三帶著自己的書去圖書館把書先還了,再幫舍友把書借出來,整個流程就執行完了,這是正常情況下的指令重排序的好處。但是指令重排序也有“副作用”,而“副作用”是發生在多線程執行中的,還是以張三借書和幫舍友還書為例,如果張三是一件事做完再做另一件事是沒有問題的(也就是單線程執行是沒有問題的),但如果是多線程執行,就是兩件事由多個人混合著做,比如張三在圖書館遇到了自己的多個同學,于是就把任務分派給多個人一起執行,有人借了幾本書、有人借了還了幾本書、有人再借了幾本書、有人再借了還了幾本書,執行的很混亂沒有明確的目標,到最后悲劇就發生了,這就是在指令重排序帶來的線程安全問題。
在 Java 中,解決線程安全問題的手段有 3 種:
使用線程安全的類,如 AtomicInteger 類。
AtomicInteger 是線程安全的類,使用它可以將 ++ 操作和 – 操作,變成一個原子性操作,這樣就能解決非線程安全的問題了。
使用鎖 synchronized 或 ReentrantLock 加鎖排隊執行。
同步鎖synchronized:synchronized 是 JVM 層面實現的自動加鎖和自動釋放鎖的同步鎖。
可重入鎖ReentrantLock:ReentrantLock 可重入鎖需要程序員自己加鎖和釋放鎖。
使用線程本地變量 ThreadLocal 來處理。
使用 ThreadLocal 線程本地變量也可以解決線程安全問題,它是給每個線程獨自創建了一份屬于自己的私有變量,不同的線程操作的是不同的變量,所以也不會存在非線程安全的問題。
回答所涉及的環境:聯想天逸510S、Windows 10。