排它式AQS(EXCLUSIVE模式)

排它式AQS(EXCLUSIVE模式)

CLHLock是自旋锁,不支持阻塞,AQS支持。
和CLHLock一样,AQS有一个头节点引用和一个尾节点引用,每当一个线程过来请求锁,就会创建一个节点,节点和线程绑定,然后插入到尾节点。

通过源码分析阻塞式AQS

通过ReentrantLock分析AQS的阻塞锁。顺便说一下ReentrantLock这个名字,从名字可以看出,这个类默认是支持重入的(重入是指已经获取到锁的线程,可以再次获取锁)
public synchronized void a(){
b();
}
public synchronized void b(){

}
例如上面的方法a和b,上面都加了synchronized关键字,当a方法获得锁之后,再去调用方法b(也要获取锁),如果不支持重入,那么程序此时就死了,因为锁已经被他自己占用了,所以ReentrantLock一定会支持锁的重入。
首先我们先来回顾下,ReentrantLock的一般使用方式。

public class ReentrantLockTest {

	    public static void main(String[] args) throws InterruptedException {
	        ReentrantLock lock = new ReentrantLock(true);
	        for (int i = 0; i < 5; i++) {
	            new Thread(new Runnable() {
	                @Override
	                public void run() {
	                    try {
	                        lock.lock();
	                        System.out.println(new Date() + ":我获取到锁了");
	                        TimeUnit.SECONDS.sleep(1);
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    } finally {
	                        lock.unlock();
	                    }
	                }
	            }).start();
	        }
	    }
	}
  • 第一步定义ReentrantLock对象。
  • 在线程内部使用lock()方法获取锁。
  • 在finally内调用unLock()释放锁。
    上面程序打印出的结果显示:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xW3iiIhm-1587126216157)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-5.png)]
    程序每隔1秒打印输出一次。

lock的内部实现

ReentrantLock要获取锁就要调用lock()方法,首先我们来看下lock方法的实现。

 public void lock() {
	        sync.lock();
	    }

再来看一下unLock的实现

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

所有的加锁解锁的方法都是通过一个叫做sync的对象实现。
我们来看一下ReentrantLock的内部接口[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LwKf2cdE-1587126216161)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-6.png)]
从类结构可以看出ReentrantLock支持公平锁和非公平锁,两者的区别后面再说。
sync对象继承AbstractQueuedSynchronizer。
我们来看下sync.lock()的内部实现。
首先看公平锁的实现。

final void lock() {
	            acquire(1);
	        }

很简单就一句话,调用AQS的acquire方法。
acquire的具体实现

public final void acquire(int arg) {
	        if (!tryAcquire(arg) &&
	            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	            selfInterrupt();
	    }
  1. 调用tryAcquire方法,试图获取锁
  2. 当获取不到锁,执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    接着往下走看tryAcquire的实现。
 protected boolean tryAcquire(int arg) {
	        throw new UnsupportedOperationException();
	    }

竟然是一个空实现,说明此处需要子类实现此方法,既然需要子类实现此方法,为什么不写成抽象方法呢,当我们了解了AQS的实现全貌就全明白了。
这里我们看下ReentrantLock中公平锁FairSync的tryAcquire实现。

/**
	         * Fair version of tryAcquire.  Don't grant access unless
	         * recursive call or no waiters or is first.
	         */
	        protected final boolean tryAcquire(int acquires) {
	            final Thread current = Thread.currentThread();
	            int c = getState();
	            if (c == 0) {//首先通过变量state判断锁是否被占用,0代表未被占用
	                if (!hasQueuedPredecessors() &&
	                    compareAndSetState(0, acquires)) {//hasQueuedPredecessors判断队列中是否有其他线程在等待,如果没有线程在等待,设置锁的状态为1(被占用),因为获取锁的过程是多线程同时获取的,所以需要使用CAS。
	                    setExclusiveOwnerThread(current);//设置占用排它锁的线程是当前线程
	                    return true;
	                }
	            }
	            else if (current == getExclusiveOwnerThread()) {//当前锁已被占用,但是占用锁的是当前线程本身
	//还记得我们上面说过ReentrantLock是支持重入的,下面就是重入的实现,当已经获取锁的线程每多获取一次锁,state就执行加1操作
	                int nextc = c + acquires;
	                if (nextc < 0)
	                    throw new Error("Maximum lock count exceeded");
	                setState(nextc);
	                return true;
	            }
	            return false;
	        }

当获取到锁的时候返回true,当获取不到锁的时候返回false。
我们再来看下AQS中acquire方法的实现,当获取不到锁就执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
在了解acquireQueued方法之前,先看下addWaiter方法的实现。

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;
	        }
	    }
	    enq(node);
	    return node;
	}
  1. 判断尾节点是否存在,如果存在,则直接将新节点插入到尾节点之后,然后修改尾节点指向。
  2. 如果尾节点不存在,则执行enq方法,该方法是一个死循环,保证新增的节点一定会被加入到队列中
