後端開發必知的11個線程安全小技巧

来源:https://www.cnblogs.com/88223100/archive/2023/07/31/11-Thread-safety-tips-for-back-end-development.html
-Advertisement-
Play Games

對於從事後端開發的同學來說,線程安全問題是我們每天都需要考慮的問題。 線程安全問題通俗地講主要是在多線程的環境下,不同線程同時讀和寫公共資源(臨界資源)導致的數據異常問題。 比如:變數a=0,線程1給該變數+1,線程2也給該變數+1。此時,線程3獲取a的值有可能不是2,而是1。線程3這不就獲取了錯誤 ...


 

對於從事後端開發的同學來說,線程安全問題是我們每天都需要考慮的問題。

 

線程安全問題通俗地講主要是在多線程的環境下,不同線程同時讀和寫公共資源(臨界資源)導致的數據異常問題。

 

比如:變數a=0,線程1給該變數+1,線程2也給該變數+1。此時,線程3獲取a的值有可能不是2,而是1。線程3這不就獲取了錯誤的數據?

 

線程安全問題會直接導致數據異常,從而影響業務功能的正常使用,所以這個問題還是非常嚴重的。

 

那麼,如何解決線程安全問題呢?

 

今天跟大家一起聊聊,保證線程安全的11個小技巧,希望對你有所幫助。

 

一、無狀態

我們都知道只有多個線程訪問公共資源的時候,才可能出現數據安全問題,那麼如果我們沒有公共資源,是不是就沒有這個問題呢?

 

例如:

public class NoStatusService {


    public void add(String status) {
        System.out.println("add status:" + status);
    }


    public void update(String status) {
        System.out.println("update status:" + status);
    }
}

 

這個例子中NoStatusService沒有定義公共資源,換句話說是無狀態的。

 

這種場景中,NoStatusService類肯定是線程安全的。

 

二、不可變

如果多個線程訪問的公共資源是不可變的,也不會出現數據的安全性問題。

 

例如:


public class NoChangeService {
    public static final String DEFAULT_NAME = "abc";


    public void add(String status) {
        System.out.println(DEFAULT_NAME);
    }
}

 

DEFAULT_NAME被定義成了static final的常量,在多線程中環境中不會被修改,所以這種情況也不會出現線程安全問題。

 

三、無修改許可權

有時候,我們定義了公共資源,但是該資源只暴露了讀取的許可權,沒有暴露修改的許可權,這樣也是線程安全的。

 

例如:

public class SafePublishService {
    private String name;


    public String getName() {
        return name;
    }


    public void add(String status) {
        System.out.println("add status:" + status);
    }
}

 

這個例子中,沒有對外暴露修改name欄位的入口,所以不存線上程安全問題。

 

四、synchronized

使用JDK內部提供的同步機制,這也是使用比較多的手段,分為同步方法和同步代碼塊。

 

我們優先使用同步代碼塊,因為同步方法的粒度是整個方法,範圍太大,相對來說,更消耗代碼的性能。

 

其實,每個對象內部都有一把鎖,只有搶到那把鎖的線程,才被允許進入對應的代碼塊執行相應的代碼。

 

當代碼塊執行完之後,JVM底層會自動釋放那把鎖。

 

例如:

public class SyncService {
    private int age = 1;
    private Object object = new Object();


    //同步方法
    public synchronized void add(int i) {
        age = age + i;        
        System.out.println("age:" + age);
    }


    
    public void update(int i) {
        //同步代碼塊,對象鎖
        synchronized (object) {
            age = age + i;                     
            System.out.println("age:" + age);
        }    
     }
     
     public void update(int i) {
        //同步代碼塊,類鎖
        synchronized (SyncService.class) {
            age = age + i;                     
            System.out.println("age:" + age);
        }    
     }
}

 

五、Lock

除了使用synchronized關鍵字實現同步功能之外,JDK還提供了Lock介面這種顯示鎖的方式。

 

通常我們會使用Lock介面的實現類:ReentrantLock,它包含了公平鎖、非公平鎖、可重入鎖、讀寫鎖等更多更強大的功能。

 

例如:

public class LockService {
    private ReentrantLock reentrantLock = new ReentrantLock();
    public int age = 1;
    
    public void add(int i) {
        try {
            reentrantLock.lock();
            age = age + i;           
            System.out.println("age:" + age);
        } finally {
            reentrantLock.unlock();        
        }    
   }
}

 

