Java併發編程之AQS原理

AQS框架基本特性與結構


Java併發包當中的大多數同步器實現都是圍繞着共同的基礎行爲,比如等待隊列、獨佔獲取、共享獲取等,而這個行爲的抽象就是基於AbstractQueuedSynchronize簡稱AQS。AQS是一個抽象同步框架,可以用來實現一個依賴狀態的同步器。

AQS核心


AQS核心有三點:

  • 自旋:控制線程不跳出邏輯
  • LockSupport類中有park(線程阻塞)、unpark(喚醒)方法,阻塞自旋的線程,出讓資源
  • CAS:保證狀態修改的原子性

AQS中的數據結構——節點和同步隊列


在這裏插入圖片描述
AQS有2個重要組成部分:
1、state同步狀態,int類型
當state的值等於0的時候,表明沒有線程佔用它。當state>0時,表示有線程在佔用它,新來的線程需要被加入到同步隊列中。
state的訪問方式有三種:

getState()
setState(int newState)
compareAndSetState(int expect, int update)
private volatile int state;
//返回當前的同步狀態
protected final int getState() {
    return state;
}
//設置同步狀態
protected final void setState(int newState) {
    state = newState;
}
//以CAS的方式設置當前狀態
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2、一個同步隊列
AQS維持着一個同步隊列,它是一個雙向鏈表,每一個節點都是一個Node,代表着一個線程。

static final class Node {
    //標記,表示節點正在共享模式下等待
    static final Node SHARED = new Node();
    //標記,表示節點正在獨佔模式下等待
    static final Node EXCLUSIVE = null;

     //waitStatus值,表明線程已取消
     static final int CANCELLED =  1;
     //waitStatus值,表明此節點釋放或取消,當前節點需要喚醒下一個節點。
    static final int SIGNAL    = -1;
    //waitStatus值,表明當前節點位於條件隊列中,它不會作爲同步隊列的節點。
    static final int CONDITION = -2;
    //waitStatus值,表明下一個共享式同步狀態將被無條件傳播
    static final int PROPAGATE = -3;

    volatile int waitStatus;
    
    //前驅節點
    volatile Node prev;
    //後繼節點
    volatile Node next;
    //此節點持有的線程
    volatile Thread thread;
    //等待隊列中的後繼節點,如果當前線程是共享的,那麼該字段是SHARED
    Node nextWaiter;

    //如果節點在共享模式下等待,則返回true
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    //返回上一個節點,如果爲空,則拋出異常
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    
    //用於建立初始頭部或共享標記
    Node() {    
    }
    
    //被addWaiter使用
    Node(Thread thread, Node mode) {     
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    //被Condition使用
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

同步隊列由一個個節點構成,同步器擁有首節點和尾節點,假設有一個線程獲取了鎖,在這個線程沒有釋放鎖之前,其他線程都將作爲一個Node被加入到同步隊列的尾部,這時候需要通過CAS保證其原子性,線程需要傳遞當前線程認爲的尾節點和當前節點,只有設置成功後,當前節點纔會被加入到隊列的尾部。
節點加入到同步隊列:
在這裏插入圖片描述
隊列的首節點有且只有一個,因此不需要通過CAS去設置首節點,當隊頭釋放鎖後,會喚醒後面的節點,並出隊,後繼節點將會在獲取同步狀態成功後將自己設置爲首節點。
首節點的變化:
在這裏插入圖片描述

同步狀態


AQS定義了兩種同步狀態,一種是獨佔式同步狀態,一種是共享式同步狀態。
獨佔式:只有單個線程能夠成功獲得同步狀態並執行
共享式:多個線程可以成功獲得同步狀態並執行

獨佔式同步狀態獲取


獨佔式獲取同步狀態的方法是acquire(int arg)

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//嘗試以獨佔模式獲取
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
//創建節點並通過enq方法加入到同步隊列尾部
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //以自旋的方式通過CAS將當前節點加入到隊尾
    enq(node);
    return node;
}
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

首先該方法會通過tryAcquire方法嘗試獲取同步狀態,如果獲取失敗,就會調用addWaiter方法構造一個獨佔式(Node.EXCLUSIVE)的節點,並利用CAP方式不斷自旋,直到將該節點加入到同步隊列尾部,通過acquireQueued方法進行自旋,直到獲取同步狀態。
在這裏插入圖片描述

獨佔式同步狀態釋放


同步器通過release方法進行同步狀態的釋放,該方法會喚醒頭結點的後繼節點線程。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

共享式同步狀態的獲取


共享式同步狀態的獲取的方法是acquireShare。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
//參數獲取鎖
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

通過tryAcquireShared方法嘗試獲取鎖,如果返回值大不小於0,表示能夠獲得鎖,如果返回值小於0,就需要通過doAcquireShared不斷自旋獲得鎖。

共享式同步狀態的釋放


共享式同步狀態的釋放同步狀態的方法是releaseShared。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

瞭解LockSupport工具


作用:LockSupport可以阻塞一個線程、喚醒一個線程、它是構建同步組件的基礎工具。
它提供了一系列的靜態方法包括park開頭的方法和unpark(Thread thread)方法分別用來阻塞和喚醒一個線程。

Condition分析


AQS的一個內部類ConditionObject是Condition的一個實現類,每一個Condition包含一個等待隊列,這個等待隊列是一個單鏈表,在每一個Condition中都會有兩個指針,一個指向頭節點,一個指向尾節點。
在這裏插入圖片描述

同步隊列與等待隊列


一個鎖可以包含有多個Condition,每一個Condition上面可能都會有多個線程進入等待狀態。
在這裏插入圖片描述

節點在隊列之間的移動


當一個線程調用await方法時,節點將從同步隊列移動到等待隊列中。
在這裏插入圖片描述
當一個線程調用signal方法時,當前Condition中的同步節點的線程就會被喚醒,該線程就會嘗試去競爭鎖,如果這個鎖被其他線程所持有,那麼當前線程就會再次被加入到同步隊列的尾部。
在這裏插入圖片描述

實現一個獨佔鎖


我們創建一個自定義鎖的類MyLock,實現Lock接口,在類中定義內部類Sync繼承抽象類AbstractQueuedSynchronizer,我們需要重寫其tryAcquire方法和tryRelease方法,因爲這兩個方法中是直接拋出了異常,同時,爲了實現newCondition方法,我們直接使用抽象類中已經實現了ConditionObject類

public class MyLock implements Lock {
	//實現獨佔鎖功能
	private class Sync extends AbstractQueuedSynchronizer {
		//獲取鎖
		@Override
		protected boolean tryAcquire(int arg) {
			int state = getState();
			if (state == 0) { //證明能夠獲得鎖
				//利用CAS原理修改state
				if(compareAndSetState(0, arg)) {
					//設置當前線程佔有資源
					setExclusiveOwnerThread(Thread.currentThread());
					return true;
				}
			}
			return false;
		}
  