private Node enq(final Node node) {
	    for (;;) {
	        Node t = tail;
	        if (t == null) { // Must initialize
	            if (compareAndSetHead(new Node()))
	                tail = head;
	        } else {
	            node.prev = t;
	            if (compareAndSetTail(t, node)) {
	                t.next = node;
	                return t;
	            }
	        }
	    }
	}

此时AQS队列的结构如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rXDMnc9-1587126216168)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-7.png)]
addWaiter保证了,节点一定会被插入到队列中。
接着看acquireQueued的实现

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())//锁获取失败,首先,第一次循环设置当前节点的前一个节点的waitStatus为SIGNAL(待通知),第二次循环执行parkAndCheckInterrupt,挂起当前线程LockSupport.park(this);
	                interrupted = true;
	        }
	    } finally {
	        if (failed)
	            cancelAcquire(node);
	    }
	}

此时AQS队列的结构如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oADWf0oz-1587126216178)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-8.png)]
如果再有第三个线程过来,则AQS队列结构如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4X1qeSVb-1587126216186)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-9.png)]
acquireQueued主要做了两件事

  1. 使当前节点的上一个节点的状态为SIGNAL状态.
  2. 阻塞当前节点的线程

unLock的内部实现

unLock内部也是通过sync对象来实现的,具体实现如下:

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

relese方法是AQS的方法

public final boolean release(int arg) {
	    if (tryRelease(arg)) {//判断是否能释放锁
	//释放下一个线程,删除原头结点的指向
	        Node h = head;
	        if (h != null && h.waitStatus != 0)
	            unparkSuccessor(h);
	        return true;
	    }
	    return false;
	}
  1. 在获取锁的时候,我们知道tryAcquire是一个空方法,需要子类去实现。可以猜测tryRelease方法应该也是空方法。
protected boolean tryRelease(int arg) {
	    throw new UnsupportedOperationException();
	}

果不其然,是空方法,我们来看下ReentrantLock对tryRelase的重写。

protected final boolean tryRelease(int releases) {
	    int c = getState() - releases;//锁的重入次数减1
	    if (Thread.currentThread() != getExclusiveOwnerThread())
	        throw new IllegalMonitorStateException();
	    boolean free = false;
	    if (c == 0) {
	        free = true;
	        setExclusiveOwnerThread(null);
	    }
	    setState(c);
	    return free;
	}

此时AQS的数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uzVQhcOI-1587126216195)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-10.png)]
此时仅仅能说明,第二个线程可以去获取锁,但是并不能代表它已经获取到锁,因为头结点并没有变。
当第二个线程的阻塞状态被释放后,acquireQueued方法

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;
	}

此时AQS的数据结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ewCZHnUe-1587126216202)(http://oi7y6lfs5.bkt.clouddn.com/DraggedImage-11.png)]

公平锁与非公平锁

上面介绍的是公平锁的实现,那么非公平锁呢,其实很简单,我们看下ReentrantLock的非公平lock实现。

final void lock() {
	    if (compareAndSetState(0, 1))//直接试图获取锁,如果获取不成功,再放入队尾
	        setExclusiveOwnerThread(Thread.currentThread());
	    else
	        acquire(1);
	}

非公平锁无非是在获取锁的时候,先去尝试获取一次,如果获取不到再加入到队尾,等待执行。而公平锁则是,直接放到队尾,等待执行。

阻塞式锁总结

  1. 当新增一个线程时,实际上是在队列尾部新增加一个节点,调用LockSupport.park方法。
  2. 当唤醒一个线程时,实际上是删除head所指向的节点,调用LockSupport.unPark方法。
  3. LockSupport内部是通过UNSAFE来实现,UNSAFE是本地方法。
  4. 阻塞式的特点是,同一个时刻只能有一个线程能获取到执行权,当该线程执行完,才会通知下一个节点执行。所以ReentrantLock的unLock方法必须在finally中,否则一旦执行体中抛出异常,导致unLock方法无法执行,队列的下一个节点就不会得到通知,因此整个线程队列都得不到执行。

你可能感兴趣的:(java,多线程)