Java併發編程:淺析幾種線程安全模型 [轉]

来源:https://www.cnblogs.com/garryfu/archive/2018/02/01/8401709.html
-Advertisement-
Play Games

多線程編程一直是老生常談的問題,在Java中,隨著JDK的逐漸發展,JDK提供給我們的併發模型也越來越多,本文摘取三例使用不同原理的模型,分析其大致原理。目錄如下: 1.COW之CopyOnWriteArrayList 2.CAS之ConcurrentHashMap 3.讀寫分離之LinkedBlo ...


多線程編程一直是老生常談的問題,在Java中,隨著JDK的逐漸發展,JDK提供給我們的併發模型也越來越多,本文摘取三例使用不同原理的模型,分析其大致原理。目錄如下:

1.COW之CopyOnWriteArrayList

2.CAS之ConcurrentHashMap

3.讀寫分離之LinkedBlockingQueue

COW之CopyOnWriteArrayList

cow是copy-on-write的簡寫,這種模型來源於linux系統fork命令,Java中一種使用cow模型來實現的併發類是CopyOnWriteArrayList。相比於Vector,它的讀操作是無需加鎖的:

1 2 3 public E get(int index) {      return (E) elements[index]; }

之所以有如此神奇功效,其採取的是空間換取時間的方法,查看其add方法:

1 2 3 4 5 6 7 public synchronized boolean add(E e) {      Object[] newElements = new Object[elements.length + 1];      System.arraycopy(elements, 0, newElements, 0, elements.length);      newElements[elements.length] = e;      elements = newElements;      return true; }

我們註意到,CopyOnWriteArrayList的add方法是需要加鎖的,但其內部並沒有直接對elements數組做操作,而是先copy一份當前的數據到一個新的數組,然後對新的數組進行賦值操作。這樣做就讓get操作從同步中解脫出來。因為更改的數據並沒有發生在get所需的數組中。而是放生在新生成的副本中,所以不需要同步。但應該註意的是,儘管如此,get操作還是可能會讀取到臟數據的。

CopyOnWriteArrayList的另一特點是允許多線程遍歷,且其它線程更改數據並不會導致遍歷線程拋出ConcurrentModificationException 異常,來看下iterator()

1 2 3 4 public Iterator<E> iterator() {      Object[] snapshot = elements;      return new CowIterator<E>(snapshot, 0, snapshot.length); }

這個CowIterator 是 ListIterator的子類,這個Iterator的特點是它並不支持對數據的更改操作:

1 2 3 4 5 6 7 8 9 public void add(E object) {      throw new UnsupportedOperationException(); } public void remove() {     throw new UnsupportedOperationException(); } public void set(E object) {     throw new UnsupportedOperationException(); }

這樣做的原因也很容易理解,我們可以簡單地的認為CowIterator中的snapshot是不可變數組,因為list中有數據更新都會生成新數組,而不會改變snapshot, 所以此時Iterator沒辦法再將更改的數據寫回list了。同理,list數據有更新也不會反映在CowIterator中。CowIterator只是保證其迭代過程不會發生異常。

CAS之ConcurrentHashMap(JDK1.8)

CAS是Compare and Swap的簡寫,即比較與替換,CAS造作將比較和替換封裝為一組原子操作,不會被外部打斷。這種原子操作的保證往往由處理器層面提供支持。

在Java中有一個非常神奇的Unsafe類來對CAS提供語言層面的介面。但類如其名,此等神器如果使用不當,會造成武功盡失的,所以Unsafe不對外開放,想使用的話需要通過反射等技巧。這裡不對其做展開。介紹它的原因是因為它是JDK1.8中ConcurrentHashMap的實現基礎。

ConcurrentHashMapHashMap對數據的存儲有著相似的地方,都採用數組+鏈表+紅黑樹的方式。基本邏輯是內部使用Node來保存map中的一項key, value結構,對於hash不衝突的key,使用數組來保存Node數據,而每一項Node都是一個鏈表,用來保存hash衝突的Node,當鏈表的大小達到一定程度會轉為紅黑樹,這樣會使在衝突數據較多時也會有比較好的查詢效率。

瞭解了ConcurrentHashMap的存儲結構後,我們來看下在這種結構下,ConcurrentHashMap是如何實現高效的併發操作,這得益於ConcurrentHashMap中的如下三個函數。

