多線程筆記:同步機制(1)

来源:https://www.cnblogs.com/llh4cnblogs/archive/2020/03/06/12431659.html
-Advertisement-
Play Games

同步機制簡介 線程同步機制是一套用於協調線程間的數據訪問及活動的機制,該機制用於保障線程安全以及實現這些線程的共同目標。 線程同步機制是編程語言為多線程運行制定的一套規則,合理地運用這些規則可以很大程度上保障程式的正確運行。 這套機制包含兩方面的內容,一是關於多線程間的數據訪問的規則,二是多線程間活 ...


同步機制簡介

線程同步機制是一套用於協調線程間的數據訪問及活動的機制,該機制用於保障線程安全以及實現這些線程的共同目標。

線程同步機制是編程語言為多線程運行制定的一套規則,合理地運用這些規則可以很大程度上保障程式的正確運行。

這套機制包含兩方面的內容,一是關於多線程間的數據訪問的規則,二是多線程間活動的規則。前者關乎程式運行的正確與否,是相當重要的內容;後者很大程度上是影響程式的運行效率,也是不容忽視的內容。不太嚴謹地說,數據訪問的規則主要是由鎖來實現,線程間活動的規則則表現線程調度上。

線程安全問題的產生前提是多個線程併發訪問共用數據,那麼一種保障線程安全的方法就是將多個線程對共用數據的併發訪問轉換為串列訪問,即一個共用數據一次只能被一個線程訪問,該線程訪問結束後其他線程才能對其進行訪問。鎖就是利用這種思路來實現線程同步機制。

GoLang中換了個思路,通過通道(channel)來實現共用數據的安全性。

鎖的相關概念

鎖在編程里是個蠻有趣的概念。

鎖:置於可啟閉的器物上,以鑰匙或暗碼(如字碼機構、時間機構、自動釋放開關、磁性螺線管等)打開的扣件 ——線上新華字典

特定代碼的作用域或是lock()unlock()方法之間的代碼構成的區域就是“器物”的表徵,線程訪問其中的共用數據相當於解開“扣件”,打開了“器物”;通常所說“獲得xx鎖”,更像是獲得了“鑰匙或暗碼”能夠打開“扣件”的憑證。

說人話就是,鎖在生活通常是“護衛”、“保護”的含義,應當是阻止進入或下一步行動的拒絕機制;在編程里略有不同,它一方面是指代了需要被保護代碼(或數據)的範圍,一方面又指代了進入受保護代碼(或數據)的憑證。基於此,許多書與博客中的“申請鎖”這說法才能說的通。不然,按生活常理,你獲得了一把鎖,沒有鑰匙好像也沒什麼用。

上述內容是本人的一點心得體會,不保證正確,不保證嚴謹

下麵介紹與鎖相關的一些概念。

  1. 臨界區(Critical Section):獲得鎖之後和釋放鎖之前的這段時間內執行的代碼
  2. 內部鎖(Intrinsic Lock)與顯式鎖(Explicit Lock):按時Java虛擬機對鎖實現的方式劃分,內部鎖(由關鍵字synchronized實現)與顯式鎖(Lock介面的實現類實現)
  3. 可重入性(Reentrancy):一個線程在其持有一個鎖的時候能否再次(或多次)申請該鎖
  4. 鎖的爭用與調度:鎖也可以被看作是一種排他性的資源,因此爭用、調度概念也對鎖適用。鎖的調用基本上是Java虛擬機的設計者需要考慮的問題。Java平臺中鎖的調度策略包括了公平與非公平兩種,內部鎖屬於非公平鎖而顯式鎖則既支持公平鎖又支持非公平鎖
  5. 鎖的粒度:一個鎖實例所保護的共用數據的數量大小就被稱為鎖的粒度。但這是一個相對概念,應該根據實際情況來說明鎖粒度的大小
  6. 如果有多個線程訪問同一個鎖所保護的共用數據,那麼就你這些線程同步在這個鎖上,或是對這些線程所訪問的共用數據訪問進行了加鎖;相應地,這些線程所執行的臨界區就被為這個鎖所引導的臨界區
  7. 鎖的排他、互斥:都是指一個鎖一次只能被一個線程所持有的特性。

以上就是與鎖相關的一些概念,這些概念也比較通用,在其他編程語言里或多或少也會有它們的身影。

內部鎖:synchronized關鍵字

Java平臺中任何一個對象都有唯一一個與之關聯的鎖。這種鎖被稱為監視器或是內部鎖。內部鎖是一種排他鎖,它能保障原子性、可見性和有序性。

內部鎖通過synchronized關鍵字實現的。synchronized關鍵字可以修飾方法及代碼塊。修飾方法時,此方法被稱為同步(靜態/實例)方法;修飾代碼塊時,被稱為同步(靜態)代碼塊。

同步方法

synchronized修飾的方法被稱為同步方法。同步方法的整個方法體就是一個臨界區。

public synchronized void sayHello(){
     // do something
}

同步靜態方法相當於以當前類對象為引導鎖的同步塊。

同步塊

synchronized (鎖句柄){
// do something
}

