Java線程安全問題的解決方案
前言:
線程安全是指某個方法或某段代碼,在多線程中能夠正確的執(zhí)行,不會出現(xiàn)數(shù)據(jù)不一致或數(shù)據(jù)污染的情況,我們把這樣的程序稱之為線程安全的,反之則為非線程安全的。在 Java 中,
解決線程安全問題有以下 3 種手段:
- 使用線程安全類,比如 AtomicInteger。
- 加鎖排隊執(zhí)行
- 使用 synchronized 加鎖。
- 使用 ReentrantLock 加鎖。
- 使用線程本地變量 ThreadLocal。
接下來我們逐個來看它們的實現(xiàn)。
線程安全問題演示
我們創(chuàng)建一個變量 number 等于 0,之后創(chuàng)建線程 1,執(zhí)行 100 萬次 ++ 操作,同時再創(chuàng)建線程 2 執(zhí)行 100 萬次 -- 操作,等線程 1 和線程 2 都執(zhí)行完之后,打印 number 變量的值,如果打印的結(jié)果為 0,則說明是線程安全的,否則則為非線程安全的,
示例代碼如下:
public class ThreadSafeTest { // 全局變量 private static int number = 0; // 循環(huán)次數(shù)(100W) private static final int COUNT = 1_000_000; public static void main(String[] args) throws InterruptedException { // 線程1:執(zhí)行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { number++; } }); t1.start(); // 線程2:執(zhí)行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { number--; } }); t2.start(); // 等待線程 1 和線程 2,執(zhí)行完,打印 number 最終的結(jié)果 t1.join(); t2.join(); System.out.println("number 最終結(jié)果:" + number); } }
以上程序的執(zhí)行結(jié)果如下圖所示:
從上述執(zhí)行結(jié)果可以看出,number 變量最終的結(jié)果并不是 0,和預(yù)期的正確結(jié)果不相符,這就是多線程中的線程安全問題。
解決線程安全問題
1.原子類AtomicInteger
AtomicInteger 是線程安全的類,使用它可以將 ++ 操作和 -- 操作,變成一個原子性操作,這樣就能解決非線程安全的問題了,
如下代碼所示:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerExample { // 創(chuàng)建 AtomicInteger private static AtomicInteger number = new AtomicInteger(0); // 循環(huán)次數(shù) private static final int COUNT = 1_000_000; public static void main(String[] args) throws InterruptedException { // 線程1:執(zhí)行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // ++ 操作 number.incrementAndGet(); } }); t1.start(); // 線程2:執(zhí)行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // -- 操作 number.decrementAndGet(); } }); t2.start(); // 等待線程 1 和線程 2,執(zhí)行完,打印 number 最終的結(jié)果 t1.join(); t2.join(); System.out.println("最終結(jié)果:" + number.get()); } }
以上程序的執(zhí)行結(jié)果如下圖所示:
2.加鎖排隊執(zhí)行
Java 中有兩種鎖:synchronized 同步鎖和 ReentrantLock 可重入鎖。
2.1 同步鎖synchronized
synchronized 是 JVM 層面實現(xiàn)的自動加鎖和自動釋放鎖的同步鎖,它的實現(xiàn)代碼如下:
public class SynchronizedExample { // 全局變量 private static int number = 0; // 循環(huán)次數(shù)(100W) private static final int COUNT = 1_000_000; public static void main(String[] args) throws InterruptedException { // 線程1:執(zhí)行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // 加鎖排隊執(zhí)行 synchronized (SynchronizedExample.class) { number++; } } }); t1.start(); // 線程2:執(zhí)行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { // 加鎖排隊執(zhí)行 synchronized (SynchronizedExample.class) { number--; } } }); t2.start(); // 等待線程 1 和線程 2,執(zhí)行完,打印 number 最終的結(jié)果 t1.join(); t2.join(); System.out.println("number 最終結(jié)果:" + number); } }
以上程序的執(zhí)行結(jié)果如下圖所示:
2.2 可重入鎖ReentrantLock
ReentrantLock 可重入鎖需要程序員自己加鎖和釋放鎖,它的實現(xiàn)代碼如下:
import java.util.concurrent.locks.ReentrantLock; /** * 使用 ReentrantLock 解決非線程安全問題 */ public class ReentrantLockExample { // 全局變量 private static int number = 0; // 循環(huán)次數(shù)(100W) private static final int COUNT = 1_000_000; // 創(chuàng)建 ReentrantLock private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { // 線程1:執(zhí)行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { lock.lock(); // 手動加鎖 number++; // ++ 操作 lock.unlock(); // 手動釋放鎖 } }); t1.start(); // 線程2:執(zhí)行 100W 次 -- 操作 Thread t2 = new Thread(() -> { for (int i = 0; i < COUNT; i++) { lock.lock(); // 手動加鎖 number--; // -- 操作 lock.unlock(); // 手動釋放鎖 } }); t2.start(); // 等待線程 1 和線程 2,執(zhí)行完,打印 number 最終的結(jié)果 t1.join(); t2.join(); System.out.println("number 最終結(jié)果:" + number); } }
以上程序的執(zhí)行結(jié)果如下圖所示:
3.線程本地變量ThreadLocal
使用 ThreadLocal 線程本地變量也可以解決線程安全問題,它是給每個線程獨自創(chuàng)建了一份屬于自己的私有變量,不同的線程操作的是不同的變量,所以也不會存在非線程安全的問題,它的實現(xiàn)代碼如下:
public class ThreadSafeExample { // 創(chuàng)建 ThreadLocal(設(shè)置每個線程中的初始值為 0) private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0); // 全局變量 private static int number = 0; // 循環(huán)次數(shù)(100W) private static final int COUNT = 1_000_000; public static void main(String[] args) throws InterruptedException { // 線程1:執(zhí)行 100W 次 ++ 操作 Thread t1 = new Thread(() -> { try { for (int i = 0; i < COUNT; i++) { // ++ 操作 threadLocal.set(threadLocal.get() + 1); } // 將 ThreadLocal 中的值進行累加 number += threadLocal.get(); } finally { threadLocal.remove(); // 清除資源,防止內(nèi)存溢出 } }); t1.start(); // 線程2:執(zhí)行 100W 次 -- 操作 Thread t2 = new Thread(() -> { try { for (int i = 0; i < COUNT; i++) { // -- 操作 threadLocal.set(threadLocal.get() - 1); } // 將 ThreadLocal 中的值進行累加 number += threadLocal.get(); } finally { threadLocal.remove(); // 清除資源,防止內(nèi)存溢出 } }); t2.start(); // 等待線程 1 和線程 2,執(zhí)行完,打印 number 最終的結(jié)果 t1.join(); t2.join(); System.out.println("最終結(jié)果:" + number); } }
以上程序的執(zhí)行結(jié)果如下圖所示:
總結(jié)
在 Java 中,解決線程安全問題的手段有 3 種:1.使用線程安全的類,如 AtomicInteger 類;2.使用鎖 synchronized 或 ReentrantLock 加鎖排隊執(zhí)行;3.使用線程本地變量 ThreadLocal 來處理。
到此這篇關(guān)于Java線程安全問題的解決方案的文章就介紹到這了,更多相關(guān)Java線程安全內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合Spring Data Elasticsearch的過程詳解
這篇文章主要介紹了SpringBoot整合Spring Data Elasticsearch的過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-09-09Java調(diào)用JavaScript實現(xiàn)字符串計算器代碼示例
這篇文章主要介紹了Java調(diào)用JavaScript實現(xiàn)字符串計算器代碼示例,具有一定參考價值,需要的朋友可以了解下。2017-12-12webuploader+springmvc實現(xiàn)圖片上傳功能
這篇文章主要為大家詳細介紹了webuploader+springmvc實現(xiàn)圖片上傳功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-09-09通過實例了解Java 8創(chuàng)建Stream流的5種方法
這篇文章主要介紹了通過實例了解Java 8創(chuàng)建Stream流的5種方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12SpringBoot日志框架之Log4j2快速入門與參數(shù)詳解
本文介紹了SpringBoot日志框架log4j2的基本使用和配置方法,包括將日志輸出到控制臺、文件、Elasticsearch和Kafka,多個輸出目的地的配置,異步日志記錄器的使用以及l(fā)og4j2.xml配置文件的詳細語法和參數(shù)含義,需要的朋友可以參考下2023-05-05