java并发编程之队列

本文代码示例已放入github:请点击我

快速导航------>src.main.java.yq.Thread.MyQueue

在开始讲解队列之前我们先了解一下下面这三个东西

  1. CountDownLatch(计数器)
  2. CyclicBarrier(屏障)
  3. Semaphore(计数信号量)

他们三个是干什么的呢?有什么用?那我们接下来慢慢讲解。

实现方法:CountDownLatch

        需求一:现在我们有这样一个需求,在主线程中开了新的线程去执行任务,大家都知道,线程的执行不会影响其他线程,也就是说在我们的主线程中创建的新的线程,可能会在子线程执行完毕之前就执行完毕了,但是我们不想要这样,我们想要等子线程执行完毕之后再进行执行主线程中的部分代码。

        CountDownLatch可以实现类似于计数器的作用,也就是说只有指定数量的线程执行完毕之后才会放行。他不会影响到其他线程执行,但是会阻赛调用countDownLatch.await()的线程,在await处会被阻赛。我们接下来上实例代码。

 /**
     * CountDownLatch j计数器:只有指定书目的子线程执行完毕才,被阻塞的线程才可以执行
     */
    static final class MyCountDownLatch extends Thread {

        private CountDownLatch countDownLatch;

        public MyCountDownLatch(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }

        public static void main(String[] args) {
            CountDownLatch countDownLatch = new CountDownLatch(4);
            try {
                System.out.println("主线程开始执行了");
                for (int i = 0; i < 5; i++) {
                    new MyCountDownLatch(countDownLatch).start();
                }
//                countDownLatch.await();
                System.out.println("好了所有线程执行完毕了");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "---------我执行了");
//            countDownLatch.countDown();
        }

    }

首先我们创建了CountDownLatch,但是我们并没有使用,我们看看会发生什么情况。

接下来,我们放开注释,使用CountDownLatch,我们再来看执行结果。

        为什么我们不使用CountDownLatch的时候就会执行的结果不是我们想要的呢。那是因为我们在主线程中开了新的四个线程,大家都知道线程是一条新的独立的执行的路径,当我们所有的子线程还没有执行完毕的时候,但是我们的主线程就执行完毕了,所以就出现了这种打印样式。

        为什么使用了CountDownLatch之后就达到了我们的需求,所有子线程执行完毕主线程后面的打印才开始执行,是因为我们使用了CountDownLatch.await()阻塞了主线程,然后我们在每个子线程执行完毕调用了一次CountDownLatch.countDown()方法,表示当前线程执行完毕了,在创建CountDownLatch对象的时候,需要指定一个数字,每次调用一下CountDownLatch.countDown()方法就会进行减一的操作,当为0的时候才会恢复当前线程。这里我们有四个线程,这也是为啥我们创建CountDownLatch对象传递了参数为4的原因。另外,使用await方法同样可以指定阻塞时间,当时间过了之后就算没有调用countDown方法也会进行执行。

实现方法:CyclicBarrier

        需求二:现在有这个场景,我需要达到指定线程数才可以进行执行,就比如我们玩王者,如果没有10个人,那么就不允许开始游戏,只有存在10个人了我们才开始进行游戏。我们就可以把一个玩家看作一个线程。只有达到了10个人,才允许开始游戏。那么就可以使用我们的CyclicBarrier进行实现。

        CyclicBarrier说是屏蔽,但是感觉不像,容易让人发生为误解,他的实际作用就是会阻塞所有的线程,只有达到指定线程数量时,那么所有线程才会开始执行,就是感觉可以让所有的线程同时知悉,不知道测试工具jmter是不是用到了这个东西。

