【Java基础】线程笔记——多线程交互:线程阀

线程阀是线程与线程之间相互制约和交互的一种机制

基础概念

  • Queue:保存一组元素,采用FIFO(先进先出)原则
  • Deque:保存一组元素,采用先进后出原则(栈结构),双端队列主要用于栈操作
  • 阻塞队列:支持两个附加操作的队列。队列中元素为空时,获取元素的线程会等待队列变成非空;队列元素满时,存储元素的线程会等待队列变成可用(常用于生产者和消费者场景)。生产者往队列添加元素。消费者取出队列元素。

阻塞队列4种处理方法

method | throwException | return specialValue | block | exit

插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit)
移除方法 | remove() | poll() | take() | poll(time,unit)
检查方法 | element() | peek() |  无   |     无

阻塞队列

数组阻塞队列
链表阻塞队列
优先级阻塞队列
延时队列
同步队列
链表双向阻塞队列

链表传输队列
同步计数器
抽象队列化同步器

数组阻塞队列(ArrayBlockingQueue)

public class ArrayBlockingQueueDemo {

    public static void main(String[] args) {
        //新建一个等待队列
        final BlockingQueue<String> queue = new ArrayBlockingQueue<>(16);
        //启动4个线程
        for (int i = 0; i < 4; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {

                    while(true){

                        try {
                            String log = queue.take();
                            parseLong(log);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                    }

                }
            }).start();;
        }

