首頁 > Java > java教程 > 主體

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

不言
發布: 2018-09-17 15:23:07
原創
2045 人瀏覽過

這篇文章帶給大家的內容是關於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中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板