首頁 Java java教程 Java中線程安全實作以及CLH 佇列原理的簡單介紹(程式碼範例)

Java中線程安全實作以及CLH 佇列原理的簡單介紹(程式碼範例)

Sep 17, 2018 pm 03:23 PM
多執行緒 並行

這篇文章帶給大家的內容是關於Java中線程安全實作以及CLH 佇列原理的簡單介紹(程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

阻塞同步

在Java 中,我們經常使用synchronized 關鍵字來做到互斥同步以解決多執行緒並發存取共享資料的問題。 synchronzied 關鍵字在編譯後,會在 synchronized 所包含的同步程式碼區塊前後分別加入 monitorenter 和 monitorexit 這兩個字節碼指令。 synchronized 關鍵字需要指定一個物件來進行加鎖和解鎖。例如:

public class Main {

    private static final Object LOCK = new Object();
    
    public static void fun1() {
        synchronized (LOCK) {
            // do something
        }
    }
    
    public static void fun2() {
        synchronized (LOCK) {
            // do something
        }
    }
}
登入後複製

在沒有明確指定該物件時,根據 synchonized 修飾的是實例方法還是靜態方法,從而決定採用物件實例或類別的class實例作為所物件。例如:

public class SynchronizedTest {
    public synchronized void doSomething() {
        //采用实例对象作为锁对象
    }
}
登入後複製
public class SynchronizedTest {
    public static synchronized void doSomething() {
        //采用SynchronizedTest.class 实例作为锁对象
    }
}
登入後複製

由於基於synchronized 實現的阻塞互斥,除了需要阻塞操作線程,而且喚醒或阻塞作業系統層級的原生線程,需要從使用者態轉換到核心狀態中,這個狀態的轉換消耗的時間可能比使用者程式碼執行的時間還要長,因此我們經常說synchronized 是Java 語言中的「重量級鎖定」。

非阻塞同步


##使用synchronized 關鍵字的同步方式最主要的問題就是進行執行緒阻塞和喚醒時所帶來的效能消耗問題。阻塞同步屬於悲觀的並發策略,只要有可能出現競爭,它都認為一定要加鎖。

然而同步策略還有另外一種樂觀的策略,樂觀並發策略先進性對數據的操作,如果沒有發現其它線程也操作了數據,那麼就認為這個操作是成功的。如果發生了其它線程也操作了數據,那麼一般採取不斷重試的手段,直到成功為止,這種樂觀鎖的策略,不需要把線程阻塞,屬於非阻塞同步的一種手段。

CAS

樂觀並發策略主要有兩個重要的階段,一個是對資料進行操作,另外一個是進行衝突的偵測,即偵測其它執行緒有無同時也對該數據進行了操作。這裡的資料操作和衝突偵測需要具備原子性
,否則就容易出現類似 i 的問題。
CAS 的意義為compare and swap,目前絕大多數CPU 都原生支援CAS 原子指令,例如在IA64、x86的指令集中,就有cmpxchg 這樣的指令來完成CAS 功能,它的原子性要求是在硬體層面上得到保證的。
CAS 指令一般需要有三個參數,分別是值的記憶體位址、期望中的舊值和新值。 CAS 指令執行時,如果該記憶體位址上的值符合預期中的舊值,處理器會以新值更新該記憶體位址上的值,否則就不會更新。這個操作在 CPU 內部保證了是原子性的。 在Java 中有許多CAS 相關的API,我們常見的有java.util.concurrent 套件下的各種原子類,例如AtomicIntegerAtomicReference
等等。 這些類別都支援 CAS 操作,其內部實際上也依賴 sun.misc.Unsafe
這個類別裡的 compareAndSwapInt() 和 compareAndSwapLong() 方法。 CAS 並非是完美無缺的,儘管它能保證原子性,但它存在一個著名的 ABA 問題。一個變數初次讀取的時候值為 A,再一次讀取的時候也是 A,那麼我們是否能說明這個變數在兩次讀取中間沒有改變?不能。在這段期間,變數可能由 A 變成 B,再由 B 變成 A,第二次讀取的時候看到的是 A,但實際上這個變數改變了。一般的程式碼邏輯不會在意這個 ABA 問題,因為根據程式碼邏輯它不會影響並發的安全性,但如果在意的話,可能會考慮採用阻塞同步的方式而不是 CAS。實際上 JDK 本身也對這個 ABA 問題解決方案,提供了 AtomicStampedReference

這個類,為變數加上版本來解決 ABA 問題。

自旋鎖定


以synchronized 為代表的阻塞同步,因為阻塞執行緒會恢復執行緒的操作都需要涉及到作業系統層面的使用者態和核心態之間的切換,這對系統的效能影響很大。自旋鎖的策略是當執行緒去取得一個鎖時,如果發現該鎖已經被其它執行緒佔有,那麼它不馬上放棄CPU 的執行時間片,而是進入一個「無意義」的循環,查看該執行緒是否已經放棄了鎖。 但自旋鎖適用於臨界區

比較小的情況,如果鎖持有的時間過長,那麼自旋操作本身就會白白耗掉系統的性能。

以下為一個簡單的自旋鎖定實作:###
import java.util.concurrent.atomic.AtomicReference;
public class SpinLock {
   private AtomicReference<Thread> owner = new AtomicReference<Thread>();
   public void lock() {
       Thread currentThread = Thread.currentThread();
        // 如果锁未被占用,则设置当前线程为锁的拥有者
       while (!owner.compareAndSet(null, currentThread)) {}
   }

   public void unlock() {
       Thread currentThread = Thread.currentThread();
        // 只有锁的拥有者才能释放锁
       owner.compareAndSet(currentThread, null);
   }
}
登入後複製

上述的代码中, owner 变量保存获得了锁的线程。这里的自旋锁有一些缺点,第一个是没有保证公平性,等待获取锁的线程之间,无法按先后顺序分别获得锁;另一个,由于多个线程会去操作同一个变量 owner,在 CPU 的系统中,存在着各个 CPU 之间的缓存数据需要同步,保证一致性,这会带来性能问题。

公平的自旋

为了解决公平性问题,可以让每个锁拥有一个服务号,表示正在服务的线程,而每个线程尝试获取锁之前需要先获取一个排队号,然后不断轮询当前锁的服务号是否是自己的服务号,如果是,则表示获得了锁,否则就继续轮询。下面是一个简单的实现:

import java.util.concurrent.atomic.AtomicInteger;

public class TicketLock {
   private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
   private AtomicInteger ticketNum = new AtomicInteger(); // 排队号

   public int lock() {
       // 首先原子性地获得一个排队号
       int myTicketNum = ticketNum.getAndIncrement();
       // 只要当前服务号不是自己的就不断轮询
       while (serviceNum.get() != myTicketNum) {
       }
       return myTicketNum;
    }

    public void unlock(int myTicket) {
        // 只有当前线程拥有者才能释放锁
        int next = myTicket + 1;
        serviceNum.compareAndSet(myTicket, next);
    }
}
登入後複製

虽然解决了公平性的问题,但依然存在前面说的多 CPU 缓存的同步问题,因为每个线程占用的 CPU 都在同时读写同一个变量 serviceNum,这会导致繁重的系统总线流量和内存操作次数,从而降低了系统整体的性能。

MCS 自旋锁

MCS 的名称来自其发明人的名字:John Mellor-Crummey和Michael Scott。
MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isWaiting = true; // 默认是在等待锁
    }
    volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
            .newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock(MCSNode currentThread) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
        if (predecessor != null) {
            predecessor.next = currentThread;// step 2
            while (currentThread.isWaiting) {// step 3
            }
        } else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
            currentThread.isWaiting = false;
        }
    }

    public void unlock(MCSNode currentThread) {
        if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
            return;
        }

        if (currentThread.next == null) {// 检查是否有人排在自己后面
            if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                // compareAndSet返回true表示确实没有人排在自己后面
                return;
            } else {
                // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                while (currentThread.next == null) { // step 5
                }
            }
        }
        currentThread.next.isWaiting = false;
        currentThread.next = null;// for GC
    }
}
登入後複製

