Java并发编程之AQS原理

AQS框架基本特性与结构


Java并发包当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronize简称AQS。AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。

AQS核心


AQS核心有三点:

  • 自旋:控制线程不跳出逻辑
  • LockSupport类中有park(线程阻塞)、unpark(唤醒)方法,阻塞自旋的线程,出让资源
  • CAS:保证状态修改的原子性

AQS中的数据结构——节点和同步队列


Java并发编程之AQS原理_第1张图片
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保证其原子性,线程需要传递当前线程认为的尾节点和当前节点,只有设置成功后,当前节点才会被加入到队列的尾部。
节点加入到同步队列:
Java并发编程之AQS原理_第2张图片
队列的首节点有且只有一个,因此不需要通过CAS去设置首节点,当队头释放锁后,会唤醒后面的节点,并出队,后继节点将会在获取同步状态成功后将自己设置为首节点。
首节点的变化:
Java并发编程之AQS原理_第3张图片

同步状态


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方法进行自旋,直到获取同步状态。
Java并发编程之AQS原理_第4张图片

独占式同步状态释放


同步器通过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上面可能都会有多个线程进入等待状态。
Java并发编程之AQS原理_第5张图片

节点在队列之间的移动


当一个线程调用await方法时,节点将从同步队列移动到等待队列中。
Java并发编程之AQS原理_第6张图片
当一个线程调用signal方法时,当前Condition中的同步节点的线程就会被唤醒,该线程就会尝试去竞争锁,如果这个锁被其他线程所持有,那么当前线程就会再次被加入到同步队列的尾部。
Java并发编程之AQS原理_第7张图片

实现一个独占锁


我们创建一个自定义锁的类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。
对于释放锁的方法,我们并不需要去修改。

你可能感兴趣的:(编程历程,Java)