        //处理16条数据
        for (int i = 0; i < 16; i++) {
            String log =  (i + 1) + "----> ";
            try {
                queue.put(log);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void parseLong(String log){
        System.out.println(log + System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

链表阻塞队列(LinkedBlockingQueue)

生产者和消费者采用独立的锁控制数据同步,意味着高并发条件下可以并行操作队列中的数据,以此来提高队列的性能
构造LinkedBlockingQueue函数时,需要指定容量大小。否则默认是一个无限大的容量,当生产者>消费者的速度时候,内存可能已经耗尽了。

public class LinckedBlockingQueueDemo {

    public static void main(String[] args) {
        //新建一个等待队列
        final BlockingQueue<String> queue = new LinkedBlockingQueue<String>(16);
        //产生4个线程
        for (int i = 0; i < 4; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {

                    while(true){

                        try {
                            String log = queue.take();
                            parseLong(log);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                    }

                }
            }).start();;
        }

        //将数据存入队列中
        for (int i = 0; i < 16; i++) {
            String log =  (i + 1) + "----> ";
            try {
                queue.put(log);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void parseLong(String log){
        System.out.println(log + System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

优先级阻塞队列(PriorityBlockingQueue)

优先级对象通过传入Compator对象决定
PriorityBlockingQueue不会阻塞生产者,而只会在没有可消费的数据时候,阻塞消费者。
生产者产生数据的速度绝不能大于消费者接收数据的速度。否则内存容易耗尽
线程同步采用公平锁

延时队列(DelayQueue)

在新增元素时需要指定多久获取队列当前对象(getDelay(TimeUnit unit))
只有延迟期满了以后,才能获取元素
应用场景:缓存设计/定时任务调度

public class Student implements Delayed {

    @Override
    public int compareTo(Delayed delayed) {
        Student that = (Student) delayed;
        return submitTime > that.submitTime ? 1 : (submitTime < that.submitTime ? -1 : 0);
    }

    @SuppressWarnings("static-access")
    @Override
    public long getDelay(TimeUnit unit) {

        return unit.convert(submitTime - System.nanoTime(), unit.NANOSECONDS);
    }

    private String name;
    private long submitTime; 
    private long workTime; 

    public String getName() {
        return this.name + "交卷时间" + workTime;
    }

    public Student(String name, long submitTime) {
        this.name = name;
        this.workTime = submitTime;
        this.submitTime = TimeUnit.NANOSECONDS.convert(submitTime, TimeUnit.MILLISECONDS) + System.nanoTime();
        System.out.println(this.name + "交卷用时:" + workTime);
    }

    public static void main(String[] args) {

        final DelayQueue<Student> dq = new DelayQueue<Student>();
        for(int i = 0; i < 5;i++){
            Student stu = new Student("学生"+i++,Math.round(Math.random()*10 + i));
            dq.put(stu);
        }
        System.out.println("peek()"+dq.peek().getName());
    }

}

同步队列(SynchronizeQueue)

每个put操作必须等待take操作,否则不能添加元素。负责把生产者的数据直接传递给消费者,但是本身并不存储任何元素(适用传递性场景)
比如:在一个线程中的数据传递给另一个线程。它的吞吐量高于上面两个阻塞队列
声明同步队列:公平模式(采用公平锁,配合FIFO队列阻塞多余的生产者和消费者)和非公平模式管理多余的生产者和消费者。
后一种模式容易获取不到数据。

链表双向阻塞队列(LinkedBlockingDeque)

链表构成的双向阻塞队列,可以从队列两端操作元素。在多线程同时入队时减少一半竞争。相比LinkedBlockingQeque多了xxxFirst、xxxLast等方法。add()等同于addLast() remove()等同于removeFirst()

链表传输队列(LinkedTransferQueue)

TransferQueue继承BlockingQueue接口
transfer采用双重数据结构算法(保留和完成)
大致理解:消费者从队列中取一个元素,发现元素为空,则生成一个空元素放入队列。则在该空元素上继续等待,叫保留。生产者意欲向队列中放入元素时,发现前面的数据字段项为空,则将该元素填入空字段中,完成数据传送。
transfer提供了线程间直接交换对象的方法

transfer(E e) //若当前存在一个等待获取的消费者,则移交之。否则会插入当前元素e到队列尾部,从等待进入阻塞状态,直到消费者取出该线程
tryTransfer(E e) //若当前存在一个等待获取的消费者(适用poll()或者take(),立刻转移传输对象e,若不存在返回false,不进入队列
hasWaitingConsumer() //判断是否存在终端消费队列

public class Producer implements Runnable{

    private final TransferQueue<String> queue;
    public Producer(TransferQueue<String> queue) {
        this.queue = queue;
    }

    /**
     * produce:产生数据,让消费者接收
     * @param  @return    设定文件
     * @return String    DOM对象
     * @throws 
     * @since  CodingExample Ver 1.1
     */
    private String produce(){
        return "lucky number : " + (new Random().nextInt(100));
    }

    @Override
    public void run() {

        while(true){
            if(queue.hasWaitingConsumer()){
                try {
                    queue.transfer(produce());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);//等待生产者等待消费者接收数据
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}

public class Consumer implements Runnable {

    private final TransferQueue<String> queue;
    public Consumer(TransferQueue<String> queue) {
        this.queue = queue;
    }



    @Override
    public void run() {


        try {
            System.out.println("Consumer 获取元素 --> " + queue.take());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

运行

public static void main(String[] args) {

        TransferQueue<String> queue = new LinkedTransferQueue<String>();

        //生产者线程
        Thread pt = new Thread(new Producer(queue));
        pt.setDaemon(true);//设置为守护线程
        pt.start();

        for (int i = 0; i < 10; i++) {
            Thread ct = new Thread(new Consumer(queue));
            ct.setDaemon(true);
            ct.start();

            try {
                Thread.sleep(1000);//让消费者休息1s,以便让生产者生产数据
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

}

console

Consumer 接收数据 --> lucky number : 94
Consumer 接收数据 --> lucky number : 97
Consumer 接收数据 --> lucky number : 68
Consumer 接收数据 --> lucky number : 38
Consumer 接收数据 --> lucky number : 39
Consumer 接收数据 --> lucky number : 17
Consumer 接收数据 --> lucky number : 70
Consumer 接收数据 --> lucky number : 25
Consumer 接收数据 --> lucky number : 71
Consumer 接收数据 --> lucky number : 9

同步计数器(ConutDownLatch)

ConutDownLatch(直译:倒计时门闩):在完成一组正在其他线程执行的操作前,允许一个或多个线程等待。
用给定计数初始化ConutDownLatch,由于调用了countdown()方法,计数达到0之前,await()方法会一直阻塞。之后释放所有线程。await之后所有后续调用立即返回。情况只出现一次,计数无法重置。
使用场景:在一些应用场景中,需要等待某个条件达到要求后才能做后面的事情;同时当线程完成后,也会触发事件,以便进行后面操作
ConutDownLatch最重要的方法countdown()和await():前者倒数一次,后者等待倒数到0
应用场景:①开5个多线程下载,5个线程执行完毕才算成功 ② 当用户多文件上传,可采用多线程上传,多个文件都上传成功后,才算成功。


public class CountDownLatchDemo {

    public static void main(String[] args) {

        CountDownLatch latch = new CountDownLatch(3);

        Worker w1 = new Worker("Jack", latch);
        Worker w2 = new Worker("Rose", latch);
        Worker w3 = new Worker("Allen", latch);
        w1.start();
        w2.start();
        w3.start();
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main Thread End");
    }

    static class Worker extends Thread{
        private String workerName;
        private CountDownLatch latch;
        public Worker(String workerName, CountDownLatch latch) {
            this.workerName = workerName;
            this.latch = latch;
        }
        @Override
        public void run() {

            try {
                System.out.println("Worker:"+workerName+"is beginning.");
                Thread.sleep(1000L);
                System.out.println("Worker:"+workerName+"is ending.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            latch.countDown();//模拟干活
        }
    }
}

console

Worker:Jackis beginning.
Worker:Allenis beginning.
Worker:Roseis beginning.
Worker:Roseis ending.
Worker:Allenis ending.
Worker:Jackis ending.
Main Thread End

同步计数器(Semaphore)

Semaphore计数信号量。从概念入手,信号量维护一个许可集合。
如有必要,在许可可用前阻塞每一个acquire(),然后才获取该许可,每个release()添加一个许可,从而释放一个正在阻塞的获取者。
例如:就像排队进入博物馆一样,先放一批人进去,等这批人离开后,再放一批人进去,以此循环。类似一种排队机制

Semaphore(int permits) //创建给定的许可数和给定非公平的公平设置的Semaphore数量
Semaphore(int permits,boolean fair)创建给定的许可数和给定公平的公平设置的Semaphore数量
acquire() //获取一个许可,提供一个许可之前线程阻塞
release() //释放一个许可,将其返回给信号量

所谓公平性就是先进来的先释放。默认false

public class SemaphoreDemo {

    public static void main(String[] args) {

        final Semaphore semaphore = new Semaphore(3);//一次只允许3个人访问
        for (int i = 0; i < 10; i++) {
            final int num = i;
            Runnable thread = new Runnable() {
                @Override
                public void run() {
                    System.out.println("用户"+num+"进入成功");
                    try {
                        Thread.sleep(300L);
                        //获取执行许可
                        semaphore.acquire();
                        System.out.println("用户"+num+"正在进行支付...");
                        Thread.sleep(1000L);
                        //释放获取下一用户访问
                        semaphore.release();
                        System.out.println("用户"+num+"支付完成");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };

            new Thread(thread).start();
        }
            System.out.println("Main Thread End");
    }

}

console

用户1进入成功
用户3进入成功
用户2进入成功
用户0进入成功
Main Thread End
用户4进入成功
用户5进入成功
用户7进入成功
用户6进入成功
用户8进入成功
用户9进入成功
用户9正在进行支付...
用户6正在进行支付...
用户7正在进行支付...
用户6支付完成
用户8正在进行支付...
用户4正在进行支付...
用户9支付完成
用户5正在进行支付...
用户7支付完成
用户0正在进行支付...
用户4支付完成
用户8支付完成
用户5支付完成
用户3正在进行支付...
用户2正在进行支付...
用户0支付完成
用户1正在进行支付...
用户3支付完成
用户2支付完成
用户1支付完成

同步计数器(CyclicBarrier)

CyclicBarrier(直译:循环栅栏):允许一组线程互相等待,直到达到一个公共屏障点。然后所有这些线程才同步往后面执行。
使用场景:
大数据运算需要拆分多步骤的时候。
例如:统计全国的业务数据。数据库互相独立(按省分库),统计过程比较慢,数据量比较大,为了提高运算效率。采取并发方式,多个线程计算各省数据,最后汇总统计。

public class CyclicBarrierDemo {

    public static void main(String[] args) {

        CyclicBarrier cb = new CyclicBarrier(3, new TotalTask());

        BillTask bt1 = new BillTask("重庆", cb);
        BillTask bt2 = new BillTask("四川", cb);
        BillTask bt3 = new BillTask("浙江", cb);
        bt1.start();
        bt2.start();
        bt3.start();

        System.out.println("Main Thread End");
    }

    static class TotalTask extends Thread{
        @Override
        public void run() {

            System.out.println("所有任务执行完毕,就该执行主任务了");

        }
    }

    static class BillTask extends Thread{
        private String billName;
        private CyclicBarrier cb;
        public BillTask(String billName, CyclicBarrier cb) {
            this.billName = billName;
            this.cb = cb;
        }
        @Override
        public void run() {

            System.out.println("市区:" + billName + "运算开始");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("市区:" + billName + "运算完成,等待中...");

            try {
                cb.await();//假设第一次运算不完,第二次依赖第一次运算
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }

            System.out.println("全部都结束"+billName+"开启后面的工作");

        }
    }

}

console

Main Thread End
市区:四川运算开始
市区:浙江运算开始
市区:重庆运算开始
市区:四川运算完成,等待中...
市区:重庆运算完成,等待中...
市区:浙江运算完成,等待中...
所有任务执行完毕,就该执行主任务了
全部都结束重庆开启后面的工作
全部都结束浙江开启后面的工作
全部都结束四川开启后面的工作

CountDownLatch和CyclicBarrier区别:一个线程等待其他线程完成某件事情的时候才能继续。N个线程互相等待,任何一个线程完成之前,所有线程必须等待。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值