阅读 393

从ReentrantLock到AQS源码

从ReentrantLock到AQS源码

大家都知道在JDK1.6之前synchronized这个重量级锁器性能一直都是较为低下,虽然在1.6之后进行了大量的锁的优化策略,但是使用起来没有Lock灵活,例如获取锁与释放锁的可操作性,可中断性,超时获取锁等;在性能方面synchronized优化之后基本上和Lock扯平;在1.8之后ConcurrentHashMap用CAS和Synchronized取代了ReentrantLock,从1.8之后对ConcurrentHashMap的优化就可以看的出来对synchronized还是很重视的,毕竟是亲儿子;话不多说开始我们的源码之旅:

在源码之前我们先了解一下ReentrantLock和AQS(AbstractQueuedSynchronizer)的关系!

image-20210513150735399

我们可以看到JUC包下的工具基本上都是基于AQS来实现的;,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等。而这些类的底层实现都依赖于AbstractQueuedSynchronizer这个类!可想而知这个AQS是多么的重要!

ReentrantLock实现了一个内部类Sync,该内部类继承了AbstractQueuedSynchronizer,所有锁机制的实现都是依赖于Sync内部类,也可以说ReentrantLock的实现就是依赖于AbstractQueuedSynchronizer类

public class ReentrantLock implements Lock, java.io.Serializable {
   
    private final Sync sync;
		//....
}
复制代码
image-20210513151516301

我们点到为止,大致知道这个情况就可以,方便后面的理解;

一般我们使用ReentrantLock时如下:

private Lock lock = new ReentrantLock(); 
public void method() {
        try {
            lock.lock(); //获得锁
            System.out.println("执行一些操作。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();//释放锁
        }
    }
复制代码

这里需要注意的是在初始化ReentrantLock的时候在构造方法中什么都没有传,使用的是默认的构造方法!也就是非公平锁!当我们传入ture时实例化的是公平锁;

//非公平锁    
public ReentrantLock() {
      sync = new NonfairSync();
    }

//公平锁 
public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
    }

复制代码

公平锁与非公平锁的区别

公平锁

下面我们先看看在源码中公平锁和非公平锁实现区别!接下来我们继续跟踪源码,我们先看公平锁的实现:

final void lock() {
    acquire(1);
}
复制代码

上面调用lock.lock()方法之后就进入了AQS(AbstractQueuedSynchronizer)类中的acquire(1)方法!并将1传入进去;我们看看acquire(1)方法中做了什么?到这里就来到了AQS中!

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
复制代码

我们看到首先是调用了tryAcquire()方法!这句话就是尝试去加锁!我们这里先不关心acquireQueued()方法!我们点进去tryAcquire()方法查看发现这个方法AQS自身是没有实现的!而是需要子类自己去实现,这就能看的出来AQS使用的是模板设计模式!点进去只有我们就来到了ReentrantLock自己实现的tryAcquire()方法中:记住我们先看的是公平锁(FairSync)的实现:

 protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
复制代码
  • 首先是获取当前线程,拿到state这个用volatile修饰的int变量,这个变量的目的很简单,0表示没任何线程持有锁,大于0说明有线程持有锁!这里判断是否等于0,假设这时只有一个线程T1过来尝试加锁,这时state就是等于0的,因为目前为止没有任何地方给state赋值(int类型默认是初始是0);所以这个判断会进去,进到这个方法里面之后首先执行了hasQueuedPredecessors()方法,这个方法就是判断自己是否需要排队!记住这个方法,这也是和非公平锁的一个区别之处!

这里我们看下这个方法的实现:

    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
复制代码

这个方法是AQS实现的,讲到这里我们第一次看到Node,我们还没有给Node赋值!所以到这里无论是tail还是head都是null;所以判断h!=t这个是返回是false;这个hasQueuedPredecessors()方法返回是false那上面的!hasQueuedPredecessors()方法返回就是true;那就紧接着执行了compareAndSetState(0, acquires)方法!对,你猜的没有错这个就是CAS操作!将当前T1的线程的状态从0设置为1,表示T1线程加锁成功!到这里我们先不向下分析了,我们这里的重点是放在公平锁和非公平锁的区别上,上面我们讲解了公平锁的实现,其实非公平锁和公平锁的实现差不多,我们来看下:

非公平锁

 final void lock() {
      if (compareAndSetState(0, 1))
           setExclusiveOwnerThread(Thread.currentThread());
        else
           acquire(1);
        }

复制代码

如果是非公平锁再调用lock.lock()方法的时候,我们在这里就看到了与公平锁的区别之处,这里上来就是调用CAS操作将自己的状态设置为持有锁!如果成功那就加锁成功,如果失败就是同样调用acquire(1)方法!如果抢占锁成功就没有acquire(1)方法啥事了,但是我们还是得去看一下acquire(1)方法里面的实现!

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
复制代码

tryAcquire(arg)方法同样需要子类去实现,那我们就看看ReentrantLock这个子类怎么实现的非公平锁,

 protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
复制代码

在非公平锁里面的tryAcquire()方法中调用的是nonfairTryAcquire(acquires)方法!废话不多说直接进去看看:

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;
        }
复制代码

呦!似曾相识不;和公平锁的实现上大同小异!同的我们就不看了!我们看看异在哪里?

  • 同样上来就是获取当前线程,然后获取state的状态值;我们同样假设只有T1线程,这里state的值为0;然后直接执行了compareAndSetState(0, acquires)方法!还记得上面这块公平锁的实现吗?公平锁的实现多了一步操作就是看自己是否需要去队列中排队;但是非公平锁不用去判断!非公平锁不会去关心队列中有多少在排队的;自己先尝试去加锁;如果加锁成功就设置当前持有锁的线程为自己!如果CAS失败这个nonfairTryAcquire()就会返回false!然后再去执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg),将自己添加到队列中的操作!到这里我们就看出来公平锁与非公平锁的区别了!

重入锁实现原理

我们知道synchronizedReentrantLock都是可以重入的,都是属于重入锁!那重入锁怎么实现的呢?我们就以ReentrantLock为例看看(因为我们看不到synchronized的源码!)

我们上面介绍公平锁与非公平锁其实已经贴出了重入锁实现的代码:

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;
        }
复制代码

重入锁的实现就是在else if 里面;上面我们假设只有T1一个线程过来,这里我们还是假设只有T1线程但是T1线程就不止有一个了,例如下面:

public static void main(String[] args) {

        Lock lock = new ReentrantLock(true);
        for (int i = 0; i < 3; i++) {

            lock.lock();

            System.out.println("执行一些操作。。。。。" + i);
        }
        for (int i = 0; i < 3; i++) {
            lock.unlock();
        }

    }
复制代码

这个时候代码就会进行执行的流程是:

  • 当第一个T1过来时,state等于0,CAS加锁成功!
  • 当第二个T1过来时,因为第一个T1还没有释放锁,还没有执行unlock()方法!所以这个时候 getState()方法返回的不是0而是1,所以就会进入到else if 中进行判断;因为都是T1线程current == getExclusiveOwnerThread()执行结果为true;然后将state的值加1;
  • 以此类推,当第三个T1进来的时候和第二个T1流程是一样的;

总结

用流程图解释一下上面讲述的公平锁,非公平锁,重入锁的整体流程:

image-20210513122505463

这只是AQS的冰山一角;后面我们还没有继续深入讲解ReentrantLock的解锁过程,AQS的独占锁的源码实现,共享锁的源码实现,可响应中断,不可响应中断的区别;

文章分类
后端
文章标签