JUC-AQS入门

1. 简介

AQS是AbstractQueuedSynchronizer的简写,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)。

2. 工作原理

AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

aqs.png
  • 使用Node实现FIFO队列,可以用于构建锁或者其它同步装置的基础框架。
  • 利用了一个int类型表示状态。
  • 使用方法是继承,子类通过继承并通过实现它的方法管理其状态{ acquire 和release }的方法操纵状态。
  • 可以同时实现排它锁和共享锁模式(独占、共享)。

3.AQS组件

3.1 CountDownLatch

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。


CountDownLatch.png
@Slf4j
public class CountDownLatchExample1 {
    private final static int threadCount = 200;
    public static void main(String[] args) throws Exception {
        ExecutorService exec = Executors.newCachedThreadPool();
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        //countDownLatch.await(100,TimeUnit.MILLSECONDS),设定等候时间
        countDownLatch.await();
        log.info("finish");
        exec.shutdown();
    }
    private static void test(int threadNum) throws Exception {
        Thread.sleep(100);
        log.info("{}", threadNum);
        Thread.sleep(100);
    }
}
3.2 Semaphore

Semaphore负责协调各个线程,以保证它们能够正确、合理的使用公共资源,也是操作系统中用于控制进程同步互斥的量。Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。

Semaphore主要方法:

//构造方法,创建具有给定许可数的计数信号量并设置为非公平信号量
Semaphore(int permits)
//构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量
Semaphore(int permits,boolean fair)
//从此信号量获取一个许可前线程将一直阻塞。相当于一辆车占了一个车位
void acquire()
//从此信号量获取给定数目许可,在提供这些许可前一直将线程阻塞。比如n=2,就相当于一辆车占了两个车位
void acquire(int n)
//尝试获取许可,停车场有车位就进入,没有就走
tryAcquire()
//释放一个许可,将其返回给信号量。就如同车开走返回一个车位
void release()
//释放n个许可
void release(int n)
//当前可用的许可数
int availablePermits()

下面一起看看如何使用Semaphore

@Slf4j
public class SemaphoreExample1 {
    private final static int threadCount = 20;
    public static void main(String[] args) throws Exception {
        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(3); 获取多个许可
                    //semaphore.tryAcquire() 尝试获取许可
                    //semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS) 尝试等待获取许可
                    semaphore.acquire(); // 获取一个许可
                    test(threadNum);
                    semaphore.release(); // 释放一个许可
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }
    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}
3.3 CyclicBarrier

CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。


CyclicBarrier.png
@Slf4j
public class CyclicBarrierExample3 {
    private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
        //此处代码在barrier满足条件时优先执行
        log.info("callback is running");
    });
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }
    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        barrier.await();
        log.info("{} continue", threadNum);
    }
}
3.4 ReetrantLock

,JDK6.0版本之后synchronized 得到了大量的优化,二者性能也不分伯仲,但是重入锁是可以完全替代synchronized关键字的。除此之外, ReetrantLock又自带一系列高逼格BUFF:可中断响应、锁申请等待限时、公平锁。另外可以结合Condition来使用,使其更是逼格满满。

ReentrantLock与Synchronized区别

  • 可重入性(都可重入)
  • 锁的实现:Synchronized是依赖于JVM实现的,而ReentrantLock是依赖于程序实现。
  • 性能区别:在JDK5.0版本之前, ReetrantLock的性能远远好于synchronized关键字,但是随着锁的不断优化(自旋锁、轻量级锁、偏向锁),两者性能也差不太多。在两者都能满足需求的情况,更推荐使用synchronized,简单。
  • 功能区别:Synchronized的使用便于ReentrantLock,并且它是由编译器是保证锁的加锁和释放的,而ReentrantLock是由我们自己控制的;第二点锁定粒度与灵活度,明显ReentrantLock优于Synchronized。
  • ReentrantLock独有功能:1,可指定公平锁或非公平锁。2,提供了一个Condition类,可以分组唤醒需要唤醒的线程。3,提供能够中断等待锁的线程的机制,lock.lockInterruptibly()。

Condition的使用:Condition可以非常灵活的操作线程的唤醒,下面是一个线程等待与唤醒的例子,其中用1234序号标出了日志输出顺序

public static void main(String[] args) {
    ReentrantLock reentrantLock = new ReentrantLock();
    Condition condition = reentrantLock.newCondition();//创建condition
    //线程1
    new Thread(() -> {
        try {
            reentrantLock.lock();
            log.info("wait signal"); // 1
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("get signal"); // 4
        reentrantLock.unlock();
    }).start();
    //线程2
    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();
}
3.5 ReentrantReadWriteLock读写锁

在没有任何读写锁的时候才可以取得写入锁(悲观读取,容易写线程饥饿),也就是说如果一直存在读操作,那么写锁一直在等待没有读的情况出现,这样我的写锁就永远也获取不到,就会造成等待获取写锁的线程饥饿。 平时使用的场景并不多。

public class LockExample3 {
    private final Map map = new TreeMap<>();
    private final 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 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 {
            writeLock.unlock();
        }
    }
    class Data {}
}
3.6 StampedLock