那么我们还是使用代码演示:

 /**
     * CyclicBarrier 屏障:只有达到指定数目线程数达到就绪状态才会进行执行
     */
    static final class MyCyclicBarrier extends Thread{
        private CyclicBarrier cyclicBarrier;

        public MyCyclicBarrier(CyclicBarrier cyclicBarrier){
            this.cyclicBarrier = cyclicBarrier;
        }

        public static void main(String[] args) {
            CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
            try {
                System.out.println("开始匹配");
                for(int i = 0 ; i < 10 ; i++){
                    Thread.sleep(500);
                    new MyCyclicBarrier(cyclicBarrier).start();
                }
                Thread.sleep(50);
                System.out.println("所有玩家准备完毕,游戏开始!!!");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        @Override
        public void run() {
            try {
                System.out.println("我是玩家 "+Thread.currentThread().getName()+"我准备好了");
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

        

        这里我们可以看到,我们创建了CyclicBarrier对象,并且传递了参数为10,那么按照之前的解释,意思就是当达到十个线程在就绪状态之后才会开始执行,为了更好的看到效果,我们在创建线程的时候sleep半秒,结果发现在五秒之后基本上加上主线程都同时进行执行。这便是我得到的结论,CyclicBarrier的作用就是当达到指定的线程数才开始执行,否则会被阻塞。另外CyclicBarrier对象的await方法也可以指定时间,当时间过了之后如果没有达到指定的线程数量,那么被阻塞的线程会被唤醒进行执行。

实现方法:Semaphore

        需求三:我们一个方法只允许五个线程同时进行执行,如果有执行完毕,或者退出的,另外的线程就又可以进行补充,就好像是王者荣耀,我们一个房间只允许五个玩家自由加入,一旦有五个人加入了就不允许加入了,当游戏未开始,但是有玩家没有同意,那么这个时候到了30秒之后他就会自动退出房间,那么这个时候又会有新的玩家加入,那么这种就可以使用我们的Semaphore实现。(数据库连接池

        Semaphore就是相当一可以设定一个阀值,当未达到阀值的时候线程就可以竞争钥匙(许可信号),当使用完毕之后就需要进行归还钥匙(许可信号),当归还了钥匙之后,其他的线程才可以继续竞争拿到钥匙(许可信号),说白了就是只允许指定数量的线程同时访问资源。

 /**
     * Semaphore 计数信号量:设置指定资源只允许指定数量线程进行同时执行,其余线程会被阻塞
     * 有许可被释放时,被阻塞的线程会发起竞争。
     */
    static final class MySemaphore extends Thread{

        private Semaphore semaphore;
        private CyclicBarrier cyclicBarrier;

        public MySemaphore(Semaphore semaphore,CyclicBarrier cyclicBarrier){
            this.semaphore = semaphore;
            this.cyclicBarrier = cyclicBarrier;
        }

        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(3);
            CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
            try {
                MySemaphore mySemaphore = null;
                for (int i = 0 ; i < 10 ; i++){
                    mySemaphore = new MySemaphore(semaphore,cyclicBarrier);
                    mySemaphore.start();
                    System.out.println(mySemaphore.getName()+"线程就绪");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        @Override
        public void run() {
            try {
                cyclicBarrier.await();
                //拿到许可
                semaphore.acquire();
                System.out.println("我是线程"+Thread.currentThread().getName()+"我拿到许可了");
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //释放许可
                semaphore.release();
            }
        }
    }

        为了方便观察,我们使用了CyclicBarrier,当线程达到10才进行之后后面的内容,我们还使用sleep进行休眠了3秒。通过控制台的打印输出我们可以很清晰的看到,当10个线程创建完毕之后,控制台每三个每三个进行打印的信息,而且这个时候我们的线程的顺序也发生了变法,这完全说明了,我们的Semaphore起到了作用,首先确实该打印方法只有三个线程进行执行,而且线程进行了竞争,不然位置不可能是正确的。

semaphore常用方法
方法名称 作用
semaphore.acquire(); 获取许可
semaphore.availablePermits(); 返回剩余可用的许可数量
semaphore.drainPermits(); 得到现在可用的所有许可
semaphore.getQueueLength(); 得到预计正在等待的线程数
semaphore.hasQueuedThreads(); 查询是否还有线程正在进行等待
semaphore.release(); 释放许可

总结:

  1. CountDownLatch:当需要指定数目线程执行完毕之后才进行执行,使用CountDownLatch
  2. CyclicBarrier:需要指定数量线程都到达就绪状态之后才进行执行,那么就是用CyclicBarrier
  3. Semaphore:一段程序只允许指定数量线程同时进行执行,那么就使用Semaphore

java并发编程之队列

       在java中队列(Queue)跟list和set是同一个级别的,他们都是继承于Collection接口。所以他们比较类似,但是Queue是先进先出原则(FIFO),队列的好处就是处理速度快,性能好,经常被用作处理并发数据,因为处理速度块呗。那么接下来我们就慢慢打开队列的大门。

队列主要分为三大类:

  1. 阻塞队列
  2. 非阻塞队列
  3. 双端队列

阻塞队列:顾名思义,阻塞队列会发生阻塞,下面是阻塞队列被阻塞的两大场景:

  • 当阻塞队列中没有数据,这时有线程进行取数据,那么该线程会被阻塞,直到有新的数据被添加,然后被该线程读取。
  • 当阻塞队列中存满了数据,这时有线程进行存数据,同样会被阻塞,直到队列中有新的空间,然后该线程才允许存放数据。

非阻塞队列:也是很好理解,就是不会进行阻塞,他的性能更好,底层通过链表实现以及CAS无锁机制,所以当然性能更好。

双端队列:栈的原理是先进后出现,而队列的原理是先进先出(FIFO),但是双端队列,则是头也可以近,尾也可以进,即双向操作,而且可以实现栈的功能。之前如果要实现先进后出的话,可以使用Stack类,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque;既然Queue只是一个接口,当需要使用队列时也就首选ArrayDeque了(次选是LinkedList),这里的ArrayDeque和LinkedList就是双端队列(Deque)的直接实现类。如果想深入了解双端链表请点击我:Stack And Queue

下面是阻塞队列的直接实现:

下面我们讲解一下几个主要的阻塞队列:

  • ArrayBlockingQueue:底层是数组实现,而且创建时应该指定容量大小
  • LinkedBlockingQueue:如果指定大小,那么就是有限的,如果不指定那就是无效的,说是无限其实还是有限的,不指定           则是 0x7fffffff 这么大
  • PriorityBlockingQueue:没有边界的一个队列,允许插入null,但是插入的对象必须是实现   java.lang.Comparable   接             口,而排序规则也是在   java.lang.Comparable   中实现的  ----> 可以设置优先级
  • SynchronousQueue:内部只允许容纳一个元素,只有该元素被消费了,才允许继续插入

上诉几个队列都是阻塞队列,都可能会被阻塞。

阻塞队列的常用方法:

BlockingQueue常用方法
排序方法 平均情况 最好情况
add 增加一个元素 如果队列已满,则抛出一个IllegalSlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
poll 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 返回队列头部的元素 如果队列满,则阻塞
take 返回队列头部的元素 如果队列为空,则阻塞

       那么下下面我们就使用阻塞队列实现生产者和消费者的例子,还记得在我们的--  Java并发编程之线程之间通讯 -- 之中为了实现生产者和消费者的例子,我们使用了wait,notify以及Cendition,相对比较麻烦,而且性能还没有队列高。

首先我们提取出一些公共的方法,消费者和生产者。到时候只需要传递不同的队列实例,可以达到代码的复用。

 private String name;
    private String sex;

    public MyThreadPool(String name, String sex) {
        this.name = name;
        this.sex = sex;
    }

    //生产者
    static final class Producer extends Thread{

        private Queue<MyThreadPool> queue;

        public Producer(Queue<MyThreadPool> queue) {
            this.queue = queue;
        }

        public void create(){
            int i = 0 ;
            MyThreadPool myThreadPool = null;
            while (true){
                try {
                    Thread.sleep(1000);
                    //永远都是0 或者1 要不是小红 要不是张三
                    if(i % 2 == 0){
                        myThreadPool = new MyThreadPool("张三", "男");
                    }else{
                        myThreadPool = new MyThreadPool("小红", "女");
                    }
                    queue.offer(myThreadPool);
                    System.out.println("生产完毕:"+myThreadPool.toString());
                    i++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void run() {
            create();
        }
    }

    //消费者
    static final class Consumer extends Thread{

        private Queue<MyThreadPool> queue;

        public Consumer(Queue<MyThreadPool> queue) {
            this.queue = queue;
        }

        public void consumer(){
            try {
                while (true){
                    Thread.sleep(1000);
                    MyThreadPool poll = queue.poll();
                    System.out.println("开始消费:"+poll.toString());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            consumer();
        }
    }


    //开始的方法
    public static void start(Queue queue){
        new Producer(queue).start();
        try {
            //因为我们要使生产者先执行,不然获取不到值,会报错
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Consumer(queue).start();
    }
ArrayBlockingQueue实例:
 /**
     *
     * LinkedBlockingQueue: 无边界大小阻塞线程,说是无限大 其实也不是,只是底层给定了一个很大的默认值
     * ArrAyBlockingQueue:需要指定大小的一个阻塞队列
     */
    @Data
    static final class MyArrAyBlockingQueue{
        public static void main(String[] args) {
            ArrayBlockingQueue<MyThreadPool> arrAyBlockingQueues = new ArrayBlockingQueue<>(4);
            start(arrAyBlockingQueues);
        }
    }

执行结果:

        这个例子很简单,我们创建了两个线程,一个生产者,一个消费者,很简单的几步,我们就实现了生产者和消费者。是不是很简单呢。其中LinkedBlockingQueue和ArrAyBlockingQueue作用差不多,只是一个需要指定大小,一个可以不用指定,这里就不演示了。

PriorityBlockingQueue实例:
 /**
     * PriorityBlockingQueue: 该队列也是一个没有边界的阻塞队列,而且可以指定排序规则
     */
    static final class MyPriorityBlockingQueue{
        public static void main(String[] args) {
            PriorityBlockingQueue<MyThreadPool> priorityBlockingQueue = new PriorityBlockingQueue<>();
            start(priorityBlockingQueue);

        }
    }

另外修改MyThreadPool,添加实现接口,添加优先级,

另外添加不同的优先级:

为了更好的效果,我们使消费者先阻塞3秒:Thread.sleep(3000);

接下来 我们看运行及结果:

        我们知道阻塞线程是遵循FIFO,也就是先进先出,那为什么我们这里是level为1的元素先出来呢,就是因为我们使用了PriorityBlockingQueue指定了优先级。数字越小,优先级就越高。

主要的非阻塞队列:

  • ConcurrentLinkedQueue:非阻塞队列,底层是通过链表加上CAS无锁机制,所以性能会更好 

      ConcurrentLinkedQueue的使用方法跟阻塞队列BlockingQeque差不多,但是新增了两个方法,clear()和addAll(Collection<? extends E> c) 很明显一个可以清空队列,一个可以批量添加。

这里就不演示了,跟阻塞队列都差不多。另外双端队列这里也不讲了,内容太多了不好,至于双端队列可以点击我:Stack And Queue

总结:

  1. CountDownLatch:会阻塞调用await的线程,只有当我们设置的count值为o的时候才会继续执行。
  2. CyclicBarrier:调用await的线程会被阻塞,只有当线程数目达到了指定线程数量,那么才会继续执行。
  3. Semaphore:被锁定的资源一次只允许指定数目线程执行,线程会竞争许可,执行完毕之后会释放许可。
  4. 三大类型队列:BlockingQueue:阻塞队列<-->AbstractQueue:非组赛队列<-->Deque:双端队列

谢谢大家的观看~~

本文代码示例已放入github:请点击我

快速导航------>src.main.java.yq.Thread.MyQueue

发布了25 篇原创文章 · 获赞 9 · 访问量 3053

猜你喜欢

转载自blog.csdn.net/qq_40053836/article/details/99983241