自己動手寫把”鎖”---終極篇

原文鏈接:https://www.cnblogs.com/qingquanzi/p/8274078.html

鎖是整個Java併發包的實現基礎,通過學習本系列文章,將對你理解Java併發包的本質有很大的幫助。

前邊幾篇中,我已經把實現鎖用到的技術,進行了一一講述。這其中有原子性、內存模型、LockSupport還有CAS,掌握了這些技術,即使沒有本篇,你也完全有能力自己寫一把鎖出來。但爲了本系列的完整性,我在這裏還是把最後這一篇補上。

先說一下鎖的運行流程:多個線程搶佔同一把鎖,只有一個線程能搶佔成功,搶佔成功的線程繼續執行下邊的邏輯,搶佔失敗的線程進入阻塞等待。搶佔成功的線程執行完畢後,釋放鎖,並從等待的線程中挑一個喚醒,讓它繼續競爭鎖。

轉變成程序實現:我們首先定一個state變量,state=0表示未被加鎖,state=1表示被加鎖。多個線程在搶佔鎖時,競爭將state變量從0修改爲1,修改成功的線程則加鎖成功。state從0修改爲1的過程,這裏使用cas操作,以保證只有一個線程加鎖成功,同時state需要用volatile修飾,已解決線程可見的問題。加鎖成功的線程執行完業務邏輯後,將state從1修改回0,同時從等待的線程中選擇一個線程喚醒。所以加鎖失敗的線程,在加鎖失敗時需要將自己放到一個集合中,以等待被喚醒。這個集合需要支持多線程併發安全,在這裏我通過一個鏈表來實現,通過CAS操作來實現併發安全。

把思路說清楚了,咱們看下代碼實現。

首先咱們實現一個ThreadList,這是一個鏈表結合,用來存放等待的處於等待喚醒的線程:

複製代碼

public class ThreadList{
private volatile Node head = null;
private static long headOffset;
private static Unsafe unsafe;
static {
try {
Constructor constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]);
constructor.setAccessible(true);
unsafe = constructor.newInstance(new Object[0]);
headOffset = unsafe.objectFieldOffset(ThreadList.class.getDeclaredField(“head”));
}catch (Exception e){
}
}
/**
*
* @param thread
* @return 是否只有當前一個線程在等待
*/
public boolean insert(Thread thread){
Node node = new Node(thread);
for(;😉{
Node first = getHead();
node.setNext(first);
if(unsafe.compareAndSwapObject(this, headOffset,first,node)){
return firstnull?true:false;
}
}
}
public Thread pop(){
Node first = null;
for(;😉{
first = getHead();
Node next = null;
if(first!=null){
next = first.getNext();
}
if(unsafe.compareAndSwapObject(this, headOffset,first,next)){
break;
}
}
return first
null?null:first.getThread();
}
private Node getHead(){
return this.head;
}
private static class Node{
volatile Node next;
volatile Thread thread;
public Node(Thread thread){
this.thread = thread;
}
public void setNext(Node next){
this.next = next;
}
public Node getNext(){
return next;
}
public Thread getThread(){
return this.thread;
}
}
}

複製代碼
加鎖失敗的線程,調用insert方法將自己放入這個集合中,insert方法裏將線程封裝到Node中,然後使用cas操作將node添加到列表的頭部。同樣爲了線程可見的問題,Node裏的thread和next都用volatile修飾。
加鎖成功的線程,調用pop方法獲得一個線程,進行喚醒,這裏邊同樣使用了cas操作來保證線程安全。

接下來在看看鎖的實現:
複製代碼

public class MyLock {
private volatile int state = 0;
private ThreadList threadList = new ThreadList();
private static long stateOffset;
private static Unsafe unsafe;
static {
try {
Constructor constructor = Unsafe.class.getDeclaredConstructor(new Class<?>[0]);
constructor.setAccessible(true);
unsafe = constructor.newInstance(new Object[0]);
stateOffset = unsafe.objectFieldOffset(MyLock.class.getDeclaredField(“state”));
}catch (Exception e){
}

}
public void lock(){
    if(compareAndSetState(0,1)){
    }else{
        addNodeAndWait();
    }
}
public void unLock(){
    compareAndSetState(1,0);
    Thread thread = threadList.pop();
    if(thread != null){
        LockSupport.unpark(thread);
    }
}
private void addNodeAndWait(){
    //如果當前只有一個等待線程時,重新獲取一下鎖,防止永遠不被喚醒。
    boolean isOnlyOne = threadList.insert(Thread.currentThread());
    if(isOnlyOne && compareAndSetState(0,1)){
        return;
    }
    LockSupport.park(this);//線程被掛起
    if(compareAndSetState(0,1)){//線程被喚醒後繼續競爭鎖
        return;
    }else{
        addNodeAndWait();
    }
}
private boolean compareAndSetState(int expect,int update){
    return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
}

}

複製代碼

線程調用lock方法進行加鎖,cas將state從0修改1,修改成功則加鎖成功,lock方法返回,否則調用addNodeAndWait方法將線程加入ThreadList隊列,並使用LockSupport將線程掛起。(ThreadList的insert方法,返回一個boolean類型的值,用來處理一個特殊情況的,稍後再說。)
獲得鎖的線程執行完業務邏輯後,調用unLock方法釋放鎖,即通過cas操作將state修改回0,同時從ThreadList拿出一個等待線程,調用LockSupport的unpark方法,來將它喚醒。

將我們在《自己動手寫把"鎖"—鎖的作用》的例子修改爲如下,來測試下咱們的鎖的效果:
複製代碼

public class TestMyLock {
private static List list = new ArrayList<>();
private static MyLock myLock = new MyLock();
public static void main(String[] args){
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++){
add(i);
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
print();
}
});
t1.start();
t2.start();
}
private static void add(int i){
myLock.lock();
list.add(i);
myLock.unLock();
}
private static void print(){
myLock.lock();
Iterator iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
myLock.unLock();
}
}

複製代碼
ok,正常運行了,不在報錯。

到這裏咱們的一個簡單地鎖已經實現了。接下來我再把上邊的,一個沒講的細節說一下。即如下這段代碼:

boolean isOnlyOne = threadList.insert(Thread.currentThread());
if(isOnlyOne && compareAndSetState(0,1)){
return;
}

ThreadList的insert方法,在插入成功後,會判斷當前鏈表中是否只有自己一個線程在等待,如果是則返回true。從而進入後邊的if語句。這個邏輯的用意就是:如果只有自己一個線程在等待時,則試着通過cas操作重新獲取鎖,如果獲取失敗才進入阻塞等待。它是用來解決以下邊界情況:
在這裏插入圖片描述
在只有線程A和線程B兩個線程的時候,如果沒有以上判斷邏輯,線程B將有可能會永遠處於阻塞不被喚醒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章