MCS 的能够保证较高的效率,降低不必要的性能消耗,并且它是公平的自旋锁。

CLH 自旋锁

CLH 锁与 MCS 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。
CLH 锁的名字也与他们的发明人的名字相关:Craig,Landin and Hagersten。
CLH 锁与 MCS 锁最大的不同是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。
实现如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
    public static class CLHNode {
        private volatile boolean isWaiting = true; // 默认是在等待锁
    }
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
            . newUpdater(CLHLock.class, CLHNode .class , "tail" );
    public void lock(CLHNode currentThread) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThread);
        if(preNode != null) {//已有线程占用了锁,进入自旋
            while(preNode.isWaiting ) {
            }
        }
    }

    public void unlock(CLHNode currentThread) {
        // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThread, null)) {
            // 还有后续线程
            currentThread.isWaiting = false ;// 改变状态,让后续线程结束自旋
        }
    }
}
登入後複製

从上面可以看到,MCS 和 CLH 相比,CLH 的代码比 MCS 要少得多;CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋;CLH的队列是隐式的,通过轮询关注上一个节点的某个变量,隐式地形成了链式的关系,但CLHNode并不实际持有下一个节点,MCS的队列是物理存在的,而 CLH 的队列是逻辑上存在的;此外,CLH 锁释放时只需要改变自己的属性,MCS 锁释放则需要改变后继节点的属性。

