Java并发编程-Java多线程之AQS

AQS

        AbstractQueuedSynchronized

实现说明        

使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架。

利用了一个int类型表示状态;

使用方法是继承;

子类通过继承并通过实现它的方法管理其状态{acquire和release}的方法操纵状态;

可以实现排他锁和共享锁模式(独占、共享)

AQS实现的大致思路

        aqs内部维护了一个CLH队列来管理锁,线程首先会尝试获取锁,如果失败,就将线程以及等待状态的信息包装成一个Node节点,加入到Sync Queue同步队列中,接着会不断循环尝试获取锁,它的条件是,当前节点为head的直接后记才会尝试,如果失败就会阻塞自己,直到自己被唤醒,而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。

AQS同步组件

CountDownLatch

主要提供的机制是多个(具体数量等于初始化CountDownLatch的值)线程都达到预期状态或者完成预期工作是触发的时间,达道自己预期状态的线程会调用CountDownLatch的countDown方法,而等待的线程会调用CountDownLatch的await方法。

Semaphore

信号量,控制并发访问的线程数。控制某个资源可悲同时访问的个数。

CyclicBarrier

CyclicBarrier从字面理解是指循环屏障,它可以协同多个线程,让多个线程在这个屏障前等待,直到所有线程都达到了这个屏障时,再一起继续执行后面的动作。

ReentrantLock

可重入锁。

Condition

Future

可以得到其他线程的返回值。

FutureTask

即可以作为Callable执行,又可以作为Future获取返回值

CountDownLatch

        CountDownLatch类是一个同步工具类,它允许一个或多个线程彼此等待,直到其他线程执行完后再执行后面的代码。

实现原理:

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

         countDownLatch.await()方法保证countDown递减为0。

public class CountDownLatchTest {
    public static int clientTotal = 5000;//请求总数
    public static int threadTotal = 100;//同时并发执行的线程数
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        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 (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                     countDownLatch.countDown();//保证执行
                }
            });
        }
        countDownLatch.await();//阻塞,保证前面的线程执行完,等待计数器减为0
        //countDownLatch.await(10, TimeUnit.MICROSECONDS);//只允许线程等待10毫秒
        System.out.println(count);
        executorService.shutdown();
    }
    private static void add() throws InterruptedException { //线程不安全
        Thread.sleep(100);
        count++;
    }
}

Semaphore

        Semaphore类是一个计数信号量,必须由获取它的线程释放,通常用于限制可以访问某些资源线程数目,信号量控制的是线程并发的数量。

实现原理:

        Semaphore是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。

        如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

public class SemaphoreTest {
    private static final int threadCount = 20;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(3);//控制并发的数量
        for (int i=0; i<threadCount; i++){
            final int num = i;
            executorService.execute(()->{
                try {
                    if(semaphore.tryAcquire()){//尝试获取一个许可
                        test(num);
                        semaphore.release();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
        log.info("finish");
    }
    private static void test(int threadNum) throws InterruptedException {
        log.info("{}",threadNum);
        Thread.sleep(1000);
    }
}

CyclicBarrier

        允许一组线程全部等待彼此达到共同屏障点的同步辅助类。这些线程必须相互等待彼此,每个线程都准备就绪后才能往下执行后续的操作。屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。

实现原理:

        CyclicBarrier.await方法调用CyclicBarrier.dowait方法,每次调用await方法都会使计数器-1,当减少到0时就会唤醒所有的线程。

         await()方法使线程进入等待。

public class CyclicBarrierTest {
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
        log.info("callback is running");//进入屏障之后优先执行
    });
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i=0; i<10; i++){
            final int threadNum = i;
            Thread.sleep(1000);
            executorService.execute(()->{
                try {
                    race(threadNum);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    log.error("exception", e);
                }
            });
        }
    }
    private static void race(int threadNum) throws InterruptedException, BrokenBarrierException {
        Thread.sleep(1000);
        log.info("{} is ready",threadNum);
        cyclicBarrier.await();
        log.info("{} continue",threadNum);
    }
}

CountDownLatch与CyclicBarrier区别:

CountDownLatch的计数器只能使用一次,而CyclicBarrier的的计数器可以使用rset方法重置,循环使用。

CountDownLatch实现一个或多个线程需要等待其他线程完成某项操作后才能继续向下执行(一个或多个线程等待其他线程完成的关系),CyclicBarrier实现了多个线程之间相互等待,直到所有线程都满足条件之后才能执行后续的操作(多个线程内部相互等待)。

CyclicBarrier同时支持Runnable参数,表示屏障结束后优先执行的Runnable中的内容。

ReentrantLock

        ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。

可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

ReentrantLock持有的是对象监视器。

ReentrantLock持有的锁是需要手动去unlock()的。

        ReentrantLock独有的功能:

可以指定锁是公平锁还是非公平锁。

提供了一个Condition类,可以分组唤醒需要唤醒的线程。

提供了能够中断等待锁的线程机制,lock.lockInterruptibly()。

        特点:

ReentrantLock持有的是对象监视器。

公平锁表示线程获取锁的顺序是按照线程排队的顺序来分配的。

