java模板模式与AQS实现

本博客先简单看下模板模式,然后解析下java中的AQS是如何使用模板模式的,最后使用AQS自定义一种锁。

模板模式

顾名思义,本模式旨在"套模板",跟写PPT时套模板道理一样,PPT模板事先给好布局、图片、配色等,用户添加自己的内容即可。模板模式则是父类事先准备好一些函数框架,子类(用户类)继承父类并实现自己的功能即可。UML图如下:
在这里插入图片描述
父类为虚类,其中execute方法伪代码如下:

private void execute(){
	other logic.....
	methodOne();
	other logic.....
	other logic.....
	methodTwo();
	other logic.....
	methodThree();
	other logic.....
}

即execute方法内容是固定的,子类不可复写,但是methodOne等方法是protected的,子类可以复写,这就达到一种目的:父类核心流程不能改,但是具体实现可以由子类实现。

AQS

全称:AbstractQueuedSynchronizer(队列同步器),以下基于jdk12分析,某些方法已不同于java7、8
它是一种机制,用数字表示资源个数,用一个队列存储想获取资源的线程信息,并通过各种三种方式使线程竞争资源。
这三种方式是

  1. 独占资源:一次仅能有一个线程访问资源
  2. 共享资源:一次能有多个线程访问资源
  3. 超时独占资源:与独占资源一样,只不过支持超时放弃

首先来看资源控制:

    /**
     * The synchronization state.
     */
    private volatile int state;

注释中说state为同步状态,AQS子类通过set和get这个变量控制同步状态,可以视为资源个数,下图可以看到getState方法被AQS子类调用。为volatile修饰,保证了内存可见性。
在这里插入图片描述
存储线程信息的队列,这里称作同步队列吧,是一个双向链表,节点信息如下:
在这里插入图片描述
关键信息图中已标注,节点类型分为SHARED共享型和EXCLUSIVE排他(独占)型,节点状态有CANCELLED、SIGNAL等,当然还有当前线程本身
下面分三种情况分析线程竞争情况:

情况一:独占资源

AQS中的模板方法为:

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

此方法定义了一种流程,子类不可复写,子类需要做的是复写tryAcquire方法,即获取资源(设置state变量)的方式,如下可以看到这个方法默认抛出异常,即子类要想实现独占锁,这个方法必须复写。后文简单介绍可重入锁的实现,即基于此方法。

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