synchronized關鍵字所引導的代碼塊就是臨界區。鎖句柄是一個對象的引用。鎖句柄可以填寫this關鍵字,表示當前對象。習慣上也直接稱鎖句柄為鎖。鎖句柄對應的監視器就被稱為相應同步塊的引導鎖。相應地,我們稱呼相應的同步為該鎖引導的同步塊
作為鎖句柄的變數通常採用final修飾。這是因為鎖句柄變數的值一旦改變,會導致執行同一個同步塊的多個線程實際上使用不同的鎖,從而導致競態。因而,鎖句柄的變數通常聲明形式為private final Object lock = new Object();

特性

線程在執行臨界區代碼時必須持有該臨界區的引導鎖。一個線程執行到同步塊(同步方法也可看作是同步塊)時必須先申請該同步塊的引導鎖,只有申請成功該的線程才能夠執行相應的臨界區。一個線程執行完成臨界區代碼後引導該臨界區的鎖就會被自動釋放。在這個過程中,線程對內部鎖申請與釋放的動作由Java虛擬機負責完成,這也是synchronized實現的鎖被稱之為內部鎖的原因。
內部鎖的使用並不會導致鎖泄漏。Java編譯器對同步塊代碼作了特殊的處理,這使得臨界區的代碼即使拋出異常也不會妨礙內部鎖的釋放。

內部鎖的調度

Java虛擬機會為每個內部鎖分配一個入口集,用於記錄等待獲得相應內部鎖的線程。多個線程申請同一個鎖的時候,只有一個申請都能夠成為該鎖的持有線程(即申請鎖成功),而其他申請者的申請操作會失敗。這些申請失敗的線程並不會拋出異常,而是會被暫停(生命周期狀態變為BLOCKED)並被存入相應鎖的入口集中等待再申請鎖的機會。入口 集中的線程就被稱為相應內部鎖的等待線程。當該內部鎖被釋放時,入口集中的任意線程會被Java虛擬機喚醒,得到再次申請鎖的機會。由於是非公平的高度,被喚醒的等待處理器運行時可能還有其他新和活躍線程(RUNNABLE狀態且未進入過入口集)與該線程搶占這個被釋放的鎖,因此被喚醒的線程不一定就能成為該鎖的持有線程。另外,Java虛擬機從入口集中選擇一個等待線程的演算法與虛擬機具體實現有關,總的來說是隨機的。(像極了女神和她的備胎們)

顯式鎖:Lock介面

顯式鎖是自JDK1.5引入的排他鎖,其作用與內部鎖大致相同,並額外提供些特性。

顯式鎖是java.util.concurrent.locks.Lock介面的實例。該介面對顯式鎖進行了抽象,類java.uitl.concurrent.locks.ReentrantLock是它預設實現類。

使用模版:

private final Lock lock = ....; // 一個Lock介面的實例
...
lock.lock(); // 申請鎖
try{
// do something
}finally{
lock.unlock(); // 手動釋放鎖,避免鎖泄漏
}

顯式鎖支持公平調度,但開銷相對較大,預設使用非公平調度。

改進型鎖:讀寫鎖

鎖的排他性使得多個線程無法以線程安全的方式在同一時刻對變數進行讀取(只讀不更新),不利於提高系統的併發性。
讀寫鎖(Read/Write Lock)是一種改進型的排他鎖,也被稱為共用/排他鎖(Shared/Exclusive Lock)。讀寫鎖允許多個線程可以同時讀取(只讀)共用變數,但是一次只允許一個線程對共用變數進行更新(包括讀取後再更新)。任何線程讀取變數的時候,其他線程無法更新這些變數 ;一個線程更新共用變數的時候,其他任何線程都無法訪問該變數。

輕量級同步機制:volatile關鍵字

volatile有“易揮發”的意思,引申為“不穩定”。volatile關鍵字用於修飾共用可變變數,即沒有使用final關鍵字修飾的實例變數或靜態變數,相應的變數被稱為volatile變數。

volatile關鍵字表示被修飾的變數的值容易變化(即被其他線程更改),因而不穩定。volatile變數的不穩定性意味著對這種變數的讀寫操作都必須從高速緩存或者主記憶體中讀取,以獲取變數的相對新值。因些,volatile變數不會被編譯器分配到寄存器進行存儲,對volatile變數的讀寫操作都是記憶體訪問操作。

volatile關鍵字常被稱為輕量級鎖,其作用與鎖的作用有相同的地方:保證可見性的有序性。在原子性方面,它僅能保障寫volatile變數操作的原子性,但沒有鎖的排他性;其次,volatile關鍵字的使用不會引起上下文切換(正是“輕量級”的原因)。

作用

volatile關鍵字的作用包括:保障可見性、有序性和long/double型變數讀寫操作的原子性(不是賦值操作)。
某些32位Java虛擬機上long/double型變數寫操作不具有原子性,加上volatile關鍵字後,其讀寫操作本身就具有了原子性。一般地,對於volatile變數的賦值操作,其右邊表達式中涉及共用變數(包括賦值的volatile變數本身),那麼這個同仁操作就不是原子操作,要保障這樣操作的原子性,仍然需要鎖。
讀線程對volatile變數讀取操作會產生類似於獲得鎖的效果;寫線程則會產生類似於釋放鎖的效果;因此,volatile具有保障有序性和可見性的作用。
如果volatile變數是數組,那麼volatile關鍵字只能對數組引用本身的操作起作用(讀取數組引用和更新數組引用),而無法對數組元素的操作起作用(讀取、更新數組元素)。

