JUC包下的AQS --- 队列同步器

JUC包下的AQS — 队列同步器

AQS简介

  AQS,即AbstractQueuedSynchronizer,在java.util.concurrent.locks包下面。AQS是用来构建锁和同步器的框架,基于AQS可以简单高效的开发出适合自己的同步器。
  ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的,AQS可以认为是JUC的核心。

可重入锁非可重入锁

  在讲解原理之前,我们先弄清楚两个概念,可重入锁非可重入锁。可重入锁指的是当一个线程获取锁之后,可以再次获取该锁,从而避免死锁。
  ReentrantLock和synchronized都是可重入锁,AQS中共享变量state的初始值为0。对于可重入锁,当一个线程尝试获取锁时,会判断state是否等于0,等于0则可以获取锁,并将state置为1,若state值不是0,会判断获取锁的线程是否是占有锁的线程,如果是则执行state++。释放锁时,同样的也会执行state–,直到state等于0,线程才会真正释放锁。
  对于非可重入锁,当一个线程尝试获取锁时,判断state值是不是0,是0则可以获取锁,不是0则进入阻塞状态,如果是同一个线程就会产生死锁。

乐观锁和悲观锁

悲观锁
  总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
  
乐观锁
  总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

这篇博客对于乐观锁和悲观锁写的非常详细。
面试必备之乐观锁与悲观锁

AQS原理

  1.使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架。
  2.使用一个int类型表示状态。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

  3.子类通过继承并通过实现它的方法管理其状态{acquire和release}的方法操纵状态。
  4.可以同时实现排它锁和共享锁模式(独占、共享),指的是实现它的子类可以选择独占或者共享,不是说可以选择独占和共享。

Exclusive(独占): 只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁。   公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。
  非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。
Share(共享): 多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、CyclicBarrier、ReadWriteLock。

  AQS的核心思想是: 当共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
  并且只有head节点的直接后继节点可以请求获取锁,失败则将自己阻塞直到获取成功。

AQS同步组件

CountDownLatch

  在程序执行过程中,可能需要满足某些条件才可以继续执行后续的操作,所以JDK1.5之后引入了该类。

在这里插入图片描述

//调用该方法的线程会被暂时挂起,直到count值减到0,才会被唤醒继续执行。
public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}

//和await()方法用法相同,可以设置时间,当一段时间后没有达到条件也会被继续执行
public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

//令count值减一
public void countDown() {
        sync.releaseShared(1);
}

  CountDownLatch可以使一个线程等待其他线程各自执行完毕后再执行,但是不支持count值进行重置,如果业务上有要求的话,可以考虑使用CyclicBarrier。

Semaphore(信号量)

  可以控制某个资源可以被同时访问的线程个数,下面这是两个比较重要的方法。

//获得一个许可
public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}

//方法可以传入permits参数,代表依次获取多个,同样可以依次释放多个
public void acquire(int permits) throws InterruptedException {
        if (permits < 0) throw new IllegalArgumentException();
        sync.acquireSharedInterruptibly(permits);
}

//释放一个许可
public void release() {
        sync.releaseShared(1);
}

例子:

private static int threadCount = 20;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    //获取许可
                    semaphore.acquire();
                    test(threadNum);
                    //释放许可
                    semaphore.release();
                } catch (InterruptedException e) {
                    log.info("exception",e);
                }
            });
        }
        log.info("finish");
        exec.shutdown();
    }

    private static void test(int threadNum) throws InterruptedException {
        log.info("{}",threadNum);
        Thread.sleep(1000);
	}

  从这个例子可以看出,执行结果是3个3个出现的,对应了new Semaphore(3)将信号量设置为3。

Semaphore还有一个比较重要的方法:tryAcquire()

//尝试获取信号量,在特定时间获取不到则放弃执行
public boolean tryAcquire(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

CyclicBarrier

  CyclicBarrier和CountDownLatch用法很相似,只不过CountDownLatch执行的是减一操作,CyclicBarrier执行的是加一操作。可以实现一组线程相互等待,直到到达某个条件才会继续执行,当一个线程调用await()方法时,就会进入阻塞状态,直到达到临界值才会唤醒所有等待的线程继续执行。
  比如可以对多个目标进行相加的操作时,需要等待每个线程都操作完毕时再进行统一相加,就可以使用CyclicBarrier来实现。相对的,CyclicBarrier的临界值可以循环使用。

在这里插入图片描述

//可以设置等待时间,超过时间就算没有到达指定条件,依然会执行	
public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
}