1 2 3 4 5 6 7 8 9 10 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {     return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); } static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,                                     Node<K,V> c, Node<K,V> v) {     return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); } static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {     U.putOrderedObject(tab, ((long)i << ASHIFT) + ABASE, v); }

其中的U就是我們前文提到的Unsafe的一個實例,這三個函數都通過Unsafe的幾個方法保證了是原子性:

  • tabAt作用是返回tab數組第i項
  • casTabAt函數是對比tab第i項是否與c相等,相等的話將其設置為v。
  • setTabAt將tab的第i項設置為v

有了這三個函數就可以保證ConcurrentHashMap的線程安全嗎?並不是的,ConcurrentHashMap內部也使用比較多的synchronized,不過與HashTable這種對所有操作都使用synchronized不同,ConcurrentHashMap只在特定的情況下使用synchronized,來較少鎖的定的區域。來看下putVal方法(精簡版):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 final V putVal(K key, V value, boolean onlyIfAbsent) {     if (key == null || value == null) throw new NullPointerException();     int hash = spread(key.hashCode());     int binCount = 0;     for (Node<K,V>[] tab = table;;) {         Node<K,V> f; int n, i, fh;         if (tab == null || (n = tab.length) == 0)             tab = initTable();         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {             if (casTabAt(tab, i, null,                          new Node<K,V>(hash, key, value, null)))                     break;                   // no lock when adding to embin         }         else if ((fh = f.hash) == MOVED)             tab = helpTransfer(tab, f);         else {             V oldVal = null;             synchronized (f) {                     ....             }         }     }     addCount(1L, binCount);     return null; }

整個put流程大致如下:

  • 判斷key與value是否為空,為空拋異常
  • 計算kek的hash值,然後進入死迴圈,一般來講,caw演算法與死迴圈是搭檔。
  • 判斷table是否初始化,未初始化進行初始化操作
  • Node在table中的目標位置是否為空,為空的話使用caw操作進行賦值,當然,這種賦值是有可能失敗的,所以前面的死迴圈發揮了重試的作用。
  • 如果當前正在擴容,則嘗試協助其擴容,死迴圈再次發揮了重試的作用,有趣的是ConcurrentHashMap是可以多線程同時擴容的。這裡說協助的原因在於,對於數組擴容,一般分為兩步:1.新建一個更大的數組;2.將原數組數據copy到新數組中。對於第一步,ConcurrentHashMap通過CAW來控制一個int變數保證新建數組這一步只會執行一次。對於第二步,ConcurrentHashMap採用CAW + synchronized + 移動後標記 的方式來達到多線程擴容的目的。感興趣可以查看transfer函數。
  • 最後的一個else分支,黑科技的流程已嘗試無效,目標Node已經存在值,只能鎖住當前Node來進行put操作,當然,這裡省略了很多代碼,包括鏈表轉紅黑樹的操作等等。

相比於put,get的代碼更好理解一下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public V get(Object key) {     Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;     int h = spread(key.hashCode());     if ((tab = table) != null && (n = tab.length) > 0 &&         (e = tabAt(tab, (n - 1) & h)) != null) {         if ((eh = e.hash) == h) {             if ((ek = e.key) == key || (ek != null && key.equals(ek)))                 return e.val;         }         else if (eh < 0)             return (p = e.find(h, key)) != null ? p.val : null;         while ((e = e.next) != null) {             if (e.hash == h &&                 ((ek = e.key) == key || (ek != null && key.equals(ek))))                 return e.val;         }     }     return null; }
  • 檢查表是否為空
  • 獲取key的hash h,獲取key在table中對應的Node e
  • 判斷Node e的第一項是否與預期的Node相等,相等話, 則返回e.val
  • 如果e.hash < 0, 說明e為紅黑樹,調用e的find介面來進行查找。
  • 走到這一步,e為鏈表無疑,且第一項不是需要查詢的數據,一直調用next來進行查找即可。

讀寫分離之LinkedBlockingQueue

還有一種實現線程安全的方式是通過將讀寫進行分離,這種方式的一種實現是LinkedBlockingQueueLinkedBlockingQueue整體設計的也十分精巧,它的全局變數分為三類:

  • final 型
  • Atomic 型
  • 普通變數

final型變數由於聲明後就不會被修改,所以自然線程安全,Atomic型內部採用了cas模型來保證線程安全。對於普通型變數,LinkedBlockingQueue中只包含head與last兩個表示隊列的頭與尾。並且私有,外部無法更改,所以,LinkedBlockingQueue只需要保證head與last的安全即可保證真個隊列的線程安全。並且LinkedBlockingQueue屬於FIFO型隊列,一般情況下,讀寫會在不同元素上工作,所以, LinkedBlockingQueue定義了兩個可重入鎖,巧妙的通過對head與last分別加鎖,實現讀寫分離,來實現良好的安全併發特性:

1 2 3 4 5 6 7 8 /** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock(); /** Wait queue for waiting takes */ private final Condition notEmpty = takeLock.newCondition(); /** Lock held by put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock(); /** Wait queue for waiting puts */ private final Condition notFull = putLock.newCondition();

首先看下它的offer 方法:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public boolean offer(E e) {     if (e == null) throw new NullPointerException();     final AtomicInteger count = this.count;     if (count.get() == capacity)         return false;     int c = -1;     Node<E> node = new Node<E>(e);     final ReentrantLock putLock = this.putLock;     putLock.lock();     try {         if (count.get() < capacity) {             enqueue(node);             c = count.getAndIncrement();             if (c + 1 < capacity)                 notFull.signal();         }     } finally {         putLock.unlock();     }     if (c == 0)         signalNotEmpty();     return c >= 0; }

可見,在對隊列進行添加元素時,只需要對putLock進行加鎖即可,保證同一時刻只有一個線程可以對last進行插入。同樣的,在從隊列進行提取元素時,也只需要獲取takeLock鎖來對head操作即可:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public E poll() {     final AtomicInteger count = this.count;     if (count.get() == 0)         return null;     E x = null;     int c = -1;     final ReentrantLock takeLock = this.takeLock;     takeLock.lock();     try {         if (count.get() > 0) {             x = dequeue();             c = count.getAndDecrement();             if (c > 1)                 notEmpty.signal();         }     } finally {         takeLock.unlock();     }     if (c == capacity)         signalNotFull();     return x; }
LinkedBlockingQueue整體還是比較好理解的,但有幾個點需要特殊註意:
  • LinkedBlockingQueue是一個阻塞隊列,當隊列無元素為空時,所有取元素的線程會通過notEmpty 的await()方法進行等待,直到再次有數據enqueue時,notEmpty發出signal信號。對於隊列達到上限時也是同理。
  • 對於remove,contains,toArray, toString, clear之類方法,會調用fullyLock方法,來同時獲取讀寫鎖。但對於size方法,由於隊列內部維護了AtomicInteger類型的count變數,是不需要加鎖進行獲取的。

本文轉自:http://www.importnew.com/27922.html

 


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

-Advertisement-
Play Games
更多相關文章
  • package com.test.test; import java.io.File;import java.io.IOException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import ...
  • 相關: 字元串 常用函數 (更詳細參照函數庫或者使用編譯器時註意提示) 字元串格式化 原始字元串 列表 常用函數 列表生成式 字典 常用函數 集合 常用函數 附加: python的很多編譯... ...
  • Python 與 java 對比,代碼更為簡潔。 Python 3.X 版本 Hello World 程式: java Hello World 程式: Python 每句代碼後面不需要加分號。 Python 的註釋寫法與 java 不同,單行註釋為 單行註釋 用 #被註釋內容 多行註釋用三個單引號或 ...
  • 本節主要學習的是:1.列表、元組操作 2.字元串操作 3.字典操作 4.集合操作 5.文件操作 ...
  • 引言:CSP(http://www.cspro.org/lead/application/ccf/login.jsp)是由中國電腦學會(CCF)發起的“電腦職業資格認證”考試,針對電腦軟體開發、軟體測試、信息管理等領域的專業人士進行能力認證。認證對象是從事或將要從事IT領域專業技術與技術管理人 ...
  • python 的paramiko 模塊,改模塊是基於ssh用於連接遠程伺服器並執行相關操作 SSHClient 用於連接遠程伺服器並執行基本命令: 基於用戶名密碼的連接: 如果遇到這樣的問題 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy ...
  • 1.文件上傳下載 http://blog.csdn.net/gplihf/article/details/52128225 ...
  • 參考這個博主文章http://www.cnblogs.com/leechenxiang/p/7091731.html 完成之後最後還得在安裝包目錄重新配置一次 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...