CLH 队列是 J.U.C 中 AQS 框架实现的核心原理。

以上是Java中線程安全實作以及CLH 佇列原理的簡單介紹(程式碼範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

C++ 函式異常與多執行緒:並發環境下的錯誤處理 C++ 函式異常與多執行緒:並發環境下的錯誤處理 May 04, 2024 pm 04:42 PM

C++中函數異常處理對於多執行緒環境特別重要,以確保執行緒安全性和資料完整性。透過try-catch語句,可以在出現異常時擷取和處理特定類型的異常,以防止程式崩潰或資料損壞。

PHP 多執行緒如何實作? PHP 多執行緒如何實作? May 06, 2024 pm 09:54 PM

PHP多執行緒是指在一個行程中同時執行多個任務,透過建立獨立運行的執行緒實作。 PHP中可以使用Pthreads擴充模擬多執行緒行為,安裝後可使用Thread類別建立和啟動執行緒。例如,處理大量資料時,可將資料分割為多個區塊,並建立對應數量的執行緒同時處理,提高效率。

並發和協程在Golang API設計中的應用 並發和協程在Golang API設計中的應用 May 07, 2024 pm 06:51 PM

並發和協程在GoAPI設計中可用於:高效能處理:同時處理多個請求以提高效能。非同步處理:使用協程非同步處理任務(例如傳送電子郵件),釋放主執行緒。流處理:使用協程高效處理資料流(例如資料庫讀取)。

C++中如何處理多執行緒中的共享資源? C++中如何處理多執行緒中的共享資源? Jun 03, 2024 am 10:28 AM

C++中使用互斥量(mutex)處理多執行緒共享資源:透過std::mutex建立互斥量。使用mtx.lock()取得互斥量,對共享資源進行排他存取。使用mtx.unlock()釋放互斥。

C++ 多執行緒程式測試的挑戰與策略 C++ 多執行緒程式測試的挑戰與策略 May 31, 2024 pm 06:34 PM

多執行緒程式測試面臨不可重複性、並發錯誤、死鎖和缺乏可視性等挑戰。策略包括:單元測試:針對每個執行緒編寫單元測試,驗證執行緒行為。多執行緒模擬:使用模擬框架在控制執行緒調度的情況下測試程式。資料競態偵測:使用工具尋找潛在的資料競態,如valgrind。調試:使用調試器(如gdb)檢查運行時程序狀態,找到資料競爭根源。

C++ 記憶體管理在多執行緒環境中的挑戰與應對措施? C++ 記憶體管理在多執行緒環境中的挑戰與應對措施? Jun 05, 2024 pm 01:08 PM

在多執行緒環境中,C++記憶體管理面臨以下挑戰:資料競爭、死鎖和記憶體洩漏。因應措施包括:1.使用同步機制,如互斥鎖和原子變數;2.使用無鎖資料結構;3.使用智慧指標;4.(可選)實現垃圾回收。

Go 並發函數的單元測試指南 Go 並發函數的單元測試指南 May 03, 2024 am 10:54 AM

對並發函數進行單元測試至關重要,因為這有助於確保其在並發環境中的正確行為。測試並發函數時必須考慮互斥、同步和隔離等基本原理。可以透過模擬、測試競爭條件和驗證結果等方法對並發函數進行單元測試。

C++ 技術中的例外處理:如何在多執行緒環境中正確處理例外狀況? C++ 技術中的例外處理:如何在多執行緒環境中正確處理例外狀況? May 09, 2024 pm 12:36 PM

在多執行緒C++中,例外處理遵循以下原則:及時性、執行緒安全性和明確性。在實戰中,可以透過使用mutex或原子變數來確保異常處理程式碼線程安全。此外,還要考慮異常處理程式碼的重入性、效能和測試,以確保其在多執行緒環境中安全有效地運作。

See all articles