我之见--java多线程之可重入锁,读写锁源码分析 及自定义锁AQS

来源:互联网 发布:笑话软件 编辑:程序博客网 时间:2024/06/06 11:56

    ReentrantLock锁是jdk1.5之后加的轻量级锁,相对以前的重量级锁,它有很多的优势。ReentrantLock只支持独占方式的获取操作,它将同步状态用于保存锁获取操作的次数,并且还维护一个owner变量来保存当前所有的线程标识符,只有当线程获取或者释放锁的时候才会修改这个变量。
    
   1. 可重入锁的源码分析:

当我打开的ReentrantLock源码的时候发现它的代码却是非常简单的。总共有三个内部类:第一个抽象内部类Sync (abstract static class Sync extends AbstractQueuedSynchronize) 直接继承AQS 我们可以大概了解一下什么 AQS?   AQS是Java并发类库的基础,其提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需要使用 这个同步器提供的常用方法对状态进行操作;

第二个内部类NonfairSync(非公平锁):非公平锁是直接获取锁,没有维护等待队列.第三个内部类FairSync(公平锁):当遇到阻塞的时候,会把请求锁的进程添加到维护等待队列,下次释放锁的时候会从队列的头节头进行处理。


锁的申请和释放都是成对出现的,我们先来看一下ReentrantLock对常规lock和unlock的处理.对于常规的独占锁,ReentrantLock用0和1 分别表示是否有线程持有锁。0代表没有线程持有锁 ,如果有线程申请锁就会把状态改为1,如果释放锁了,就会把状态改为0;

 

我们来看一下源码:

 lock方法:

  final void lock() {            if (compareAndSetState(0, 1))                setExclusiveOwnerThread(Thread.currentThread());            else                acquire(1);        }
compareAndSetState比较当前的状态是否是0,如果是0的同时,会把当前状态设置成1。如果两个步骤都完成,证明获取锁成功,同时设置进程状态为当前进程。


unlock方法:

 public void unlock() {        sync.release(1);    }
简单的把当前状态减1.


可重入方法trylock:

 public boolean tryLock() {        return sync.nonfairTryAcquire(1);    }
再看nonfairTryAcquire方法:

final boolean nonfairTryAcquire(int acquires) {            final Thread current = Thread.currentThread();            int c = getState();            if (c == 0) {                if (compareAndSetState(0, acquires)) {                    setExclusiveOwnerThread(current);                    return true;                }            }            else if (current == getExclusiveOwnerThread()) {                int nextc = c + acquires;                if (nextc < 0) // overflow                    throw new Error("Maximum lock count exceeded");                setState(nextc);                return true;            }            return false;        }
如果当前锁状态为0,那么直接获取锁并且返回,如果锁的状态不是0,证明有线程持有锁,再比较当前线程与请求线程是否是同一条线程,则会把累加当前持有的进程数,否则获取锁失败。

目前为此,我们大概已经可以明白可重入锁的实现了,主要是借助AQS 框架来实现,后面会再分析AQS。

   2.  读写锁的源码分析:

           读写锁和可重入锁是都是基于AQS 来实现的,所以读写内部还是会有一个Sync类。除此之外还有两个类:ReadLock和WriteLock类,不过这两个类都是用同一个 Sync的,当初没有源码的时候,我以为会有两个Sync类,所以读写锁是用一个AQS子类 同时管理讯读取加锁和写入加锁。AQS在内部维护一个等待线程队列,其中记录了某个线程是独占访问(相当于写)还是共享访问(相当于读)。当锁可用时,如果位于队列头部的线程是执行写入操作,那么线程会得到这个锁,如果位于队列头部的线程是读取访问,那么队列中在第一个写入线程之前的所有线程都将获得这个锁。这是一种没有读或者写优先等待的策略。
  下面简单的分析一下(因为和上面太多相同):
  看一下ReadLock的lock方法:
public void lock() {
            sync.acquireShared(1);
        }
 这里申请的是共享锁。

WriteLock的lock方法:
public void lock() {
            sync.acquire(1);
        }
这里申请的是独占锁。从这里可以看到AQS的强大,下面我们还是重点看一下AQS。

  3. AQS分析   

    前面已经说过了,AQS用一个int来表示的状态,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此AQS提供getState,setState 和compareAndSetState 这三个原子方法.常用的获取锁和释放锁的流程:
获取操作过程如下:
if(尝试获取成功){
    return;
 }else{
     加入等待队列;park自己
}
释放操作:
if(尝试释放成功){
    unpark等待队列中第一个节点
}else{
    return false
}
   在多线程中也必须保证等待队列是线程安全,而且是非阻塞式的。我们来看一下队列的实现:
        Node类里面有分别pre指向上一个节点,next指向下一个节点。同时有SHARED共享和EXCLUSIVE独占两种模式.我们先接着从前面的ReentrantLock的lock 方法分    析,如果没有获取锁就会调用 acquire(1) 方法。我们接着AQS里面的方法:
         
    public final void acquire(int arg) {        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }
      tryAcquire再次尝试获取锁,如果还是失败,就会添加到队列:
    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;    }
首先:判断尾节点是否为空,如果不为空,直接插入到尾部,如果为空则做特殊处理


我们再来看一下:
 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);        }    }