StampedLock是Java8引入的一种新的锁机制,可以认为它是读写锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观锁策略。如果有大量的读线程,他也有可能引起写线程的饥饿。而StampedLock则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。

@Slf4j
@ThreadSafe
public class LockExample5 {
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;
    public static int count = 0;
    private final static StampedLock lock = new StampedLock();
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }
    private static void add() {
        long stamp = lock.writeLock();
        try {
            count++;
        } finally {
            lock.unlock(stamp);
        }
    }
}
3.7 Callable、Future、FutureTask

线程的创建方式中有两种,一种是实现Runnable接口,另一种是继承Thread,但是这两种方式都有个缺点,那就是在任务执行完成之后无法获取返回结果,于是就有了Callable接口,Future接口与FutureTask类的配合取得返回的结果。

@FunctionalInterface
public interface Callable {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

该接口声明了一个名称为call()的方法,同时这个方法可以有返回值V,也可以抛出异常。
下面看下Future接口定义

public interface Future {
    //如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执    
   //行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停 
   //止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执 
   //行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经/ 
   //完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中 
   //断执行中的线
    boolean cancel(boolean mayInterruptIfRunning);
    //如果任务完成前被取消,则返回true
    boolean isCancelled();
    //如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true
    boolean isDone();
    //获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成
    V get() throws InterruptedException, ExecutionException;
    //获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限      
    //制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

通过方法我们可知Future提供了3种功能:(1)能够中断执行中的任务(2)判断任务是否执行完成(3)获取任务执行完成后的结果。
下面我们再看下FutureTask的定义

public class FutureTask implements RunnableFuture {
}
public interface RunnableFuture extends Runnable, Future {
    void run();
}

FutureTask除了实现了Future接口外还实现了Runnable接口,
通过上面的介绍,我们对Callable,Future,FutureTask都有了比较清晰的了解了,那么它们到底有什么用呢?我们前面说过通过这样的方式去创建线程的话,最大的好处就是能够返回结果,加入有这样的场景,我们现在需要计算一个数据,而这个数据的计算比较耗时,而我们后面的程序也要用到这个数据结果,那么这个时Callable岂不是最好的选择?我们可以开设一个线程去执行计算,而主线程继续做其他事,而后面需要使用到这个数据时,我们再使用Future获取不就可以了吗?下面我们就来编写一个这样的实例。

@Slf4j
public class FutureExample {
    static class MyCallable implements Callable {
        @Override
        public String call() throws Exception {
            log.info("do something in callable");
            Thread.sleep(5000);
            return "Done";
        }
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future future = executorService.submit(new MyCallable());
        log.info("do something in main");
        Thread.sleep(1000);
        String result = future.get();
        log.info("result:{}", result);
    }
}

下面我们用FutureTask再次实现类似功能

@Slf4j
public class FutureTaskExample {

    public static void main(String[] args) throws Exception {
        FutureTask futureTask = new FutureTask(new Callable() {
            @Override
            public String call() throws Exception {
                log.info("do something in callable");
                Thread.sleep(5000);
                return "Done";
            }
        });
        new Thread(futureTask).start();
        log.info("do something in main");
        Thread.sleep(1000);
        String result = futureTask.get();
        log.info("result:{}", result);
    }
}
3.8 Fork/Join框架

Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。此处我们不做过多的说明,其原理是基于工作窃取算法,指某个线程从其他队列里窃取任务来执行。下面我们以一个demo来介绍如何使用

@Slf4j
public class ForkJoinTaskExample extends RecursiveTask {
    public static final int threshold = 2;
    private int start;
    private int end;
    public ForkJoinTaskExample(int start, int end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected Integer compute() {
        int sum = 0;

        //如果任务足够小就计算任务
        boolean canCompute = (end - start) <= threshold;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任务大于阈值,就分裂成两个子任务计算
            int middle = (start + end) / 2;
            ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
            ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);

            // 执行子任务
            leftTask.fork();
            rightTask.fork();

            // 等待任务执行结束合并其结果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();

            // 合并子任务
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkjoinPool = new ForkJoinPool();

        //生成一个计算任务,计算1+2+3+4
        ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);

        //执行一个任务
        Future result = forkjoinPool.submit(task);
        try {
            log.info("result:{}", result.get());
        } catch (Exception e) {
            log.error("exception", e);
        }
    }
}
3.9 Queue

在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列,无论哪种都集成自Queue。


类结构.png

ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。

BlockingQueue接口:

  • ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,其内部没有实现读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列
  • LinkedBlockingQueue:基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持一个数据缓冲队列(由列表构成),LinkedBlockingQueue之所以能够高效的处理并发数据,是因为内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作完全并发,是一个无界队列。
  • SynchronousQueue:一种没有缓冲的队列,生产者生产的数据直接会被消费者获取并消费。
  • PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步得锁采用的是公平锁,它是一个无界的队列。
  • DelayQueue:带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素,DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、任务超时处理、空闲链接的关闭等等。

你可能感兴趣的:(JUC-AQS入门)