从acquire方法看到,如果tryAcquire失败了,即没有当前线程没有获取到资源,会先调用addWaiter方法,即向同步队列中添加一个包含当前线程的节点:

    private Node addWaiter(Node mode) {
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;     //1
            if (oldTail != null) {   //2
                node.setPrevRelaxed(oldTail); 	//3
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }

可以看到是一个自旋添加的过程,tail为成员变量,即同步队列中的末尾,为volatile修饰,只有通过CAS机制判断当前tail符合预期值时才会将新建的node添加到末尾。这里的CAS是不断获取队列末尾,看这个末尾是否与经过123操作后的末尾是否一致,因为用的volatile修饰,内存可见性得到保证,便防止了其他线程对tail的修改,CAS本质上是一种忙则等待的调度策略CAS一般称为CompareAndSwap,最终调用Unsafe的相关方法,其实jdk12已经不这么叫了,这是jdk8及以前的叫法,jdk12称为CompareAndSet,最终调用VarHandle的相关方法,但是道理是一样的,都是拿内存中最新值与预期值对比,如果一致则赋予变量另一个新值。

addWaiter成功后各个线程调用accquiredQueued方法进入自旋状态:

    final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

这里需要注意两点,一是只有当前节点的前驱为头节点时才可尝试获取资源(tryAcquire),保证队列的先进先出嘛,
二是一个细节,p.next = null; 这个操作使得p的后继标记为空,帮助GC完成垃圾收集。对象标记为null来促进GC在大部分情况下是错误的!因为编译器会对这个操作进行优化,赋值为null其实大部分情况下是被干掉的,没有意义,但这里是正确的,后续可能更新博客详细解释。

至此,一个线程可以顺利获取资源,如果获取不到则添加到同步队列里,一旦自旋轮到自己就可以获取到资源。可是还没释放呢,总不能自己占着不然别人用吧。下边是释放资源相关逻辑:

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

同样tryRelease是子类必须复写的方法,否则直接抛异常。释放资源(修改同步状态state)成功后,会执行下边的核心方法unparkSuccessor:

    private void unparkSuccessor(Node node) {
        ……………其他逻辑……………
        /*
         * 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 p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

大部分情况会执行LockSupport.unpark(s.thread);注释也说道如果s为空了,则从后向前遍历得到一个非空的继任者,但最终都是调用LockSupport.unpark(s.thread);这个方法作用是唤醒某个线程,在release这个情景下就是唤醒下一个等待的线程。注意这里release后没有直接删除相应节点,删除节点在上述acquireQueued方法中。

至此一个独占式锁完成。大体流程如下:
在这里插入图片描述

情况二:共享式资源

共享式与独占式核心区别在于,前者可以同时有多个线程访问同步状态state,后者仅能有一个线程访问,与独占锁类似,AQS提供如下模板:

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared为子类复写,注意这里不同的是返回数值小于0后才执行doAcquireShared,意为,共享资源可以有多个线程同时访问资源。

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        } finally {
            if (interrupted)
                selfInterrupt();
        }
    }

与独占类似,也是添加节点,然后进入自旋,如果前驱是头节点则尝试获取资源,不在赘述。
释放资源最终也是调用unparkSuccessor方法,与独占锁一致。

情况三:独占超时

与独占模型一样,只不过加上超时控制:

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                //核心
                if (nanosTimeout <= 0L) {
                    cancelAcquire(node);
                    return false;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }

核心在于加入nanosTimeout字段用于判断自旋状态是否超时,超时则取消请求,其他逻辑与独占类似。

AQS在jdk中的应用之一:重入锁

重入锁,顾名思义,可以重新进入的锁,通俗来讲就是说,当一个线程获取到锁以后,该线程又请求同样的锁,可重入就是允许这种情况发生,不可重入就是不允许同一个线程请求同一个锁。常见的锁基本都是可重入锁,比如synchronized关键字其实是可重入的。java中有ReentrantLock,即为基于AQS的可重入锁。
在这里插入图片描述
ReentrantLock分为公平锁和非公平锁,下边分别来看。
首先可重入的前提是同步状态的意义,这时state字段不是资源数目,而是当前线程获得此资源的次数,当一个线程获取到此资源时,state加一,释放一次时减一,当state为0时说明没有线程占用此资源。

非公平锁
所谓非公平锁,关键点在于当一个线程请求资源时先不必关心AQS中的同步队列,如下代码,当资源没有被占用时,当前线程直接执行CAS操作,如果成功,则此资源被当前线程占用,并把当前线程设为“owner thread”。如果state不是0时,先进行判断,当前线程是否是“owner thread”,不是则不往下进行,tryAcquire返回失败,进行AQS的addWaiter以及后续操作,这便实现了独占。如果当前是“owner thread”,则state加acquires,实现计数(一般是+1),这里没有必要使用CAS,因为是单线程
随后可以执行释放资源,即进行state减,见下面代码,不在赘述。

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

        @ReservedStackAccess
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

公平锁
与非公平锁不同的地方在于获取资源部分,即tryAcquire,如下代码,比非公平多了一个hasQueuedPredecessors方法的判断,其余都一样。

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

继续看hasQueuedPredecessors这个方法干了什么:

    public final boolean hasQueuedPredecessors() {
        Node h, s;
        if ((h = head) != null) {
            if ((s = h.next) == null || s.waitStatus > 0) {
                s = null; // traverse in case of concurrent cancellation
                for (Node p = tail; p != h && p != null; p = p.prev) {
                    if (p.waitStatus <= 0)
                        s = p;
                }
            }
            if (s != null && s.thread != Thread.currentThread())
                return true;
        }
        return false;
    }

这个方法是AQS类的方法,不可复写,目的在于查看同步队列中有无前驱节点,如果没有前驱,则说明当前线程前边没有别的线程尝试获取资源,当前线程才会进行CAS操作尝试获取资源。如果有前驱,当前线程不会尝试获取资源,而是执行addWaiter以及后续操作,主动进入同步队列。
所以,非公平锁和公平锁区别就在于,当某线程执行tryAcquire时,非公平锁不关心同步队列,直接尝试获取资源,成功了就获取到资源了,失败了才进入到同步队列,而公平锁首先会检查同步队列是不是有节点,如果没有才尝试获取资源,如果有节点则直接进入同步队列排队。通俗一点就是非公平锁没素质,直接插队,公平锁直接排队,到我了才获取资源。

AQS在jdk中的应用之二:Semaphore(信号量)

Semaphore是用来控制并发线程数的一种机制。先看下java的注释:

 * A counting semaphore.  Conceptually, a semaphore maintains a set of
 * permits.  Each {@link #acquire} blocks if necessary until a permit is
 * available, and then takes it.  Each {@link #release} adds a permit,
 * potentially releasing a blocking acquirer.
 * However, no actual permit objects are used; the {@code Semaphore} just
 * keeps a count of the number available and acts accordingly.
 *
 * <p>Semaphores are often used to restrict the number of threads than can
 * access some (physical or logical) resource.

大意是Semaphore持有一些许可,这些许可并不是真正的对象,而只是一个数字在记录,它可以用来限制并发的线程数。

比如有50个线程,但是同时并发执行的线程仅能有10个,其他线程处于阻塞状态。换一种方式解释,就是说,仅有10个资源,却有50个线程想获取资源,当然只能一部分一部分地获取,比如0~9号线程获取到了资源,开始执行各自的逻辑,假如3号线程执行完毕,会释放一个资源,同时唤醒阻塞队列第一个节点,使得该节点的线程得以访问资源。这就是共享锁啊!查看java源码发现,确实是基于AQS实现的共享锁。

        final int getPermits() {
            return getState();
        }

        final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

        protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

可以看到,permit(许可)其实就是AQS的state变量,在这里指代资源数量,与可重入锁一样,也区分公平和非公平,原理也一致。这里贴的是非公平锁的代码,可以看到tryAcquire方法试图获取资源,获取到则占用,获取不到则进入AQS的addWaiter逻辑。release为释放资源,都是对state这个数的操作,与前边描述一致,不再赘述。

基于AQS的自定义锁的实现

自定义锁的实现大体流程:确定实现独占还是共享锁 --> 继承AQS -->实现acquire和release方法;
如下自定义了一个共享锁,其实是造了个semaphore的简单轮子,支持自定义资源个数:

public class NSourceLock implements Lock {
    private Sync sync;

    public NSourceLock() {
        sync = new Sync(1);
    }

    public NSourceLock(long sourceCount) {
        sync = new Sync(sourceCount);
    }

    /**
     * 写成静态内部类,继承AQS模板类
     */
    private static final class Sync extends AbstractQueuedLongSynchronizer {
        Sync(long sourceCount) {
            if (sourceCount < 1) {
                throw new IllegalStateException("source should be no less than 1");
            }
            //调用AQS方法,设置资源数目
            setState(sourceCount);
        }

        @Override
        protected long tryAcquireShared(long arg) {
            //自旋
            while (true) {
                long cur = getState(); //当前资源数,注意state字段是volatile修饰,这里保证可见性
                long newCount = cur - arg; //请求多少资源,arg为请求资源数
                if (newCount < 0) {
                    return newCount; //分配失败,返回负数,详见AQS注释
                }
                if (compareAndSetState(cur, newCount)) {
                    return newCount; //分配成功
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(long arg) {
            while (true) {
                long cur = getState();
                long newCount = cur + arg;
                if (compareAndSetState(cur, newCount)) {
                    return true;
                }
            }
        }
    }

    /**
     * Lock接口方法
     */
    @Override
    public void lock() {
        sync.acquireShared(1L);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        sync.releaseShared(1L);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

调用:设置5个资源,8个线程共享这5个资源。

        NSourceLock nSourceLock = new NSourceLock(5);
        for (int i = 0; i < 8; i++) {
            new Thread(() -> {
                nSourceLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " lock " + System.currentTimeMillis());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + " unlock " + System.currentTimeMillis());
                    nSourceLock.unlock();
                }

            }, "th" + i).start();
        }

输出:
在这里插入图片描述
输出可看出,首先是0~4号线程获取到资源,大约一秒后0号释放一个资源,5号得以获取一个资源;随后2号释放一个,6号获取一个;随后3号释放一个,7号获取一个;随后相继释放资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值