并发编程(2):AQS

全称:AbstractQueuedSynchronizer

同步组件:CountDownLatch,Semaphore,CyclicBarrier,ReentrantLock,Condition,FutureTask

 

CountDownLatch

使一个线程等待其他线程完成各自的工作后再执行(5个人开发一个版本,等封板结束可以下班回家,任何一个人没有完成都不能封板,所有人都不能回家)

 

public class CountDownLatchDemo {

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

        ExecutorService executorService = Executors.newCachedThreadPool();

 

        CountDownLatch countDownLatch = new CountDownLatch(5);

        for (int i = 0; i < 5 ;i++) {

            final int threadCount = i + 1;

            executorService.execute(() ->{

                log.info("I am the No. " + threadCount + ", I arrived");

                countDownLatch.countDown();

                log.info("I am the No. " + threadCount + ", I get in th bus");

            });

            Thread.sleep(100);

        }

 

        countDownLatch.await();

        log.info("go go go");

        executorService.shutdown();

    }

}

 

注意:1、CountDownLatch 必须在所有线程开始执行之前初始化

2、countDown方法放到finally里,防止不执行

3、必须在所有线程结束的代码加上await(),不带参数会一直等待,带参数控制等待多久

4、ExecuteService.shutdown并不是立即终止线程,而是所有线程结束后才关闭线程池

 

 

CyclicBarrier(同步屏障)

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

@Slf4j

public class CyclicBarrierDemo {

 

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

        ExecutorService executorService = Executors.newCachedThreadPool();

 

        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

        for (int i = 0; i < 5 ;i++) {

            final int threadCount = i + 1;

            executorService.execute(() ->{

                try {

                    log.info("I am the No. " + threadCount + ", I arrived");

                    cyclicBarrier.await();

                    log.info("I am the No. " + threadCount + ", I get in th bus");

                }

                catch (Exception e) {

                    e.printStackTrace();

                }

            });

            Thread.sleep(100);

        }

 

        log.info("go go go");

        executorService.shutdown();

    }

}

 

CyclicBarrier和CountDownLatch的区别:

1、CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

2、CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true。

3、CountDownLatch是主线程等待其他线程就位后才可以执行什么事情,子线程该干嘛干嘛;CyclicBarrier是线程相互等待,子线程一个都不准继续走;

 

 

Semaphore(信号量)

Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。

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 threadCount = i;

        exec.execute(() -> {

            try {

                semaphore.acquire();

                Thread.sleep(1000);

                System.out.println(threadCount);

                semaphore.release();

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        });

    }

 

    exec.shutdown();

}

 

注意:1、acquire和release可以传入参数n,表示一次申请或者释放n个信号量

2、new Semaphore(3, true)表示公平信号量,不带true默认是非公平信号量

3、公平信号量表示调用aquire的顺序就是获取许可证的顺序(相当于大家都在等着排队打饭,有三个窗口,突然一个人闯到前面去了,就是非公平,公平就是按先后排队的顺序去打饭)

4、可以使用tryAquire(),返回boolean,表示是否有可用信许可证,没有就不执行。可以带上两个参数表示等待时间,超过这个时间还没有获取到许可证,就不会执行其他线程了。(排队太长,设置个时间,超过这个时间,我就不吃饭了)

 

===============================================================================================

ReentrantLock(可重入锁)

独有功能,sync没有的功能:1、可以指定公平锁或者非公平锁。2、提供Condition,可以分组唤醒特定的线程。3、可以提供中断等待锁

为什么不放弃sync:1、sync比较简单,不需要手工去释放,性能一致;2、sync可以跟踪死锁,lock不知道哪个线程持有lock

lock和condition的配合使用:主线程创建Lock和Condition,通过lock,newCondition创建condition,condition可以理解为一个信号量,当这个信号量await后,代码不准向下执行,只有别的线程把这个信号量signal后才可以继续执行。

public class LockDemo {

    private final static int threadCount = 20;

    private static int sum = 0;

 

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

 

        ExecutorService exec = Executors.newCachedThreadPool();

        Lock lock = new ReentrantLock();

 

        for (int i = 0; i < threadCount; i++) {

            final int threadCount = i;

            exec.execute(() -> {

                try {

                    lock.lock();

                    System.out.println("thread " + threadCount + " operate!");

                    System.out.println("sum= " + ++sum);

                } finally {

                    lock.unlock();

                }

            });

        }

 

        System.out.println("======finish========");

        exec.shutdown();

    }

}

 

 

多线程的深入理解:

1、线程池只是约定一个池子,至于有多少线程会运行和线程池的大小无关。

2、主线程里约定多少个线程去执行任务,主线程没有去new Thread,就不会多线程代码。

3、Runnable的run方法,Callable的calll方法还有Future里面的submit方法,里面的代码是子线程执行的,会多次执行,看到就应该当成子线程代码。

4、JUC里面的AQS组件,都是由主线程new一个组件,子线程去使用这个组件。

 

===========================================================================

 

AQS的相关扩展类

Future

一个接口!Future归根到底的作用是,在主线程里得到每个子线程的返回值

public class FutureTaskDemo {

    private static int sum = 0;

 

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

        Callable<String> callable = () -> {

            Thread.sleep(500);

            return String.valueOf(sum++);

        };

 

        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i< 5; i++) {

            Future<String> future = executorService.submit(callable);

            String result = future.get();

            System.out.println(result);

        }

 

        executorService.shutdown();

    }

}

 

FutureTask

一个类,定义来未来任务,可重用,线程未执行前没有结果的!

public class FutureTaskDemo {

    private static int sum = 0;

 

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

        Callable<String> callable = () -> {

            Thread.sleep(500);

            return String.valueOf(sum++);

        };

 

        for (int i = 0; i< 5; i++) {

            FutureTask<String> futureTask = new FutureTask<>(callable);

            new Thread(futureTask).start();

            System.out.println(futureTask.isDone());

            String result = futureTask.get();

            System.out.println(futureTask.isDone());

            System.out.println(result);

        }

    }

}

 

 

Future和FutureTask的不同之处:

1、Future是一个接口,其实现依赖于ExecutorService.submit(callble),定义就是执行;

2、FutureTask是一个类,继承于RunnableFuture,同时继承了Future和Runnable可以很方便的定义和重用,既可以使用ExecutorService.execute(futureRask)执行,也可以使用new Thread(futureTask).start()使用

 

 

Future和FutureTask的注意点:

1、每个线程都需要单独定义一个Future或者FutureTask。

2、没有get到结果前,主线程会一直阻塞!

3、FutureTask可以理解为一个任务,一个已经做过的任务,即使再次thread.start,但是不执行

3、如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。

 

===========================================================================

BlockingQueue(阻塞队列)

 

相关方法

 

放入数据:

offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程)

offer(E o, long timeout, TimeUnit unit),可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。

put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.

获取数据:

poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;

poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。

take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;

drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

 

相关实现类:

ArrayBlockingQueue:定长的队列

LinkedBlockingQueue:基于链表的阻塞队列,没有指定大小的情况下,大小为Integer.MAX_VALUE,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

DelayQueue:当其指定的延迟时间到了,才能够从队列中获取到该元素。

PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),其不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。

SynchronousQueue:无缓冲的等待队列

posted @ 2020-06-10 16:09  北大教授  阅读(118)  评论(0)    收藏  举报