AbstractQueuedSynchronizer详解

来源:互联网 发布:js验证时间格式 编辑:程序博客网 时间:2024/06/13 21:18

一、简介

AbstractQueuedSynchronizer队列同步器,简称AQS。是用来构建锁或者其他同步组中的基础框架。它使用了一个int成员变量status来表示同步状况。通过CAS对status进行操作来确保状态的改变是安全的。对于CAS不了解的可以看这个文章点击打开链接。通过内置FIFO(First In First out)队列来完成资源获取线程的排队工作。可以说这个框架在锁与并发中大有用处,需要好好理解。

二、AQS和Synchronized

AQS和Synchronized对于同步问题,采用的是两种不同的机制。

首先我们看一下Synchronized的原理,我们将下面的一段使用了Synchronized的代码

public class SyDemo {    public SyDemo(){        haha();    }    public void haha(){        synchronized (this) {            System.out.println("Method 1 start");        }    }}

通过编译后得到下面:


通过上述可以看出Synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码需要关联到一个监视对象,当线程执行monitorenter指令时,需要首先获得监视对象的锁,这里监视对象锁就是进入同步块的凭证,只有获得了凭证才可以进入同步块,当线程离开同步块时,会执行monitorexit指令,释放对象锁。

而在 AQS 同步中,使用一个 int 类型的变量 state 来表示当前同步块的状态。以独占式同步(一次只能有一个线程进入同步块)为例,state 的有效值有两个 0 和 1,其中 0 表示当前同步块中没有线程,1 表示同步块中已经有线程在执行。当线程要进入同步块时,需要首先判断 state 的值是否为 0,假设为 0,会尝试将 state 修改为 1,只有修改成功了之后,线程才可以进入同步块。注意上面提到的两个条件:
1、state 为 0,证明当前同步块中没有线程在执行,所以当前线程可以尝试获得进入同步块的凭证,而这里的凭证就是是否成功将 state 修改为 1(在 synchronized 同步中,我们说的凭证是对象锁,但是对象锁的最终实现是否和这种方式类似,没有找到相关的资料)
2、成功将 state 修改为 1,通过使用 CAS 操作,我们可以确保即便有多个线程同时修改 state,也只有一个线程会修改成功。关于 CAS 的具体解释会在后面提到。
当线程离开同步块时,会修改 state 的值,将其设为 0,并唤醒等待的线程。所以在 AQS 同步中,我们说线程获得了锁,实际上是指线程成功修改了状态变量 state,而线程释放了锁,是指线程将状态变量置为了可修改的状态(在独占式同步中就是置为了 0),让其他线程可以再次尝试修改状态变量。在下面的表述中,我们说线程获得和释放了锁,就是上述含义, 这与 synchronized 同步中说的获得和释放锁的含义不同,需要区别理解。

三、基本使用

AQS 的设计是基于模板方法的,使用者需要继承 AQS 并重写指定的方法。在后续的流程中,AQS 提供的模板方法会调用重写的方法。一般来说,我们需要重写的方法主要有下面 5 个:

方法名描述protected boolean tryAcquire(int arg)独占式获取锁,实现该方法需要查询当前状态并判断同步状态是否和预期值相同,然后使用 CAS 操作设置同步状态protected boolean tryRelease(int arg)独占式释放锁,实际也是修改同步变量protected int tryAcquireShared(int arg)共享式获取锁,返回大于等于 0 的值,表示获取锁成功,反之获取失败protected boolean tryReleaseShared(int arg)共享式释放锁protected boolean isHeldExclusively()判断调用该方法的线程是否持有互斥锁

在自定义的同步组件中,我们一般会调用 AQS 提供的模板方法。AQS 提供的模板方法基本上分为 3 类: 独占式获取与释放锁、共享式获取与释放锁以及查询同步队列中的等待线程情况。下面是相关的模板方法:

方法名描述void acquire(int)独占式获取锁,如果当前线程成功获取锁,那么方法就返回,否则会将当前线程放入同步队列等待。该方法会调用重写的 tryAcquire(int arg) 方法判断是否可以获得锁void acquireInterruptibly(int)和 acquire(int) 相同,但是该方法响应中断,当线程在同步队列中等待时,如果线程被中断,会抛出 InterruptedException 异常并返回。boolean tryAcquireNanos(int, long)在 acquireInterruptibly(int) 基础上添加了超时控制,同时支持中断和超时,当在指定时间内没有获得锁时,会返回 false,获取到了返回 truevoid acquireShared(int)共享式获得锁,如果成功获得锁就返回,否则将当前线程放入同步队列等待,与独占式获取锁的不同是,同一时刻可以有多个线程获得共享锁,该方法调用 tryAcquireShared(int)acquireSharedInterruptibly(int) 与 acquireShared(int) 相同,该方法响应中断tryAcquireSharedNanos(int, long)在 acquireSharedInterruptibly(int) 基础上添加了超时控制boolean release(int)独占式释放锁,该方法会在释放锁后,将同步队列中第一个等待节点唤醒boolean releaseShared(int)共享式释放锁Collection getQueuedThreads()获得同步队列中等待的线程集合  

四、实战演练

1、用代码实现独占锁(排它锁)即Lock锁,独占锁就是说在某个时刻内,只能有一个线程持有独占锁,只有持有锁的线程释放了独占锁,其他线程才可以获取独占锁,代码如下:

import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.AbstractQueuedSynchronizer;import java.util.concurrent.locks.Condition;/** * Created by Xi on 2017/9/5. */public class MutexLock {    // 通过继承 AQS,自定义同步器    private static class Sync extends AbstractQueuedSynchronizer {        /**         * 尝试获取锁         * @param arg         * @return         */        @Override        protected boolean tryAcquire(int arg) {            // 只有当 state 的值为 0,并且线程成功将 state 值修改为 1 之后,线程才可以获得独占锁            if(compareAndSetState(0,1)){                setExclusiveOwnerThread(Thread.currentThread());//设置当前的独占线程                return true;            }            return false;        }        @Override        protected boolean tryRelease(int arg) {            // state 为 0 说明当前同步块中没有锁了,无需释放            if(getState()==0){                throw new IllegalMonitorStateException();            }            // 将独占的线程设为 null            setExclusiveOwnerThread(null);            // 将状态变量的值设为 0,以便其他线程可以成功修改状态变量从而获得锁            setState(0);            return true;        }        //当前线程是否被独占        @Override        protected boolean isHeldExclusively() {            return getState()==1;        }        final ConditionObject newCondition() {            return new ConditionObject();        }    }    // 将操作代理到 Sync 上    private final Sync sync = new Sync();    /**     * 加锁     */    public void lock(){        sync.acquire(1);    }    /**     * 如果当前线程未被中断,则获取锁,否则抛出异常     */    public void lockInterruptibly() throws InterruptedException {        sync.acquireInterruptibly(1);    }    /**     * 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。     * @return     */    public boolean tryLock(){        return sync.tryAcquire(1);    }    /**     * 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。     * @param time     * @param unit     * @return     * @throws InterruptedException     */    public boolean tryLock(long time, TimeUnit unit)throws InterruptedException{        return sync.tryAcquireNanos(1,unit.toNanos(time));    }    /**     * 试图释放此锁     */    public void unlock(){        sync.release(1);    }    /**     *获取Condition来实现线程的休眠与唤醒功能     * @return     */    public Condition newCondition(){        return sync.newCondition();    }    /**     * 查询是否有些线程正在等待获取此锁。     * @return     */    public boolean hasQueeuedThreads(){        return sync.hasQueuedThreads();    }    /**     * 查询此锁是否由任意线程保持。     * @return     */    public boolean isLocked(){        return sync.isHeldExclusively();    }    static final MutexLock lock = new MutexLock();    public static int num=0;    public static void addNum(){        lock.lock();        for(int i=0;i<5;i++){            ++num;            System.out.println(Thread.currentThread().getName()+"执行,num值为:"+num);            try {                TimeUnit.MILLISECONDS.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        lock.unlock();    }    /**     * 测试自定义互斥锁     */    public static void startMutex() {        System.out.println("start mutex: ");        for(int i=0;i<5;i++){            new Thread(){                @Override                public void run() {                    super.run();                    addNum();                }            }.start();        }    }}

调用startMutex打印日志如下:


我们看到使用了 Mutex 之后,多个线程 不会再交替执行,而是当一个线程执行完,另外一个线程再执行。

2、用代码实现共享锁,我们定义两个共享资源,即同一时间内允许两个线程同时执行。我们将同步变量的初始状态 state 设为 2,当一个线程获取了共享锁之后,将 state 减 1,线程释放了共享锁后,将 state 加 1。状态的合法范围是 0、1 和 2,其中 0 表示已经资源已经用光了,此时线程再要获得共享锁就需要进入同步序列等待。

import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.AbstractQueuedSynchronizer;import java.util.concurrent.locks.Condition;/** * Created by Xi * 自定义共享锁 */public class SharedLock {    // 通过继承 AQS,自定义同步器    private static class Sync extends AbstractQueuedSynchronizer {        public Sync(int resourceCount) {            if (resourceCount <= 0) {                throw new IllegalArgumentException("resourceCount must be larger than zero.");            }            // 设置可以共享的资源总数            setState(resourceCount);        }        @Override        protected int tryAcquireShared(int reduceCount) {            // 使用尝试获得资源,如果成功修改了状态变量(获得了资源)            // 或者资源的总量小于 0(没有资源了),则返回。            for (; ; ) {//自旋                int lastCount = getState();//2  1  0                int newCount = lastCount - reduceCount;//2-1=1   0   -1                if (newCount < 0 || compareAndSetState(lastCount, newCount)) {                    return newCount;                }            }        }        @Override        protected boolean tryReleaseShared(int returnCount) {            // 释放共享资源,因为可能有多个线程同时执行,所以需要使用 CAS 操作来修改资源总数。            for (; ; ) {                int lastCount = getState();                int newCount = lastCount + returnCount;                if (compareAndSetState(lastCount, newCount)) {                    return true;                }            }        }    }    // 定义两个共享资源,说明同一时间内可以有两个线程同时运行    private final Sync sync = new Sync(2);    public void lock() {        sync.acquireShared(1);    }    public void lockInterruptibly() throws InterruptedException {        sync.acquireInterruptibly(1);    }    public boolean tryLock() {        return sync.tryAcquireShared(1) >= 0;    }    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {        return sync.tryAcquireNanos(1, unit.toNanos(time));    }    public void unlock() {        sync.releaseShared(1);    }    public Condition newCondition() {        throw new UnsupportedOperationException();    }    static final SharedLock lock = new SharedLock();    public static int num=0;    public static void addNum(){        lock.lock();        for(int i=0;i<5;i++){            ++num;            System.out.println(Thread.currentThread().getName()+"执行,num值为:"+num);            try {                TimeUnit.MILLISECONDS.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        lock.unlock();    }    /**     * 测试函数     */    public static void startShared(){       for(int i=0;i<5;i++){            new Thread(){                @Override                public void run() {                    super.run();                    addNum();                }            }.start();        }    }}

打印日志如下:


通过日志可以看到两个线程同时执行,都去给num加1。


原创粉丝点击