但如果使用ReentrantLock,它也帶來了一個小問題,就是需要在finally代碼塊中手動釋放鎖。

 

不過說句實話,使用Lock顯示鎖的方式解決線程安全問題,給開發人員提供了更多的靈活性。

 

六、分散式鎖

如果是在單機的情況下,使用synchronized和Lock保證線程安全是沒有問題的。

 

但如果在分散式的環境中,即某個應用如果部署了多個節點,每一個節點使用可以synchronized和Lock保證線程安全,但不同的節點之間沒法保證線程安全。

 

這就需要使用分散式鎖了。

 

分散式鎖有很多種,比如:資料庫分散式鎖、zookeeper分散式鎖、redis分散式鎖等。

 

其中我個人更推薦使用redis分散式鎖,其效率相對來說更高一些。

 

使用redis分散式鎖的偽代碼如下:

try{
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
      return true;
  }
  return false;
} finally {
    unlock(lockKey);
}  

 

同樣需要在finally代碼塊中釋放鎖。

 

七、volatile

有時候,我們有這樣的需求:如果在多個線程中,有任意一個線程,把某個開關的狀態設置為false,則整個功能停止。

 

簡單的需求分析之後發現:只要求多個線程間的可見性,不要求原子性。

 

如果一個線程修改了狀態,其他的所有線程都能獲取到最新的狀態值。

 

這樣一分析這就好辦了,使用volatile就能快速滿足需求。

 

例如:


@Service
public CanalService {
    private volatile boolean running = false;
    private Thread thread;


    @Autowired
    private CanalConnector canalConnector;
    
    public void handle() {
        //連接canal
        while(running) {
           //業務處理
        }
    }
    
    public void start() {
       thread = new Thread(this::handle, "name");
       running = true;
       thread.start();
    }
    
    public void stop() {
       if(!running) {
          return;
       }
       running = false;
    }
}

 

需要特別註意的地方是:volatile不能用於計數和統計等業務場景。因為volatile不能保證操作的原子性,可能會導致數據異常。

 

八、ThreadLocal

除了上面幾種解決思路之外,JDK還提供了另外一種用空間換時間的新思路:ThreadLocal。

 

當然ThreadLocal並不能完全取代鎖,特別是在一些秒殺更新庫存中,必須使用鎖。

 

ThreadLocal的核心思想是共用變數在每個線程都有一個副本,每個線程操作的都是自己的副本,對另外的線程沒有影響。

 

溫馨提醒一下:我們平常在使用ThreadLocal時,如果使用完之後,一定要記得在finally代碼塊中,調用它的remove方法清空數據,不然可能會出現記憶體泄露問題。

 

例如:

public class ThreadLocalService {
    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();


    public void add(int i) {
        Integer integer = threadLocal.get();
        threadLocal.set(integer == null ? 0 : integer + i);
    }
}

 

九、線程安全集合

有時候,我們需要使用的公共資源放在某個集合當中,比如:ArrayList、HashMap、HashSet等。

 

如果在多線程環境中,有線程往這些集合中寫數據,另外的線程從集合中讀數據,就可能會出現線程安全問題。

 

為瞭解決集合的線程安全問題,JDK專門給我們提供了能夠保證線程安全的集合。

 

比如:CopyOnWriteArrayList、ConcurrentHashMap、CopyOnWriteArraySet、ArrayBlockingQueue等。

 

例如:

public class HashMapTest {


    private static ConcurrentHashMap<String, Object> hashMap = new ConcurrentHashMap<>();


    public static void main(String[] args) {


        new Thread(new Runnable() {
            @Override
            public void run() {
                hashMap.put("key1", "value1");
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                hashMap.put("key2", "value2");
            }
        }).start();


        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(hashMap);
    }
}

 

在JDK底層,或者spring框架當中,使用ConcurrentHashMap保存載入配置參數的場景非常多。

 

比較出名的是spring的refresh方法中,會讀取配置文件,把配置放到很多的ConcurrentHashMap緩存起來。

 

十、CAS

JDK除了使用鎖的機制解決多線程情況下數據安全問題之外,還提供了CAS機制。

 

這種機制是使用CPU中比較和交換指令的原子性,JDK裡面是通過Unsafe類實現的。

 

CAS內部包含了四個值:舊數據、期望數據、新數據和地址,比較舊數據和期望的數據,如果一樣的話,就把舊數據改成新數據。如果不一樣的話,當前線程不斷自旋,一直到成功為止。

 