1. 获取当前节点的前驱节点;
需要获取当前节点的前驱节点,而头结点所对应的含义是当前站有锁且正在运行。
2. 当前驱节点是头结点并且能够获取状态,代表该当前节点占有锁;
如果满足上述条件,那么代表能够占有锁,根据节点对锁占有的含义,设置头结点为当前节点。
3. 否则进入等待状态。
如果没有轮到当前节点运行,那么将当前线程从线程调度器上摘下,也就是进入等待状态。

我们再来看一下AQS的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;    }
1. 尝试释放状态;
tryRelease能够保证原子化的将状态设置回去,当然需要使用compareAndSet来保证。如果释放状态成功过之后,将会进入后继节点的唤醒过程。一般由子类实现。
2. 唤醒当前节点的后继节点所包含的线程。
通过LockSupport的unpark方法将休眠中的线程唤醒,让其继续acquire状态。
我们再来看一下unparkSuccessor方法:
 private void unparkSuccessor(Node node) {        /*         * If status is negative (i.e., possibly needing signal) try         * to clear in anticipation of signalling.  It is OK if this         * fails or if status is changed by waiting thread.         */        int ws = node.waitStatus;        if (ws < 0)            compareAndSetWaitStatus(node, ws, 0);        /*         * Thread to unpark is held in successor, which is normally         * just the next node.  But if cancelled or apparently null,         * traverse backwards from tail to find the actual         * non-cancelled successor.         */        Node s = node.next;        if (s == null || s.waitStatus > 0) {            s = null;            for (Node t = tail; t != null && t != node; t = t.prev)                if (t.waitStatus <= 0)                    s = t;        }        if (s != null)            LockSupport.unpark(s.thread);    }
先取出当前节点的那个一个节点,如果为空,则从最后一个节点一直遍历到第一个节点,直到找到阻塞的节点,同时会唤醒该节点.
    
下面我们再看一下利用AQS自定义的锁:

package javaThread;import java.util.concurrent.locks.AbstractQueuedLongSynchronizer;public class MyAQSLock {        private  final Sync sync = new Sync();        public void signal() {        sync.releaseShared(0);    }        public void await() throws InterruptedException {        sync.acquireSharedInterruptibly(0);    }    private class Sync extends AbstractQueuedLongSynchronizer {        @Override        protected long tryAcquireShared(long arg) {            return (getState() ==  1) ? 1 : -1;        }                @Override        protected boolean tryReleaseShared(long arg) {            setState(1);            return true;        }    }}

这里只是一个简单的锁,用0表示关闭,1表示打开. 当调用 await方法的时候,然后会调用 tryAcquireShared方法,如果已经打开了闭锁,那么就允许通过。

0 0