1. 程式人生 > >執行緒進階:多工處理(17)——Java中的鎖(Unsafe基礎)

執行緒進階:多工處理(17)——Java中的鎖(Unsafe基礎)

1. 概述

本專題在之前的文章中詳細介紹了Java中最常使用的一種鎖機制——同步鎖。但是同步鎖肯定是不適合在所有應用場景中使用的。所以從本文開始,筆者將試圖通過兩到三篇文章的篇幅向讀者介紹Java中鎖的分類、原理和底層實現。以便大家在實際工作中根據應用場景進行使用。本篇文章我們先介紹Java中關於鎖的底層實現的基礎類sun.misc.Unsafe。

2. Unsafe原子操作

在介紹Java中除同步鎖以外的其它鎖機制前,我們要介紹一個Java中基於作業系統級別的原子操作類sun.misc.Unsafe,它是Java中對大多數鎖機制實現的最基礎類。請注意,JDK 1.8和之前JDK版本的中sun.misc.Unsafe類可提供的方法有較大的變化,而本文的講解主要是基於JDK 1.8進行的。sun.misc.Unsafe類提供的原子操作基於作業系統直接對CPU進行操作,而以下這些方法又是sun.misc.Unsafe類中經常被使用的:

2-1. 在你的程式碼中使用Unsafe

Java語言出於封裝性和安全性的考慮,它不允許技術人員直接使用Unsafe類和其中的各種方法,特別是不能直接使用“new”的方式對sun.misc.Unsafe類進行例項化(否則會報告“java.lang.SecurityException: Unsafe”錯誤)。但是你可以通過Java提供的反射機制獲取到Unsafe類的例項:

……
Field f = null;
sun.misc.Unsafe unsafe = null;
try {
    f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true
); // 得到Unsafe類的例項 unsafe = (Unsafe) f.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); } ……

2-2. Unsafe典型方法

2-2-1. Unsafe.objectFieldOffset(Field) 和類似方法

Unsafe中除了objectFieldOffset(Field) 這個方法外,還有一個類似的方法staticFieldOffset(Field)。這兩個方法用於返回類定義中某個屬性在主存中設定的偏移量。請看以下程式碼:

……
// 注意本程式碼中的unsafe物件就是根據之前程式碼獲取到的

// 開始使用unsafe物件,分別找到UserPojo物件中child屬性和name屬性的記憶體地址偏移量
// 首先是UserPojo類中的child屬性,在記憶體中設定的偏移位置
Field field = UserPojo.class.getDeclaredField("child");
// 這就是一旦這個類例項化後,該屬性在記憶體中的偏移位置
long offset = unsafe.objectFieldOffset(field);
System.out.println("child offset = " + offset);

// 然後是UserPojo類中的name屬性,在記憶體中設定的偏移位置
Field fieldName = UserPojo.class.getDeclaredField("name");
long nameOffset = unsafe.objectFieldOffset(fieldName);
System.out.println("name offset = " + nameOffset);
……

這兩個方法相比後面要介紹的各種Unsafe方法來說,顯得更為重要。因為Unsafe物件中屬性的操作方式都是直接通過記憶體偏移量的方式找到操作目標。

2-2-2. Unsafe.compareAndSwapObject(Object, long, Object, Object)和類似方法

Unsafe中除了compareAndSwapObject 這個方法外,還有兩個類似的方法:unsafe.compareAndSwapInt和unsafe.compareAndSwapLong。這些方法的作用就是對屬性進行比較並替換(俗稱的CAS過程——Compare And Swap)。當給定的物件中,指定屬性的值符合預期,則將這個值替換成一個新的值並且返回true;否則就忽略這個替換操作並且返回false。

請注意CAS過程是sun.misc.Unsafe類中除了獲取記憶體偏移量以外,提供的最重要的功能了——因為Java中很多基於“無同步鎖”方式的功能實現原理都是基於CAS過程。請看如下示例程式碼:

……
UserPojo user = new UserPojo();
user.setName("yinwenjie");
user.setSex(11);
user.setUserId("userid");

// 獲得sex屬性的記憶體地址偏移量 
Field field = UserPojo.class.getDeclaredField("sex");
long sexOffset = unsafe.objectFieldOffset(field);

