深入理解AQS同步器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yu280265067/article/details/50929681

AbstractQueuedSynchronizer,是用来构建锁或者其他同步组建的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包作者Doug Lea 期望他能够实现大部分同步需求的基础。

同步器指定方法时,需要使用同步器提供的模版方法。比如如下的三个基础方法。

getState():获取当前同步状态。

setState(int newState): 设置当前同步状态。

compareAndSetState(int expext,int update): 使用CAS设置当前状态,该方法能够保证状态设置的原子性。CAS具体原理参看博主另一篇文章。

AQS是java并发中的一个很重要的知识点,本文通过jdk的源码来详细介绍公平锁,非公平锁,独占锁,共享锁,最后加上一个自定义的共享锁的同步器的代码。

介绍AQS之前,先介绍一下Lock与AQS的关系。首先Lock是java1.5之后推出的支持并发编程的借口,在此之前如锁机制一直都是通过Synchronized关键字来实现的,关于Synchronized的原理参看本人另一博客。那么Lock接口与Synchronized有什么区别呢?Lock提供了和Synchronized关键字类似的作用,只是在使用时需要显示的获取和释放锁,也就是程序员需要自己控制锁的获取与释放。具体方式首先参看jdk中提供的几种锁是如何获取与释放的。首先来看FairSync(公平锁)。

FairSync源码

    
<span style="font-size:14px;">   /**
         * 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) {    //如果状态=0 说明可以获得同步状态
                if (!hasQueuedPredecessors() && //判断前驱节点是否为头结点
                    // CAS修改state 保证原子性(具体CAS原理参看本人另一篇文章)
            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”);
        // 重置state状态
                setState(nextc);
                return true;
            }
            return false;
        }
    }</span>


       从AQS的一个实现类FairSync(公平锁同步器)可以看到,tryAcquire的实现过程。首先判断当前结点线程前驱结点是否为头结点,如果是则CAS修改State状态。
那么接下来再来看看NofairSync。顾名思义既是不公平获取锁(不是FIFO)。先来看看jdk源码。
<span style="font-size:14px;">/**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {// 注意此处与fair不同的地方
                    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;
        }</span>


      通过源码即可一目了然,nofair方法比fair方法少了一个hasQueuedPredecessors() 检查前驱结点是否为头结点的判断。

      说之前介绍一下文章中提到的几个类的关系吧,这也是写完之后才发现类和类的关系比较乱。先从底层的Sync开始说,Sync是继承自内部类AQS的一个实现类,分为FairSync和noFairSync。可以说Sync就是真正的同步器实现类。而Semaphore和Mutex是jdk提供的两种锁————共享锁和独占锁。注意这两个类不适继承自Lock的类。而是jdk封装好了的两个类。这两个类都聚合了AQS同步器。当然了实现有所不同,文中已经通过源码说明。而Lock接口的作用是用来用户自定义同步器的实现。当然完全自定义相当于是自己写jdk了,难度自然是相对大了一些。所以jdk也封装了一系列已经实现好了的类。比如后文提到的ReentrantLock(可重入锁)还有读写锁等更复杂的锁。注意ReentrantLock是拍他锁。
     接下来说说独占锁和共享锁。同样,独占锁既是同一时刻只能有一个线程获取同步状态,共享锁及时多个。还是来看看源码。

      

扫描二维码关注公众号,回复: 4913953 查看本文章
<span style="font-size:14px;">final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

       </span>


       这里拿的是Semaphore中的内部类的方法。Semaphore是在操作系统是信号量的意思,这里可以理解为是jdk提供的一个支持共享锁的一个类。源码可以看出,获取非公平的共享锁非常简单,只需要自旋的检查当前可用锁的数量是否大于0,如果大于则直接获取锁。

   事实上,公平锁的机制往往没有非公平锁高效,但是,并不是任何场合都是以TPS作为唯一的指标,公平锁能够减少饥饿发生的概率,等待越久的请求越是能够得到满足。

   另外,可重入锁和不可重入锁就是在获取锁与释放锁的时候加了一个计数器,用来在释放锁的时候,一个对象重进入几次就释放几次锁。其实Mutex(独占锁)是不支持可重入的,而Synchronized是支持可重入的。
   那么介绍了这么多究竟自定义一个Lock的实现类该怎么做的。
而可重入锁ReentrantLock可重入锁支持获取锁的公平和非公平性选择。ReentrantLock提供了一个构造函数,能够控制锁是否公平。源码如下
 
<span style="font-size:14px;">  /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }</span>



    以上通过源码分析了jdk提供的几种锁的原理以及其中的关系。包括Semaphore和Mutex,ReentrantLock。补充说明一下其中Semaphore和Mutex都是不支持重进入的。那么接下来就尝试一下自己实现一个支持共享的Lock接口吧。
<span style="font-size:14px;">public class TwiceLock implements Lock{
    private final Sync sync = new Sync(3);
    private static final class Sync extends AbstractQueuedSynchronizer{
        Sync(int count){
            if(count<=0){
                throw new IllegalArgumentException("mount must larger than zero");
            }
            setState(count);
        }
        public int tryAcquireShared(int reduceCount){
            for(;;){
                int current = getState();
                int newCount = current - reduceCount;
                if(newCount<0||compareAndSetState(current, newCount)){
                    return newCount;
                }
            }
        }
        
        public boolean tryReleaseShared(int returnCount){
            for(;;){
                int current = getState();
                int newCount = current + returnCount;
                if(compareAndSetState(current, newCount)){
                    return true;
                }
            }
        }
    }
    
    
    public void lock() {
        // TODO Auto-generated method stub
        sync.acquireShared(1);
    }

    public void lockInterruptibly() throws InterruptedException {
        // TODO Auto-generated method stub
        
    }

    public boolean tryLock() {
        // TODO Auto-generated method stub
        return false;
    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO Auto-generated method stub
        return false;
    }

    public void unlock() {
        // TODO Auto-generated method stub
        sync.releaseShared(1);
    }

    public Condition newCondition() {
        // TODO Auto-generated method stub
        return null;
    }
    
}

测试类
public class TwiceTest {
    public static void main(String[] args) throws InterruptedException {
        final Lock lock = new TwiceLock();
        class Worker extends Thread{
            @Override
            public void run() {
                // TODO Auto-generated method stub
            while(true){
                lock.lock();
                    try {
                        sleep(1);
                    System.out.println(Thread.currentThread().getName());
                    sleep(1);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    finally {
                        lock.unlock();
                    }
                }
            }
        }
        for(int i=0;i<10;i++){
            Worker w = new Worker();
            w.setDaemon(true);
            w.start();
        }
        for(int i=0 ;i<10;i++){
            Thread.sleep(1);
            System.out.println();
        }
    }
}</span>



运行测试用例,可以看到线程名称成对输出,也就是在同一时刻只有两个线程能够获得到锁,这表明TwiceLock可以按照预期正确工作。


猜你喜欢

转载自blog.csdn.net/yu280265067/article/details/50929681