非公平锁就是一种获取锁的抢占机制,是随机获得锁的,先来的未必就一定能先得到锁,从这个角度讲,synchronized其实就是一种非公平锁。

        非公平锁的方式可能造成某些线程一直拿不到锁,自然是非公平的了。

        new ReentrantLock的时候有一个单一参数的构造函数表示构造的是一个公平锁还是非公平锁,传入true就可以了(ReentrantLock默认的是非公平锁)。

public class ReentrantLockTest {
    public static int clientTotal = 5000;//请求总数
    public static int threadTotal = 100;//同时并发执行的线程数
    public static int count = 0;
    private static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        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 (InterruptedException e) {
                    e.printStackTrace();
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println(count);
        executorService.shutdown();
    }
    private static void add(){
        lock.lock();
        try{
            count++;
        }finally {
            lock.unlock();
        }
    }
}

ReentrantLock API:

getHoldCount()

返回的是当前线程调用lock()的次数。同一线程的同一个ReentrantLock的lock()方法被调用了多少次,getHoldCount()方法就返回多少。

getQueueLength()

获取正等待获取此锁定的线程估计数。

isFair()

获取此锁是否公平锁。

hasQueuedThread()

查询指定的线程是否正在等待获取指定的对象监视器。

hasQueuedThreads()

查询是否有线程正在等待获取指定的对象监视器。

isHeldByCurrentThread()

对象监视器是否由当前线程保持。

isLocked()

此对象监视器是否由任意线程保持。

tryLock()

在调用try()方法的时候,如果锁没有被另外一个线程持有,那么就返回true,否则返回false。

tryLock(long timeout, TimeUnit unit)

在指定等待时间内获得了锁,则返回true,否则返回false。

tryLock()

只探测锁是否,并没有lock()的功能,要获取锁,还得调用lock()方法。

读写锁

ReentrantReadWriteLock

在没有任何读写锁的情况下才能取得写入的锁。可能会导致写锁的线程饥饿。

读写锁表示两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,称为排他锁。

读和读之间不互斥,因为读操作不会有线程安全问题。

写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题。

读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题。

总结起来就是,多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作

        如果一直有读锁,则可能导致写锁线程饥饿。

public class ReentrantReadWriteLockTest {
    private final Map<String, Data> map = new TreeMap<>();
    private final static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final static Lock readLock = lock.readLock();
    private final static Lock writeLock = lock.writeLock();
    public Data get(String key) {
        readLock.lock();
        try{
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }
    private 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 {
            writeLock.unlock();
        }
    }
    class Data{
    }
}

StamptedLock

public class StampedLockTest{
    class Point {
        private double x, y;
        private final StampedLock sl = new StampedLock();
        void move(double deltaX, double deltaY) { // an exclusively locked method
            long stamp = sl.writeLock();
            try {
                x += deltaX;
                y += deltaY;
            } finally {
                sl.unlockWrite(stamp);
            }
        }
        //下面看看乐观读锁案例
        double distanceFromOrigin() { // A read-only method
            long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
            double currentX = x, currentY = y;  //将两个字段读入本地局部变量
            if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
                stamp = sl.readLock();  //如果没有,我们再次获得一个读悲观锁
                try {
                    currentX = x; // 将两个字段读入本地局部变量
                    currentY = y; // 将两个字段读入本地局部变量
                } finally {
                    sl.unlockRead(stamp);
                }
            }
            return Math.sqrt(currentX * currentX + currentY * currentY);
        }
        //下面是悲观读锁案例
        void moveIfAtOrigin(double newX, double newY) { // upgrade
            // Could instead start with optimistic, not read mode
            long stamp = sl.readLock();
            try {
                while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
                    long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
                    if (ws != 0L) { //这是确认转为写锁是否成功
                        stamp = ws; //如果成功 替换票据
                        x = newX; //进行状态改变
                        y = newY;  //进行状态改变
                        break;
                    } else { //如果不能成功转换为写锁
                        sl.unlockRead(stamp);  //我们显式释放读锁
                        stamp = sl.writeLock();  //显式直接进行写锁 然后再通过循环再试
                    }
                }
            } finally {
                sl.unlock(stamp); //释放读锁或写锁
            }
        }
    }
}

Condition

        synchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型,ReentrantLock同样可以,但是需要借助Condition,且Condition有更好的灵活性,具体体现在:

一个Lock里面可以创建多个Condition实例,实现多路通知。

notify()方法进行通知时,被通知的线程时Java虚拟机随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知,这是非常重要的。

        一个Lock里面可以创建多个Condition实例,实现多路通知。

public class ConditionTest{
    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.await();
            } 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();
    }
}

        notify()方法进行通知时,被通知的线程时Java虚拟机随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知。

        注意要是用一个Condition的话,那么多个线程被该Condition给await()后,调用Condition的signalAll()方法唤醒的是所有的线程。

        如果想单独唤醒部分线程该怎么办呢?new出多个Condition就可以了,这样也有助于提升程序运行的效率。使用多个Condition的场景是很常见的,像ArrayBlockingQueue里就有。

        看一下利用Condition实现等待/通知模型的最简单用法,下面的代码注意一下,await()和signal()之前,必须要先lock()获得锁,使用完毕在finally中unlock()释放锁,这和wait()/notify()/notifyAll()使用前必须先获得对象锁是一样的:

synchronized和ReentrantLock的对比:

synchronized是关键字,就和if...else...一样,是语法层面的实现,因此synchronized获取锁以及释放锁都是Java虚拟机帮助用户完成的,ReentrantLock是类层面的实现,因此锁的获取以及锁的释放都需要用户自己去操作。特别再次提醒,ReentrantLock在lock()完了,一定要手动unlock()。

synchronized简单,简单意味着不灵活,而ReentrantLock的锁机制给用户的使用提供了极大的灵活性。在Hashtable和ConcurrentHashMap中体现得淋漓尽致。synchronized一锁就锁整个Hash表,而ConcurrentHashMap则利用ReentrantLock实现了锁分离,锁的只是segment而不是整个Hash表。

synchronized是不公平锁,而ReentrantLock可以指定锁是公平的还是非公平的。

synchronized实现等待/通知机制通知的线程是随机的,ReentrantLock实现等待/通知机制可以有选择性地通知。

和synchronized相比,ReentrantLock提供给用户多种方法用于锁信息的获取,比如可以知道lock是否被当前线程获取、lock被同一个线程调用了几次、lock是否被任意线程获取等等。

总结起来,我认为如果只需要锁定简单的方法、简单的代码块,那么考虑使用synchronized,复杂的多线程处理场景下可以考虑使用ReentrantLock。当然这只是建议性地,还是要具体场景具体分析的。

Callable、Future和FutureTask

Callable与Runnable的区别

Runnable没有返回值;Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛。

        注意:Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

Callable

        Callable和Runnable差不多,两者都是为那些其实例可能被另一个线程执行的类而设计的,最主要的差别在于Runnable不会返回线程运算结果,Callable可以(假如线程需要返回运行结果).

Future

public class FutureExample {
    static class MyCallable implements Callable<String> {
        @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<String> future = executorService.submit(new MyCallable());
        log.info("do something in main");
        Thread.sleep(1000);
        String result = future.get();
        log.info("result:{}", result);
    }
}

        Future是一个接口表示异步计算的结果,它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。Future提供了get()、cancel()、isCancel()、isDone()四种方法,表示Future有三种功能:

判断任务是否完成

中断任务

获取任务执行结果

FutureTask

        FutureTask是Future的实现类,它提供了对Future的基本实现。可使用FutureTask包装Callable或Runnable对象,因为FutureTask实现了Runnable,所以也可以将FutureTask提交给Executor。

public class FutureTaskExample {
    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
            @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);
    }
}

使用方法:

Callable、Future、FutureTask一般都是和线程池配合使用的,因为线程池ThreadPoolExecutor的父类AbstractExecutorService提供了三种submit方法:

public Future<?> submit(Runnable task){...}

public <T> Future<T> submit<Runnable task, T result>{...}

public <T> Future<T> submit<Callable<T> task>{...}

Fork/Join:

工作窃取算法+双端队列:

        Fork/Join框架是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork/Join框架要完成两件事情:

 1.任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割。

 2.执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里。

public class ForkJoinTaskTest extends RecursiveTask<Integer> {
    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();
        ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);//生成一个计算任务,计算1+2+3+4
        Future<Integer> result = forkjoinPool.submit(task);//执行一个任务
        try {
            log.info("result:{}", result.get());
        } catch (Exception e) {
            log.error("exception", e);
        }
    }
}

BlockingQueue

BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。

         API:

boolean add(E e)

将给定元素设置到队列中,如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值,推荐使用offer()方法。

boolean offer(E e)

将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。

void put(E e) throws InterruptedException

将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。

boolean offer(E e, long timeout, TimeUnit unit)throws InterruptedException

将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.

E take() throws InterruptedException

从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。

E poll(long timeout, TimeUnit unit)throws InterruptedException

在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法,为null则直接返回null。

int remainingCapacity()

获取队列中剩余的空间。

boolean remove(Object o)

从队列中移除指定的值。

public boolean contains(Object o)

判断队列中是否拥有该值。

int drainTo(Collection<? super E> c)

将队列中值,全部移除,并发设置到给定的集合中。

int drainTo(Collection<? super E> c, int maxElements)

指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。

阻塞队列的成员

队列

有界性

数据结构

ArrayBlockingQueue

bounded

加锁

arrayList

是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】

LinkedBlockingQueue

optionally-bounded

加锁

linkedList

一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序。

PriorityBlockingQueue

unbounded

加锁

heap

一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。

DelayQueue

unbounded

加锁

heap

一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。(DelayQueue可以运用在以下应用场景:1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,

从比如TimerQueue就是使用DelayQueue实现的。)

SynchronousQueue

bounded

加锁

一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

LinkedTransferQueue

unbounded

加锁

heap

一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。

LinkedBlockingDeque

unbounded

无锁

heap

一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。

posted @ 2021-10-27 22:20  Older&nbspSix  阅读(42)  评论(0)    收藏  举报