/* 
 * 比較並修改值
 * 1、需要修改的物件
 * 2、要更改的屬性的記憶體偏移量
 * 3、預期的值
 * 4、設定的新值
 * */
// 為什麼是Object而不是int呢?因為sex屬性的型別是Integer不是int嘛
if(unsafe.compareAndSwapObject(user, sexOffset, 11, 13)) {
    System.out.println("更改成功!");
} else {
    System.out.println("更改失敗!");
}
……

首先建立一個UserPojo類的例項物件,這個例項物件有三個屬性name、sex和userId。接著我們找到sex屬性在主存中設定的偏移量sexOffset,並進行CAS操作。請注意compareAndSwapObject方法的四個值:第一個值表示要進行操作的物件user,第二個引數通過之前獲取的主存偏移量sexOffset告訴方法將要比較的是user物件中的哪個屬性,第三個引數為技術人員所預想的該屬性的當前值,第四個引數為將要替換成的新值。

那麼將方法套用到以上的compareAndSwapObject執行過程中:如果當前user物件中sex屬性為11,則將這個sex屬性的值替換為13,並返回true;否則不替換sex屬性的值,並且返回false。

2-2-3. Unsafe.getAndAddInt(Object, long, int)和類似方法

類似的方法還有getAndAddLong(Object, long, long),它們的作用是利用Unsafe的原子操作性,向呼叫者返回某個屬性當前的值,並且緊接著將這個屬性增加一個新的值。在java.util.concurrent.atomic程式碼包中,有一個類AtomicInteger,這個類用於進行基於原子操作的執行緒安全的計數操作,且這個類在JDK1.8+的版本中進行了較大的修改。以下程式碼示例了該類的getAndIncrement()方法中的實現片段:

……
public class AtomicInteger extends Number implements java.io.Serializable {
    ……
    private volatile int value;
    ……
    private static final long valueOffset;
    ……
    // 獲取到value屬性的記憶體偏移量valueOffset
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    ……
    /**
     * 這是JDK1.8中的實現
     * Atomically increments by one the current value.
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    ……
}

通過以上程式碼的演示可以看到,AtomicInteger類中定義了一個value屬性,並通過unsafe.objectFieldOffset方法獲取到了這個屬性在主存中設定的偏移量valueOffset。接著就可以在getAndIncrement方法中直接使用unsafe.getAndAddInt的方式,通過偏移量valueOffset將value屬性的值加“1”。但是該方法的實現在JDK1.8之前的版本中,實現程式碼卻是這樣的:

// 獲取偏移量valueOffset的程式碼類似,這裡就不再展示了
……
public final int getAndIncrement() {
    // 一直迴圈,直到
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}
……
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
……

程式碼中採用的就是基於Unsafe的“樂觀鎖”進行實現的,後文中我們還會講解java的自旋鎖以及自旋鎖的一種具體實現方式“樂觀鎖”,還會分析“樂觀鎖”適合使用的場景。在以上程式碼中,getAndIncrement方法內部會不停的迴圈,直到unsafe.compareAndSwapInt方法執行成功。但多數情況下,迴圈只會執行一次,因為多執行緒強佔同一物件屬性的情況並不是隨時都會出現。

2-2-4. 其它典型方法

在JDK1.8中Unsafe還有一些其它實用的原子操作方法:

  • PutXXXXX(Object, long, short)

    類似的方法包括:putInt(Object, long, int)、putBoolean(Object, long, boolean)、putShort(Object, long, short)、putChar(Object, long, char)、putDouble(Object, long, double)等,這些都是針對指定物件中在偏移量上的屬性值,進行直接設定。這些操作發生在CPU一級快取(L1) 或者二級快取(L2)中,但是這些方法並不保證工作在其它核心上的執行緒“立即看到”最新的屬性值。

  • putXXXXXVolatile(Object, long, byte)

    類似的方法包括:putByteVolatile(Object, long, byte)、putShortVolatile(Object, long, short)、putFloatVolatile(Object, long, float)、putDoubleVolatile(Object, long, double)等等,這些方法的主要作用雖然也是直接針對偏移量改變指定物件中的屬性值,但是這些方法保證工作在其它核心上的執行緒能“立即看到”最新的屬性值——也就是說這些方法滿足volatile語義(後續文章會詳細介紹volatile的詳細工作原理)

===============(接下文)