開銷

總的來說,讀寫操作成本介於普通變數寫和在臨界區內進行讀寫操作之間。

CAS指令

CAS(Compare and Swap)是對一種處理器指令的稱呼,不少多線程相關的Java標準庫類的實現最終都會藉助CAS,實際中大多數情況不會直接使用。
對於簡單的自增操作count++來說,使用鎖的開銷過大,使用volatile又不能保證原子性,這種情況下可以使用CAS。它能將read-modify-write和check-and-act之類操作轉換為原子操作,其實現如偽代碼所示:

boolean compareAndSwap(Variable v ,Object oldVal ,Object newVal){
    if (oldVal == v.get()){ // check: 檢查變數值是否被其他線程修改過
        v.set(newVal); // act:更新變數值
        return true; // 更新成功
    }
    return false; // 變數值被其他線程修改,更新失敗
}

CAS操作前提假設是:如果變數v的當前值和客戶請求(即調用)CAS時所提供的變數值(即變數的舊值)是相等的,那麼就說明其他線程並沒有修改過變數v的值,可以執行更新值的操作。其他線程更新值的操作則會失敗。
這個假設並不一定總能成立。

ABA 問題

對於共用變數v,當前線程看到它的值為A的那一刻,其他線程已經將其值更新為B,接著在當前線程執行CAS時該變數又被其他線程更新為A了,這就是ABA問題。
對ABA 問題接受度與要實現的演算法有關,某些情況下無法接受ABA問題的存在。ABA問題常引入修訂號來解決,即用[共用變數實際值,修改號]這樣的元組來表示共用變數的值,AtomicStampedReference類就是基於這種思想實現的。

原子變數類

原子變數類比鎖的粒度更細,更輕量級,並且對於在多處理器系統上實現高性能的併發代碼來說是非常關鍵的。原子變數將發生競爭的範圍縮小到單個變數上。
原子變數類相當於一種泛化的 volatile 變數,能夠支持原子的、有條件的讀/改/寫操作。
原子類在內部使用 CAS 指令(基於硬體的支持)來實現同步。這些指令通常比鎖更快。
原子變數類可分為4組:

分組
基礎數據型 AtomicBoolean AtomicInteger AtomicLong
數組型 AtomicIntegerArray AtomicLongArray AtomicReferenceArray
欄位更新器 AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicStampedReference
引用型 AtomicReference AtomicReferenceFieldUpdater AtomicMarkableReference

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

-Advertisement-
Play Games
更多相關文章
  • 1 正則表達式概述 1.1 什麼是正則表達式 正則表達式(Regular Expression)是用於匹配字元串中字元組合的模式。在Javascript中,正則表達式也是對象。正則表通常被用來檢索,替換那些符合某個模式(規則)的文本,例如驗證表單:用戶名錶單隻能輸入英文字母、數字或者下劃線,昵稱輸入 ...
  • 根據VueRouter的執行流程,可以通過這三個步驟來理解它的設計思想: 第一步:我們new VueRouter創建VueRouter實例的時候會通過深度遍歷把傳入的router屬性對應的數組給解析一下,保存到一個Map中,每個Map對應router的一個元素,我們稱之為路由記錄,解析的時候會給每個 ...
  • 1.模式從何而來 ♦模式起源於建築領域 ♦模式之父—Christopher Alexander(克裡斯托弗·亞歷山大) ♦模式定義:模式是在特定環境下人們解決某類重覆出現問題的一套成功或有效的解決方案。【A pattern is a successful or efficient solution ...
  • Abp系列 一. "abp框架運行——前後端分離(基於VUE)" 二. "基於abp框架的資料庫種子數據初始化" 基於abp框架的資料庫種子數據初始化 [toc] 1.背景 最近在用abp開發項目,需要將如下區域數據通過程式的初次運行種入資料庫。 不知道大家是怎麼去管理資料庫跟後臺程式的同步的, " ...
  • 一、泛型初步 1.泛型是編譯階段的語法,在編譯階段統一集合中的類型。 package com.bjpowernode.java_learning; import java.util.*; public class D93_1_GenercityInitial { public static void ...
  • NumPy(Numerical Python) 是 Python 語言的一個擴展程式庫,支持大量的維度數組與矩陣運算,此外也針對數組運算提供大量的數學函數庫。NumPy 是一個運行速度非常快的數學庫,主要用於數組計算,包含:一個強大的N維數組對象 ndarray;廣播功能函數;整合 C/C++/Fo ...
  • 分享一下我自己整理的代碼,改兩個參數就可使用(掃描下方二維碼獲取python學習資料) import requests import time headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebK ...
  • 本文通過結合上一篇博文《openOPC與監控頁面一》展現瞭如何通過通過一步一步實現UI端到OPC服務端數據刷新的技術驗證過程, ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...