例子:

private static CyclicBarrier barrier = new CyclicBarrier(5);

    public static void main(String[] args) throws InterruptedException {

        ExecutorService exec = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            exec.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.info("Exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        try {
            barrier.await(2000, TimeUnit.MILLISECONDS);
        } catch (BrokenBarrierException  e) {
            log.info("BarrierException", e);
        }
        log.info("{} continue", threadNum);
    }

  需要注意的是: 设置时间可能会抛出BrokenBarrierException异常,如果希望下面的代码继续执行,就需要将异常捕获而不让其抛出。
  BrokenBarrierException这个异常的意思就是,当某个等待的线程被中断,其它等待的线程会抛出这个异常。

//这样做可以在达到条件时,先执行这里的代码
private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
        log.info("callback is running");
});

ReentrantLock(可重入锁)

  ReentrantLock和synchronized的区别:

① 两者都是可重入锁
② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
③ReenTrantLock 比 synchronized 增加了一些高级功能
  1.ReenTrantLock提供了一种能够中断等待锁的线程的机制,synchronized 则不能。
  2.ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  3.ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现选择性通知

在synchronized可以实现的情况下,还是比较推荐使用synchronized。

我们来看一下部分源码
//无参构造方法,默认给了一个不公平锁
public ReentrantLock() {
        sync = new NonfairSync();
}
//有参构造方法,可以自己设着公平或非公平
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

//尝试获取锁,如果没有被其它线程锁定,则获取锁
//也可以传入时间,代表一段时间后,锁没有被其它线程锁定,则获取锁
public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
}

例子:

//声明一个锁
private final static Lock lock = new ReentrantLock();

//切记解锁一定要放在finally中,防止出现死锁的情况
private static void add() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
}

ReentrantReadWriteLock(读写锁)

  在没有任何读写锁的时候,才可以获得写锁,这实际上是用的悲观锁的设计思想。所以在读操作频繁,写操作不频繁的情况下,有可能会造成饥饿现象。比如,一个线程希望进行写操作,但是由于写操作很频繁,写进程就会一直等待。

用法:
	private final Map<String, Data> map = new TreeMap<>();

    private final static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key){
        readLock.lock();
        try{
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }

    public Set<String> getAllKeys(){
        readLock.lock();
        try{
            return map.keySet();
        }finally {
            readLock.unlock();
        }
    }

    public Data put(String key,Data value){
        writeLock.lock();
        try{
            return map.put(key,value);
        }finally {
            readLock.unlock();
        }
    }

    class Data{

    }

Condition

  前面在介绍AQS时,我们知道AQS实际上是通过内置的FIFO队列来完成获取资源线程的排队工作。
  实际上在这个队列下面还存在着一个Condition队列,暂时可以将其理解为一个假死队列,调用方法进入该队列的线程就像是死掉了一样,但是它是在等待一个时机,条件达到的时候可以调用方法将其唤醒继续执行。
在这里插入图片描述

	public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();

        new Thread(() ->{
            try{
                reentrantLock.lock();
                log.info("wait signal");//1
                condition.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            log.info("get signal");//4
            reentrantLock.unlock();
        }).start();

        new Thread(() -> {
            reentrantLock.lock();
            log.info("get lock");//2
            try{
                Thread.sleep(3000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            condition.signalAll();
            log.info("send signal");//3
            reentrantLock.unlock();
        }).start();
    }

  代码中的的注释代表的是执行的顺序,当线程执行condition. wait()之后,该线程就会进入Condition队列,并且相当于执行了unlock()进行了解锁。
  当另一个线程尝试获取该线程时可以获取,当执行condition.signalAll()之后,就会唤醒Condition队列中所有的线程,当然也可以一个一个的唤醒,之后继续执行。
  实际上,用到Condition的场景很少,这里当做了解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章