不過,使用CAS保證線程安全,可能會出現ABA問題,需要使用AtomicStampedReference增加版本號解決。

 

其實,實際工作中很少直接使用Unsafe類的,一般用atomic包下麵的類即可。

public class AtomicService {
    private AtomicInteger atomicInteger = new AtomicInteger();
    
    public int add(int i) {
        return atomicInteger.getAndAdd(i);
    }
}

 

十一、數據隔離

 

有時候,我們在操作集合數據時,可以通過數據隔離,來保證線程安全。

 

例如:

 


public class ThreadPoolTest {


    public static void main(String[] args) {


      ExecutorService threadPool = new ThreadPoolExecutor(8, //corePoolSize線程池中核心線程數
      10, //maximumPoolSize 線程池中最大線程數
      60, //線程池中線程的最大空閑時間,超過這個時間空閑線程將被回收
      TimeUnit.SECONDS,//時間單位
      new ArrayBlockingQueue(500), //隊列
      new ThreadPoolExecutor.CallerRunsPolicy()); //拒絕策略


      List<User> userList = Lists.newArrayList(
      new User(1L, "蘇三", 18, "成都"),
      new User(2L, "蘇三說技術", 20, "四川"),
      new User(3L, "技術", 25, "雲南"));


      for (User user : userList) {
          threadPool.submit(new Work(user));
      }


      try {
          Thread.sleep(100);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      System.out.println(userList);
  }


    static class Work implements Runnable {
        private User user;


        public Work(User user) {
            this.user = user;
        }


        @Override
        public void run() {
            user.setName(user.getName() + "測試");
        }
    }
}

 

這個例子中,使用線程池處理用戶信息。

 

每個用戶只被線程池中的一個線程處理,不存在多個線程同時處理一個用戶的情況。所以這種人為的數據隔離機制,也能保證線程安全。

 

數據隔離還有另外一種場景:kafka生產者把同一個訂單的消息,發送到同一個partion中。每一個partion都部署一個消費者,在kafka消費者中,使用單線程接收消息,並且做業務處理。

 

這種場景下,從整體上看,不同的partion是用多線程處理數據的,但同一個partion則是用單線程處理的,所以也能解決線程安全問題。

 

作者丨蘇三呀

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/11-Thread-safety-tips-for-back-end-development.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • # 痞子衡嵌入式半月刊: 第 79 期 ![](https://raw.githubusercontent.com/JayHeng/pzh-mcu-bi-weekly/master/pics/pzh_mcu_bi_weekly.PNG) 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家介紹的是**恩智浦i.MX RT1170 FlexSPI NAND啟動時間**。 本篇是 i.MXRT1170 啟動時間評測第四彈,前三篇分別給大家評測了 [Raw NAND 啟動時間](https://www.cnblogs.com/henj ...
  • 一、插入數據優化 1.1 批量插入 如果有多條數據需要同時插入,不要每次插入一條,然後分多次插入,因為每執行一次插入的操作,都要進行資料庫的連接,多個操作就會連接多次,而一次批量操作只需要連接1次 1.2 手動提交事務 因為Mysql預設每執行一次操作,就會提交一次事務,這樣就會涉及到頻繁的事務的開 ...
  • “莆仙小館”——莆田文化展示APP 文化展示程式目的在於應用科學技術助推家鄉優秀傳統文化的展示與交流。通過圖片、視頻、音頻等展示方式向用戶立體地展示一個文化城邦。傳統文化與科學技術的有效融合,順應了社會發展的需要。傳統文化與科學技術的有效融合是發展中國特色社會主義文化的客觀需要,是傳承中國優秀傳統文 ...
  • # 解決方案 使用`ngClass`和`ngStyle`可以進行樣式的綁定。 ## ngStyle的使用 ngStyle 根據組件中的變數, isTextColorRed和fontSize的值來動態設置元素的顏色和字體大小 ```HTML This text has dynamic styles b ...
  • 在本篇文章中,我們詳細介紹了 Flutter 進階的主題,包括導航和路由、狀態管理、非同步處理、HTTP請求和Rest API,以及數據持久化。這些主題在實際應用中都非常重要,幫助你構建更複雜、功能更強大的 Flutter 應用。 ...
  • 在SpringBoot的Controller中,可以使用註解@RequestBody來獲取POST請求中的JSON數據。我們可以將這個註解應用到一個Controller方法的參數上,Spring將會負責讀取請求正文中的數據,將其反序列化為一個Java對象,並將其作為Controller方法的參數傳遞 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...