		//釋放鎖
		@Override
		protected boolean tryRelease(int arg) {
			int state = getState() - arg;
			if (state == 0) { //判斷釋放後state是否爲0
				setExclusiveOwnerThread(null);
				setState(state);//設置state爲0
				return true;
			}
			//存在線程安全嗎?no,因爲釋放鎖表明你已經獨佔了這個鎖
			setState(state);//設置當前state的值
			return false;
		}
		public Condition newConditionObject() {
			return new ConditionObject();
		}
	}
 
	private Sync sync = new Sync();
	
	@Override
	public void lock() {
		sync.acquire(1);
	}

	@Override
	public void lockInterruptibly() throws InterruptedException {
		//以中斷的方式獲得鎖
		sync.acquireInterruptibly(1);
	}

	@Override
	public boolean tryLock() {
		return sync.tryAcquire(1);
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit)
			throws InterruptedException {
		return sync.tryAcquireNanos(1, unit.toNanos(time));
	}

	@Override
	public void unlock() {
		sync.release(1);
	}

	@Override
	public Condition newCondition() {
		return sync.newConditionObject();
	}
}

測試我們所實現的鎖

public class Example01 {
	private MyLock lock = new MyLock();
	public static void main(String[] args) {
		final Example01 ex = new Example01();
		//創建兩個線程,執行out方法
		new Thread(()->{
                  ex.out();
            }).start();
            new Thread(()->{
                 ex.out();
           }).start();
	}
	public void out() {
		lock.lock();
		System.out.println("進入");
		try {
			TimeUnit.SECONDS.sleep(5);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		lock.unlock();//需要使用finally
		System.out.println("出去");
	}
}

好像可以完成我們加鎖的工作,不過,我們所實現的鎖並不是可重入鎖,下面的代碼就會出現問題。
我們創建兩個方法

public void method1() {
	lock.lock();
	System.out.println("method1");
	method2();
	lock.unlock();//需要使用finally
}
public void method2() {
	lock.lock();
	System.out.println("method2");
	lock.unlock();//需要使用finally
}

直接使用一個線程調用method1

new Thread(()->{
     ex.method1();
}).start();

我們發現,線程進入了method1,但是卻進入不了method2。
這是因爲我們獲取鎖時重寫的tryAcquire方法,將state從0變爲了arg,當我們再次訪問tryAcquire方法,state不等於0,表明有線程佔有,其直接給我們返回了false,當前線程正在等待自己釋放鎖。
我們需要對tryAcquire方法進行修改,使其支持可重入

protected boolean tryAcquire(int arg) {
	int state = getState();
	if (state == 0) { //證明能夠獲得鎖
		//利用CAS原理修改state
		if(compareAndSetState(0, arg)) {
			//設置當前線程佔有資源
			setExclusiveOwnerThread(Thread.currentThread());
			return true;
		}
	} else if(getExclusiveOwnerThread() == Thread.currentThread()) {
		//判斷進入線程是不是還是當前線程
		setState(getState() + arg);//不存在線程安全
		return true;
	}
	return false;
}

當state不等於0時,我們還需要判斷,當前所佔用鎖的線程是否與現在進行tryAcquire調用的線程是同一個線程,如果是的話,那麼返回爲true,同時其state也要在原來state的基礎上增加arg。
對於釋放鎖的方法,我們並不需要去修改。

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