JAVA篇:Java 多线程 (三) 多线程与集合
3、多线程与集合
关键字:集合、Java多线程、Queue、PriorityQueue、ConcurrentLinkedQueue、BlockingQueue、ArrayBlockingQueue 、LinkedBlockingQueue 、SynchronousQueue 、PriorityBlockingQueue、DelayQueue、生产者/消费者模型
3.1 集合简述
之前整理集合的时候写了这篇:
-
List列表是有序的集合,其实现类平常比较关注的有ArrayList(列表的经典实现,方便遍历、查询)、LinkedList(列表的链表实现,方便中间增删)、Vector(线程安全的向量)、Stack(继承于Vector的一个先进后出的堆栈结构)。
-
set集合维护一个无重复元素的集合,主要关注的实现类TreeSet(提供排序功能的Set、底层是树的结构)、HashSet(为快速查找而设计的Set,存入HashSet的对象必须实现hashCode()和equals())、LinkedHashSet(具有HashSet的查询速度,且内部使用链表维护元素的顺序--掺入的次序)。
-
Queue队列
-
Map图,维护了<K,V>的结构,主要关注的实现类TreeMap(提供了按照K排序的功能)、HashMap(具有较高的查询速度)、LinkedHashMap(和HashMap一样的查询速度,并且以链表维护了插入的次序)、HashTable(线程安全)。
3.2 线程安全
前面在整理集合的时候,我们发现有提到过线程安全的概念。
线程安全的定义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。需要保证两个点:共享变量内存的可见性及临界代码访问的顺序性。
Java的内存模型中有主内存和线程的工作内存之分,主存上存放的是线程共享的变量(实例字段、静态字段和构成数组的元素),线程的工作内存是线程私有的空间,存放的是线程私有的变量(方法参数与局部变量)。线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性能并不是直接去修改主内存而是会在线程私有的工作内存中创建一份变量的拷贝(缓存),在工作内存上对变量的拷贝修改之后再把修改的值刷回到主内存的变量中去。在多线程访问的时候,就会导致一个线程修改了共享变量后,其他线程并不知道该变量进行了修改仍基于自己工作内存中保存的副本进行操作。这就涉及了共享变量内存可见性---共享变量修改之后对所有线程可见,所有线程重新从主存读取数据。
关键字volatile就保证了共享变量的可见性。但若是多线程对同一个volatile修饰的变量进行自增,其结果仍是小于预期的,因为volatile并未保证临界代码访问的顺序性。自增代码包含三步:读取变量,变量+1,结果赋值给原变量。在多线程访问的时候,并不能保证一个线程进行这一连串代码操作时,其他线程不会对这个变量进行同一操作,导致结果覆盖,除非进行加锁同步,保证这一段代码运行时其他线程无法访问这段代码,使得代码在多线程执行时有顺序。
所以,相对而言,线程安全的集合的访问方法会更慢一些,效率较低。
前面提到的线程安全的集合包含:
-
vector:相比于arraylist多了个同步机制(线程安全),因为效率极低,现在已经不太建议使用。
-
stack:继承了vector,堆栈类,先进后出。
-
hashtable:相比于hashmap多了个线程安全。
3.3 Queue队列
3.3.1 Queue简述
Queue接口与List、Set同一级别,都是继承了Collection接口。Queue队列是一个先入先出(FIFO)的数据结构。
Queue定义的基本方法包含(插入、检索、移除):
-
boolean add(E e):若队列未满,则将元素插入队列。反之,抛出异常。
-
boolean offer(E e):若队列不满,将元素插入队列中。
-
E element():返回队列的头。若队列为空,会抛出异常。
-
E peek():返回队列的头,若队列为空,返回null。
-
E remove():返回并删除队列的头,若队列为空,会抛出异常。
-
E poll():返回队列的头并删除,若队列为空,返回null。
Queue接口下比较重要的实现:
-
Dueue双端队列:java.util.Dueue为实现类提供了双端访问,比较常用的有
-
ArrayDueue
-
LinkedList
-
ConcurrentLinkedDeque:ConcurrentLinkedQueue的双端队列实现
-
LinkedBlockingDeque:LinkedBlockingQueue 的双端队列实现
-
-
AbstractQueue:java.util.AbstractQueue不允许空值。
-
PriorityQueue
-
ConcurrentLinkedQueue
-
-
BlockingQueue阻塞队列:java.util.concurrent.BlockingQueue
-
ArrayBlockingQueue :一个由数组支持的有界队列。
-
LinkedBlockingQueue :一个由链接节点支持的可选有界队列。
-
SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。
-
PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
-
DelayQueue :一个由优先级堆支持的、基于时间的调度队列。
-
-
3.3.2 PriorityQueue
优先级队列实际上维护了一个有序列表,它的底层实现是二叉小顶堆。
优先级队列不支持空值,也不支持non-comparable(不可比较)的对象,比如说用户自定义且未实现Comparable的类。加入到优先级队列的元素根据它们的天然排序,或者传递给构造函数的java.util.Comparator实现来定位。按照优先级顺序从队头取出数据。
使用方法有三种:
-
传入可比较对象
-
自定义类实现
ComparableObject implements Comparable<ComparableObject>
,并重写compareTo方法。 -
自定义类unComparableObject不可比较,可向 PriorityQueue传入参数自定义对象
Comparator<unComparableObject>
(重写compare方法)。如果想要将优先级队列从递增更改为递减也可使用这个方法。
//自定义可比较类 class ComparableObject implements Comparable<ComparableObject> { private int value; public ComparableObject(int value){ this.value = value; } @Override public int compareTo(ComparableObject o) { return Integer.compare(this.value,o.value); } @Override public String toString(){ return "ComparableObject: "+this.value; } } //自定义类,不可比较 class unComparableObject{ private int value; public unComparableObject(int value){ this.value = value; } @Override public String toString(){ return "unComparableObject: "+this.value; } } public void test1(){ /* 测试PriorityQueue */ //1、使用Integer PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(); priorityQueue.add(2); priorityQueue.add(21); priorityQueue.add(222); priorityQueue.add(9); priorityQueue.add(2); priorityQueue.add(1111111111); System.out.println("1*******输出PriorityQueue<Integer>数据*********************"); while (!priorityQueue.isEmpty()){ System.out.println(priorityQueue.poll()); } //2、使用ComparableObject PriorityQueue<ComparableObject> priorityQueue1 = new PriorityQueue<>(); priorityQueue1.add(new ComparableObject(2)); priorityQueue1.add(new ComparableObject(21)); priorityQueue1.add(new ComparableObject(222)); priorityQueue1.add(new ComparableObject(9)); priorityQueue1.add(new ComparableObject(2)); priorityQueue1.add(new ComparableObject(1111111111)); System.out.println("2********自定义可比较类ComparableObject***********"); System.out.println("********输出PriorityQueue<ComparableObject>数据**********"); while (!priorityQueue1.isEmpty()){ System.out.println(priorityQueue1.poll()); } //3、使用unComparableObject和Comparator<unComparableObject> PriorityQueue<unComparableObject> priorityQueue2 = new PriorityQueue<>(new Comparator<unComparableObject>() { @Override public int compare(unComparableObject o1, unComparableObject o2) { return Integer.compare(o1.value,o2.value); } }); priorityQueue2.add(new unComparableObject(2)); priorityQueue2.add(new unComparableObject(21)); priorityQueue2.add(new unComparableObject(222)); priorityQueue2.add(new unComparableObject(9)); priorityQueue2.add(new unComparableObject(2)); priorityQueue2.add(new unComparableObject(1111111111)); System.out.println("3********自定义不可比较类unComparableObject,并传入参数new Comparator<unComparableObject>***********"); System.out.println("********输出PriorityQueue<unComparableObject>数据**********"); while (!priorityQueue2.isEmpty()){ System.out.println(priorityQueue2.poll()); } }
3.3.3 ConcurrentLinkedQueue
ConcurrentLinkedQueue是基于链接节点的、线程安全的无界队列。并发访问不需要同步,因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinekedQueue对公共集合的共享访问就可以工作得很好。如果需要收集关于队列大小的信息会很慢,需要遍历队列。
同时ConcurrentLinkedQueue虽然线程安全,但是它是无锁的,无法实现队列为空时阻塞等待的操作,但是相对而言效率更高。
/**************************** 测试ConcurrentLinkedQueue*****************************************************************************************************/ volatile boolean isfinsh = false; public void test2(){ ConcurrentLinkedQueue<Integer> concurrentLinkedQueue = new ConcurrentLinkedQueue<>(); /* 步骤1:将1~100的数据传入concurrentLinkedQueue */ System.out.println("步骤1:将1~100的数据传入concurrentLinkedQueue "); for(int i=1;i<=100;i++){ concurrentLinkedQueue.add(i); } System.out.println(concurrentLinkedQueue); /* 步骤2:开5个线程,每个线程从concurrentLinkedQueue取出数字-100后放回*/ System.out.println("步骤2:开5个线程,每个线程从concurrentLinkedQueue取出数字-100后放回 "); Runnable r = new Runnable() { @Override public void run() { while (!isfinsh){ int item = concurrentLinkedQueue.poll(); if(item==50){ isfinsh=true; } item = item-100; concurrentLinkedQueue.add(item); } } }; Thread[] threads = new Thread[5]; for(int i=0;i<threads.length;i++){ threads[i] = new Thread(r); threads[i].setDaemon(true); threads[i].start(); } for(int i=0;i<threads.length;i++){ try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } } /* 步骤3:输出结果*/ System.out.println("步骤3:输出结果 "); System.out.println("concurrentLinkedQueue的大小:"+concurrentLinkedQueue.size()); System.out.println(concurrentLinkedQueue); }
结果如下,可以从结果看出来,线程安全是没有问题的。
只是到50后就使用volatile boolean isfinsh通知其他线程不再运行,有时候,如果已有线程已经获取数据,会出现“刹不住车”,多处理几个数据的情况。
步骤1:将1~50的数据传入concurrentLinkedQueue [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100] 步骤2:开5个线程,每个线程从concurrentLinkedQueue取出数字-100后放回 步骤3:输出结果 concurrentLinkedQueue的大小:100 [51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, -99, -98, -97, -96, -95, -94, -93, -92, -91, -90, -89, -88, -86, -87, -85, -84, -83, -82, -81, -79, -78, -80, -77, -76, -75, -74, -73, -72, -71, -70, -69, -68, -67, -66, -65, -64, -62, -63, -61, -59, -60, -58, -57, -55, -56, -53, -54, -52, -51, -50]
3.3.4 阻塞队列
这个几个可以放在一起说,它们都是阻塞队列,即当资源耗尽(队列为空)时,从队列获取数据的操作就会阻塞。
-
ArrayBlockingQueue 是阻塞队列的数组实现,是有界的,必须指定大小。
-
LinkedBlockingQueue 是阻塞队列的链表实现,是无界的。
-
SynchronousQueue 是无缓冲区的,即向这个“队列”插入一个数据,就必须阻塞到这个数据被取出才会重新进行插入
-
PriorityBlockingQueue 则是给阻塞队列添加了优先级判定,即取出的顺序是有序的。
-
DelayQueue 除了优先级,还添加了时间判断。
BlockingQueue特有的方法如下:
-
void put(E e) throws InterruptedException:把e添加进BlockingQueue中,如果BlockingQueue中没有空间,则调用线程被阻塞,进入等待状态,直到BlockingQueue中有空间再继续
-
void take() throws InterruptedException:取走BlockingQueue里面排在首位的对象,如果BlockingQueue为空,则调用线程被阻塞,进入等待状态,直到BlockingQueue有新的数据被加入
-
int drainTo(Collection<? super E> c, int maxElements):一次性取走BlockingQueue中的数据到c中,可以指定取的个数。通过该方法可以提升获取数据效率,不需要多次分批加锁或释放锁
只有使用put()和take()会形成阻塞,同时也会在被中断时抛出异常。
以下测试代码分三部分:
-
由于ArrayBlockingQueue有界,用于测试生产阻塞。配置生产线程运行比消费线程快,由于ArrayBlockingQueue 队列已满,每次都阻塞到消费线程1消费了一个数据,生产线程1才能插入数据。
-
PriorityBlockingQueue 无界,用于测试其优先级以及消费阻塞。配置消费线程运行比生产线程快,当PriorityBlockingQueue 队列为空时,每次都阻塞到生产线程2生产了一个数据,消费线程2才能消费数据。
-
SynchronousQueue 无缓冲区,生产一个数据,消费一个数据相互阻塞。
/**************************** ArrayBlockingQueue 、LinkedBlockingQueue 、SynchronousQueue 、PriorityBlockingQueue ***********************************************************************/ public void test3(){ System.out.println("***********************测试阻塞队列*********************************"); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); System.out.println("***********************测试ArrayBlockingQueue*********************************"); ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue<>(3); arrayBlockingQueue.add(3); arrayBlockingQueue.add(1); arrayBlockingQueue.add(2); System.out.println("arrayBlockingQueue:"+arrayBlockingQueue); Random random = new Random(); Runnable p1 = new Runnable() { @Override public void run() { while(true){ try { int num = random.nextInt(99); System.out.println(df.format(new Date())+"数据生产线程:尝试放入数据"+num+"......"); arrayBlockingQueue.put(num); System.out.println(df.format(new Date())+"数据生产线程:放入一个数据(限制大小3),现在:"+arrayBlockingQueue); Thread.sleep(20); } catch (InterruptedException e) { System.out.println(df.format(new Date())+"数据生产线程:被中断!"); break; } } } }; Thread put1 = new Thread(p1); put1.setDaemon(true); put1.start(); Runnable g1 = new Runnable() { @Override public void run() { while (true){ try { System.out.println(df.format(new Date())+"数据消费线程:尝试消费数据......"); System.out.println(df.format(new Date())+"数据消费线程:消费一个数据"+arrayBlockingQueue.take()); Thread.sleep(50); } catch (InterruptedException e) { System.out.println(df.format(new Date())+"数据消费线程:被中断!"); break; } } } }; Thread get1 = new Thread(g1); get1.setDaemon(true); get1.start(); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(df.format(new Date())+"主线程:中断两个线程"); put1.interrupt(); get1.interrupt(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("arrayBlockingQueue:"+arrayBlockingQueue); System.out.println("主线程:drainTo取出全部数据"); List<Integer> list = new LinkedList<>(); arrayBlockingQueue.drainTo(list); System.out.println("arrayBlockingQueue:"+arrayBlockingQueue); System.out.println("list:"+list); System.out.println("***********************测试PriorityBlockingQueue*********************************"); PriorityBlockingQueue<Integer> priorityBlockingQueue = new PriorityBlockingQueue<>(); priorityBlockingQueue.add(3); priorityBlockingQueue.add(1); priorityBlockingQueue.add(2); System.out.println("priorityQueue:"+priorityBlockingQueue); Runnable p2 = new Runnable() { @Override public void run() { while(true){ try { int num = random.nextInt(99); System.out.println(df.format(new Date())+"数据生产线程:尝试放入数据"+num+"......"); priorityBlockingQueue.put(num); System.out.println(df.format(new Date())+"数据生产线程:放入一个数据,现在:"+priorityBlockingQueue); Thread.sleep(50); } catch (InterruptedException e) { System.out.println(df.format(new Date())+"数据生产线程:被中断!"); break; } } } }; Thread put2 = new Thread(p2); put2.setDaemon(true); put2.start(); Runnable g2 = new Runnable() { @Override public void run() { while (true){ try { System.out.println(df.format(new Date())+"数据消费线程:尝试消费数据......"); System.out.println(df.format(new Date())+"数据消费线程:消费一个数据"+priorityBlockingQueue.take()); Thread.sleep(20); } catch (InterruptedException e) { System.out.println(df.format(new Date())+"数据消费线程:被中断!"); break; } } } }; Thread get2 = new Thread(g2); get2.setDaemon(true); get2.start(); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(df.format(new Date())+"主线程:中断两个线程"); put2.interrupt(); get2.interrupt(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("priorityBlockingQueue:"+priorityBlockingQueue); System.out.println("主线程:drainTo取出全部数据"); list.clear(); priorityBlockingQueue.drainTo(list); System.out.println("priorityBlockingQueue:"+priorityBlockingQueue); System.out.println("list:"+list); System.out.println("***********************测试SynchronousQueue*********************************"); SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>(); System.out.println("synchronousQueue:"+synchronousQueue); Runnable p3 = new Runnable() { @Override public void run() { while(true){ try { int num = random.nextInt(99); System.out.println(df.format(new Date())+"数据生产线程:尝试放入数据"+num+"......"); synchronousQueue.put(num); System.out.println(df.format(new Date())+"数据生产线程:放入一个数据,现在:"+synchronousQueue); Thread.sleep(random.nextInt(50)); } catch (InterruptedException e) { System.out.println(df.format(new Date())+"数据生产线程:被中断!"); break; } } } }; Thread put3 = new Thread(p3); put3.setDaemon(true); put3.start(); Runnable g3 = new Runnable() { @Override public void run() { while (true){ try { System.out.println(df.format(new Date())+"数据消费线程:尝试消费数据......"); System.out.println(df.format(new Date())+"数据消费线程:消费一个数据"+synchronousQueue.take()); Thread.sleep(random.nextInt(50)); } catch (InterruptedException e) { System.out.println(df.format(new Date())+"数据消费线程:被中断!"); break; } } } }; Thread get3 = new Thread(g3); get3.setDaemon(true); get3.start(); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(df.format(new Date())+"主线程:中断两个线程"); put3.interrupt(); get3.interrupt(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("synchronousQueue:"+synchronousQueue); System.out.println("主线程:drainTo取出全部数据"); list.clear(); synchronousQueue.drainTo(list); System.out.println("synchronousQueue:"+synchronousQueue); System.out.println("list:"+list); }
3.3.5 DelayQueue
单单把DelayQueue 拉出来说,是因为我觉得这个实现类挺有趣的。简单说来,只需要将可以设置了不同超时时间的元素放入DelayQueue ,DelayQueue 读取时能阻塞到队列中有元素“超时”时,取出快要“超时”的元素。 所以DelayQueue 可以用来:
-
淘宝订单业务:下单之后如果三十分钟之内没有付款就自动取消订单。
-
饿了么订餐通知:下单成功后60S之后给用户发送短信通知。
-
关闭空闲链接:服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。
-
缓存:缓存中的对象,超过了空闲时间,需要从缓存中移出。
-
任务超时处理。在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。
对“超时”“过一段时间再处理”的事务,在单线程时似乎不是一件麻烦的事情,但是在多并发的情况下,不得不说,DelayQueue 给了更好的处理方式。
DelayQueue 是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,队头的延迟到期时间最长,队尾的延迟到期时间最短,即每次都能取出最快要到期的元素。
元素进入DelayQueue 队列后,先进行排序,然后只有getDelay即剩余时间为0的时候,改元素才有资格被消费者从队列中取出来。所以,传入DelayQueue 的元素必须实现Delayed接口(java.util.concurrent.Delayed接口继承了Comparable<Delayed>接口),并且重写compareTo(用于优先级排序)及getDelay(用于返回剩余时间)两个方法。
测试代码:工单按照设定时间分派给指定负责人
/**************************** DelayQueue *****************************************************************************************************/ /* 工单:工单号,负责人号码,工单处理时长限制 */ class MyTask implements Delayed{ long start = System.currentTimeMillis(); private long time; private int taskNum; private int telNum; public MyTask(int taskNum,int telNum,long time){ this.taskNum = taskNum; this.telNum = telNum; this.time = time; } @Override public long getDelay(TimeUnit unit){ return unit.convert((start+time) - System.currentTimeMillis(),TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o ){ MyTask o1 = (MyTask) o; return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); } @Override public String toString(){ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); return String.format("工单信息:工单号 %d,负责人号码 %d,设置超时 %d,设定分派时间 %s", this.taskNum,this.telNum, this.time, df.format(start+time) ); } } /* 工单分派线程,超时处理 */ class TaskProcess extends Thread{ public DelayQueue<MyTask> works; public TaskProcess(DelayQueue<MyTask> works){ super(); this.setDaemon(true); this.works = works; } @Override public void run(){ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); System.out.println(String.format("工单分派线程:%s 开始后台运行!",df.format(System.currentTimeMillis()))); while (true){ try { System.out.println(String.format("工单分派线程:%s 分派工单--%s",df.format(System.currentTimeMillis()),this.works.take())); } catch (InterruptedException e) { e.printStackTrace(); } } } } public void test4(){ /* 创建 DelayQueue队列*/ DelayQueue<MyTask> works = new DelayQueue<>(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); /* 开启工单分派线程 */ TaskProcess taskProcess = new TaskProcess(works); taskProcess.start(); /* 主线程创建工单 */ int worksnum = 5; Random random = new Random(); for(int i=0;i<worksnum;i++){ MyTask myTask = new MyTask(i,i,100+random.nextInt(3000)); System.out.println(String.format("主线程:%s 创建工单--%s",df.format(System.currentTimeMillis()),myTask)); works.put(myTask); } /* 主线程休眠等待工单分派 */ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("测试结束!剩余未处理工单:"+works); }
运行结果:
工单分派线程:2021-10-15 11:01:19:172 开始后台运行! 主线程:2021-10-15 11:01:19:173 创建工单--工单信息:工单号 0,负责人号码 0,设置超时 2986,设定分派时间 2021-10-15 11:01:22:159 主线程:2021-10-15 11:01:19:175 创建工单--工单信息:工单号 1,负责人号码 1,设置超时 1020,设定分派时间 2021-10-15 11:01:20:195 主线程:2021-10-15 11:01:19:176 创建工单--工单信息:工单号 2,负责人号码 2,设置超时 2601,设定分派时间 2021-10-15 11:01:21:777 主线程:2021-10-15 11:01:19:176 创建工单--工单信息:工单号 3,负责人号码 3,设置超时 3031,设定分派时间 2021-10-15 11:01:22:207 主线程:2021-10-15 11:01:19:176 创建工单--工单信息:工单号 4,负责人号码 4,设置超时 2011,设定分派时间 2021-10-15 11:01:21:187 工单分派线程:2021-10-15 11:01:19:175 分派工单--工单信息:工单号 1,负责人号码 1,设置超时 1020,设定分派时间 2021-10-15 11:01:20:195 工单分派线程:2021-10-15 11:01:20:197 分派工单--工单信息:工单号 4,负责人号码 4,设置超时 2011,设定分派时间 2021-10-15 11:01:21:187 工单分派线程:2021-10-15 11:01:21:196 分派工单--工单信息:工单号 2,负责人号码 2,设置超时 2601,设定分派时间 2021-10-15 11:01:21:777 工单分派线程:2021-10-15 11:01:21:790 分派工单--工单信息:工单号 0,负责人号码 0,设置超时 2986,设定分派时间 2021-10-15 11:01:22:159 测试结束!剩余未处理工单:[工单信息:工单号 3,负责人号码 3,设置超时 3031,设定分派时间 2021-10-15 11:01:22:207]
3.4 生产者/消费者模型
3.4.1 什么是生产者/消费者模型
生产者/消费者模型是一种基于等待/通知机制的重要模型,描述的是有一块缓冲区作为仓库,生产者可将产品放入仓库,消费者可以从仓库中取出产品。生产者/消费者模型关注的是以下几个点:
-
生产者生产的时候消费者不能消费。
-
消费者消费的时候生产者不能生产。
-
缓冲区空时消费者不能消费。
-
缓冲区满时生产者不能生产。
生产者/消费者模型的优点在于:
-
解耦:因为多了一个缓冲区,所以生产者或消费者并不直接相互调用。这样生产者和消费者的代码发生变化,都不会对对方产生影响,这样其实就把生产者和消费者之间的强耦合解开,变为生产者和缓冲区、消费者和缓冲区直接的弱耦合。
-
通过平衡生产者和消费者的处理能力来提高整理处理数据的速度,这是生产者/消费者模型最重要的一个优点。如果消费者直接从生产者这里拿数据,如果生产者生产与消费者消费的速度并不匹配,即使快的一方可以很快地生产/消费好数据,还是得占用CPU白白等在边。有了生产者/消费者模型,生产者和消费者就是两个独立的并发体,生产者把生产出来的数据往缓冲区一丢就好了,不必管消费者;消费者也是,从缓冲区去拿数据就好了,也不必管生产者,缓冲区满了就不生产,缓冲区空了就不消费,使生产者/消费者的处理能力达到一个动态的平衡。
从生产者/消费者模型的概念描述中,可以联想到多线程的等待/唤醒机制以及队列,其实现有以下三种方式。
-
wait()/notifyAll()
-
ReentrantLock所封装的Condition中的等待与唤醒
-
阻塞队列
3.4.2 wait()/notifyAll()实现生产者/消费者模型
需要注意的是,使用wait()/notify()实现生产者/消费者模型时,若是存在多个生产者线程和多个消费者线程,由于notify()每次唤醒的线程都是随意的,无法指定唤醒生产者线程还是消费者线程,有可能陷入“假死”状态,即缓冲区满时调用notify()唤醒的还是生产者线程。所以需要使用notifyAll()。
/**************************** wait()/notifyAll()实现生产者/消费者模型 *****************************************************************************************************/ /* 生产者 */ class Prod extends Thread{ public ArrayQueue<Integer> arrayQueue; public int capacity; public Prod(ArrayQueue<Integer> arrayQueue,int capacity){ super(); this.setDaemon(true); this.arrayQueue = arrayQueue; this.capacity = capacity; } @Override public void run(){ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); System.out.println(String.format("生产线程%s:%s 开始运行!",this.getName(),df.format(System.currentTimeMillis()))); Random random = new Random(); while (true){ synchronized (this.arrayQueue){ try { /* 当缓冲区满时,生产者休眠 */ while(arrayQueue.size()>=capacity){//这里要使用while,因为被唤醒后需要判断是否能继续运行 this.arrayQueue.wait(); } /* 生产数据 */ int r = random.nextInt(99); this.arrayQueue.add(r); System.out.println(String.format("生产线程%s:%s 生产数据%d,缓冲区%s",this.getName(),df.format(System.currentTimeMillis()),r,this.arrayQueue)); /* 唤醒全部等待的线程-唤醒消费线程 */ this.arrayQueue.notifyAll(); Thread.sleep(50); } catch (InterruptedException e) { System.out.println(String.format("生产线程%s:%s 被中断!",this.getName(),df.format(System.currentTimeMillis()))); break; } } } } } /* 消费者 */ class Cust extends Thread{ public ArrayQueue<Integer> arrayQueue; public Cust(ArrayQueue<Integer> arrayQueue){ super(); this.setDaemon(true); this.arrayQueue = arrayQueue; } @Override public void run(){ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); System.out.println(String.format("消费线程%s:%s 开始运行!",this.getName(),df.format(System.currentTimeMillis()))); Random random = new Random(); while (true){ synchronized (this.arrayQueue){ try { /* 当缓冲区空时,消费者休眠 */ while(this.arrayQueue.isEmpty()){ this.arrayQueue.wait(); } /* 消费数据 */ int r = this.arrayQueue.remove(0); System.out.println(String.format("消费线程%s:%s 消费数据%d,缓冲区%s",this.getName(),df.format(System.currentTimeMillis()),r,this.arrayQueue)); /* 唤醒全部等待的线程-唤醒生产线程 */ this.arrayQueue.notifyAll(); Thread.sleep(50); } catch (InterruptedException e) { System.out.println(String.format("消费线程%s:%s 被中断!",this.getName(),df.format(System.currentTimeMillis()))); break; } } } } } public void test5(){ /* 以有界(5)的队列作为缓冲区 */ int capacity = 5; ArrayQueue<Integer> arrayQueue = new ArrayQueue<>(capacity); System.out.println("测试开始!缓冲区开始时情况:"+arrayQueue); /* 创建2个生产者,2个消费者 */ Thread p1 = new Prod(arrayQueue,capacity); p1.start(); Thread p2 = new Prod(arrayQueue,capacity); p2.start(); Thread c1 = new Cust(arrayQueue); c1.start(); Thread c2 = new Cust(arrayQueue); c2.start(); /* 主线程休眠,生产者消费者线程运行 */ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /* 中断生产者线程 */ p1.interrupt(); p2.interrupt(); //c1.interrupt(); //c2.interrupt(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("测试结束!缓冲区结束时情况:"+arrayQueue); }
运行结果如下:
测试开始!缓冲区开始时情况:[] 消费线程Thread-2:2021-10-15 15:50:17:861 开始运行! 生产线程Thread-0:2021-10-15 15:50:17:861 开始运行! 生产线程Thread-1:2021-10-15 15:50:17:861 开始运行! 消费线程Thread-3:2021-10-15 15:50:17:861 开始运行! 生产线程Thread-1:2021-10-15 15:50:17:864 生产数据12,缓冲区[12] 生产线程Thread-1:2021-10-15 15:50:17:925 生产数据18,缓冲区[12, 18] 生产线程Thread-1:2021-10-15 15:50:17:988 生产数据42,缓冲区[12, 18, 42] 生产线程Thread-1:2021-10-15 15:50:18:052 生产数据95,缓冲区[12, 18, 42, 95] 生产线程Thread-1:2021-10-15 15:50:18:115 生产数据36,缓冲区[12, 18, 42, 95, 36] 消费线程Thread-3:2021-10-15 15:50:18:178 消费数据12,缓冲区[18, 42, 95, 36] 消费线程Thread-3:2021-10-15 15:50:18:240 消费数据18,缓冲区[42, 95, 36] 消费线程Thread-3:2021-10-15 15:50:18:304 消费数据42,缓冲区[95, 36] 消费线程Thread-3:2021-10-15 15:50:18:367 消费数据95,缓冲区[36] 消费线程Thread-3:2021-10-15 15:50:18:429 消费数据36,缓冲区[] 生产线程Thread-0:2021-10-15 15:50:18:491 生产数据98,缓冲区[98] 生产线程Thread-0:2021-10-15 15:50:18:555 生产数据58,缓冲区[98, 58] 生产线程Thread-0:2021-10-15 15:50:18:619 生产数据18,缓冲区[98, 58, 18] 生产线程Thread-0:2021-10-15 15:50:18:681 生产数据63,缓冲区[98, 58, 18, 63] 生产线程Thread-0:2021-10-15 15:50:18:743 生产数据31,缓冲区[98, 58, 18, 63, 31] 消费线程Thread-2:2021-10-15 15:50:18:806 消费数据98,缓冲区[58, 18, 63, 31] 消费线程Thread-2:2021-10-15 15:50:18:867 消费数据58,缓冲区[18, 63, 31] 消费线程Thread-2:2021-10-15 15:50:18:930 消费数据18,缓冲区[63, 31] 消费线程Thread-2:2021-10-15 15:50:18:992 消费数据63,缓冲区[31] 生产线程Thread-0:2021-10-15 15:50:19:054 生产数据22,缓冲区[31, 22] 生产线程Thread-0:2021-10-15 15:50:19:055 被中断! 消费线程Thread-3:2021-10-15 15:50:19:055 消费数据31,缓冲区[22] 消费线程Thread-3:2021-10-15 15:50:19:116 消费数据22,缓冲区[] 生产线程Thread-1:2021-10-15 15:50:19:179 生产数据89,缓冲区[89] 生产线程Thread-1:2021-10-15 15:50:19:179 被中断! 消费线程Thread-3:2021-10-15 15:50:19:179 消费数据89,缓冲区[] 测试结束!缓冲区结束时情况:[]
3.4.3 await()/signal()实现生产者/消费者模型
尝试将生产、消费两种操作都封装到了一个类中,大致相当于简单地实现了阻塞队列。
/**************************** await()/signal()实现生产者/消费者模型 *****************************************************************************************************/ /* 将生产、消费两种操作都放入到了一个类中 */ class PandC extends ReentrantLock{ public Condition Prodc = this.newCondition(); public Condition Custc = this.newCondition(); public ArrayQueue<Integer> buffer; public int capacity; public PandC(int capacity){ super(); this.capacity = capacity; this.buffer = new ArrayQueue<>(this.capacity); } public void set(int num) throws InterruptedException { this.lock(); /* 当缓冲区满时,生产者线程休眠 */ while (this.buffer.size()>=this.capacity){ Prodc.await(); } buffer.add(num); //System.out.println(this); /* 唤醒任意消费者线程 */ Custc.signal(); this.unlock(); } public int get() throws InterruptedException { this.lock(); /* 当缓冲区空时,消费者线程休眠 */ while(this.buffer.isEmpty()){ Custc.await(); } int num = buffer.remove(0); //System.out.println(this); /* 唤醒任意生产者线程 */ Prodc.signal(); this.unlock(); return num; } @Override public String toString(){ return "缓冲区:"+buffer; } } public void test6(){ /* 将生产、消费两种操作都放入到了一个类中 */ PandC pandC = new PandC(5); Random random = new Random(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); /* 创建2个生产者线程和两个消费者线程 */ Runnable prod = new Runnable() { @Override public void run() { int r ; System.out.println(String.format("生产线程%s:%s 开始运行!",Thread.currentThread().getName(),df.format(System.currentTimeMillis()))); while (true){ try { r = random.nextInt(99); System.out.println(String.format("生产线程%s:%s 生产数据%d", Thread.currentThread().getName(), df.format(System.currentTimeMillis()), r)); pandC.set(r); Thread.sleep(50+random.nextInt(100)); } catch (InterruptedException e) { System.out.println(String.format("生产线程%s:%s 被中断!",Thread.currentThread().getName(),df.format(System.currentTimeMillis()))); break; } } } }; Runnable cust = new Runnable() { @Override public void run() { int r; System.out.println(String.format("消费线程%s:%s 开始运行!", Thread.currentThread().getName(), df.format(System.currentTimeMillis()))); while (true) { try { System.out.println(String.format("消费线程%s:%s 消费数据%d", Thread.currentThread().getName(), df.format(System.currentTimeMillis()), pandC.get())); Thread.sleep(50 + random.nextInt(100)); } catch (InterruptedException e) { System.out.println(String.format("消费线程%s:%s 被中断!", Thread.currentThread().getName(), df.format(System.currentTimeMillis()))); break; } } } }; Thread p1 = new Thread(prod); p1.setDaemon(true); p1.start(); Thread p2 = new Thread(prod); p2.setDaemon(true); p2.start(); Thread c1 = new Thread(cust); c1.setDaemon(true); c1.start(); Thread c2 = new Thread(cust); c2.setDaemon(true); c2.start(); //* 主线程休眠,生产者消费者线程运行 */ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /* 中断生产者线程 */ p1.interrupt(); p2.interrupt(); //c1.interrupt(); //c2.interrupt(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("测试结束!"+pandC); }
运行结果如下:
消费线程Thread-2:2021-10-15 16:35:29:276 开始运行! 生产线程Thread-0:2021-10-15 16:35:29:276 开始运行! 消费线程Thread-3:2021-10-15 16:35:29:276 开始运行! 生产线程Thread-1:2021-10-15 16:35:29:276 开始运行! 生产线程Thread-0:2021-10-15 16:35:29:278 生产数据4 生产线程Thread-1:2021-10-15 16:35:29:278 生产数据96 消费线程Thread-3:2021-10-15 16:35:29:278 消费数据96 消费线程Thread-2:2021-10-15 16:35:29:278 消费数据4 生产线程Thread-1:2021-10-15 16:35:29:352 生产数据95 消费线程Thread-2:2021-10-15 16:35:29:344 消费数据95 生产线程Thread-0:2021-10-15 16:35:29:354 生产数据95 消费线程Thread-3:2021-10-15 16:35:29:368 消费数据95 生产线程Thread-0:2021-10-15 16:35:29:443 生产数据57 消费线程Thread-3:2021-10-15 16:35:29:429 消费数据57 生产线程Thread-1:2021-10-15 16:35:29:457 生产数据8 消费线程Thread-2:2021-10-15 16:35:29:482 消费数据8 生产线程Thread-1:2021-10-15 16:35:29:572 生产数据19 消费线程Thread-3:2021-10-15 16:35:29:580 消费数据19 生产线程Thread-0:2021-10-15 16:35:29:587 生产数据59 消费线程Thread-2:2021-10-15 16:35:29:607 消费数据59 生产线程Thread-1:2021-10-15 16:35:29:696 生产数据67 生产线程Thread-0:2021-10-15 16:35:29:706 生产数据1 消费线程Thread-2:2021-10-15 16:35:29:709 消费数据67 消费线程Thread-3:2021-10-15 16:35:29:721 消费数据1 生产线程Thread-1:2021-10-15 16:35:29:750 生产数据2 消费线程Thread-2:2021-10-15 16:35:29:812 消费数据2 生产线程Thread-0:2021-10-15 16:35:29:851 生产数据12 消费线程Thread-3:2021-10-15 16:35:29:830 消费数据12 生产线程Thread-1:2021-10-15 16:35:29:879 生产数据93 消费线程Thread-2:2021-10-15 16:35:29:902 消费数据93 生产线程Thread-1:2021-10-15 16:35:29:930 生产数据7 消费线程Thread-3:2021-10-15 16:35:29:933 消费数据7 生产线程Thread-0:2021-10-15 16:35:29:961 生产数据22 生产线程Thread-1:2021-10-15 16:35:30:020 生产数据14 生产线程Thread-0:2021-10-15 16:35:30:040 生产数据72 消费线程Thread-2:2021-10-15 16:35:30:052 消费数据22 消费线程Thread-3:2021-10-15 16:35:30:060 消费数据14 生产线程Thread-0:2021-10-15 16:35:30:094 生产数据78 生产线程Thread-1:2021-10-15 16:35:30:113 生产数据56 消费线程Thread-2:2021-10-15 16:35:30:140 消费数据72 消费线程Thread-3:2021-10-15 16:35:30:159 消费数据78 生产线程Thread-1:2021-10-15 16:35:30:195 生产数据84 消费线程Thread-2:2021-10-15 16:35:30:222 消费数据56 生产线程Thread-0:2021-10-15 16:35:30:236 生产数据74 消费线程Thread-3:2021-10-15 16:35:30:261 消费数据84 生产线程Thread-1:2021-10-15 16:35:30:282 被中断! 生产线程Thread-0:2021-10-15 16:35:30:282 被中断! 消费线程Thread-2:2021-10-15 16:35:30:326 消费数据74 测试结束!缓冲区:[]
3.4.4 ArrayBlockingQueue 实现生产者/消费者模型
阻塞队列实现生产者/消费者模型,如果需要限制缓冲区大小,ArrayBlockingQueue 是比较合适的。
如果前面是使用继承ReentrantLock来简单实现了阻塞队列的功能,那么可以将上面调用的代码简单更改,就可以看到使用ArrayBlockingQueue 实现生产者/消费者模型的过程。
/**************************** ArrayBlockingQueue 实现生产者/消费者模型 *****************************************************************************************************/ public void test7(){ /* 设置缓冲区大小5 */ ArrayBlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(5); Random random = new Random(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS"); /* 创建2个生产者线程和两个消费者线程 */ Runnable prod = new Runnable() { @Override public void run() { int r ; System.out.println(String.format("生产线程%s:%s 开始运行!",Thread.currentThread().getName(),df.format(System.currentTimeMillis()))); while (true){ try { r = random.nextInt(99); System.out.println(String.format("生产线程%s:%s 生产数据%d", Thread.currentThread().getName(), df.format(System.currentTimeMillis()), r)); buffer.put(r); Thread.sleep(50+random.nextInt(100)); } catch (InterruptedException e) { System.out.println(String.format("生产线程%s:%s 被中断!",Thread.currentThread().getName(),df.format(System.currentTimeMillis()))); break; } } } }; Runnable cust = new Runnable() { @Override public void run() { int r; System.out.println(String.format("消费线程%s:%s 开始运行!", Thread.currentThread().getName(), df.format(System.currentTimeMillis()))); while (true) { try { System.out.println(String.format("消费线程%s:%s 消费数据%d", Thread.currentThread().getName(), df.format(System.currentTimeMillis()), buffer.take())); Thread.sleep(50 + random.nextInt(100)); } catch (InterruptedException e) { System.out.println(String.format("消费线程%s:%s 被中断!", Thread.currentThread().getName(), df.format(System.currentTimeMillis()))); break; } } } }; Thread p1 = new Thread(prod); p1.setDaemon(true); p1.start(); Thread p2 = new Thread(prod); p2.setDaemon(true); p2.start(); Thread c1 = new Thread(cust); c1.setDaemon(true); c1.start(); Thread c2 = new Thread(cust); c2.setDaemon(true); c2.start(); //* 主线程休眠,生产者消费者线程运行 */ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } /* 中断生产者线程 */ p1.interrupt(); p2.interrupt(); //c1.interrupt(); //c2.interrupt(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("测试结束!"+buffer); }
运行结果如下:
消费线程Thread-3:2021-10-15 16:41:52:830 开始运行! 消费线程Thread-2:2021-10-15 16:41:52:830 开始运行! 生产线程Thread-1:2021-10-15 16:41:52:830 开始运行! 生产线程Thread-0:2021-10-15 16:41:52:830 开始运行! 生产线程Thread-1:2021-10-15 16:41:52:833 生产数据5 生产线程Thread-0:2021-10-15 16:41:52:833 生产数据67 消费线程Thread-3:2021-10-15 16:41:52:833 消费数据5 消费线程Thread-2:2021-10-15 16:41:52:833 消费数据67 生产线程Thread-0:2021-10-15 16:41:52:944 生产数据87 消费线程Thread-3:2021-10-15 16:41:52:898 消费数据87 生产线程Thread-1:2021-10-15 16:41:52:954 生产数据21 消费线程Thread-2:2021-10-15 16:41:52:926 消费数据21 生产线程Thread-1:2021-10-15 16:41:53:034 生产数据82 消费线程Thread-2:2021-10-15 16:41:53:051 消费数据82 生产线程Thread-0:2021-10-15 16:41:53:065 生产数据30 消费线程Thread-3:2021-10-15 16:41:53:051 消费数据30 生产线程Thread-1:2021-10-15 16:41:53:141 生产数据20 消费线程Thread-2:2021-10-15 16:41:53:119 消费数据20 生产线程Thread-0:2021-10-15 16:41:53:163 生产数据50 消费线程Thread-3:2021-10-15 16:41:53:131 消费数据50 生产线程Thread-0:2021-10-15 16:41:53:275 生产数据5 消费线程Thread-2:2021-10-15 16:41:53:211 消费数据5 生产线程Thread-1:2021-10-15 16:41:53:289 生产数据2 消费线程Thread-3:2021-10-15 16:41:53:274 消费数据2 生产线程Thread-0:2021-10-15 16:41:53:358 生产数据97 消费线程Thread-2:2021-10-15 16:41:53:329 消费数据97 生产线程Thread-1:2021-10-15 16:41:53:362 生产数据15 消费线程Thread-3:2021-10-15 16:41:53:347 消费数据15 生产线程Thread-0:2021-10-15 16:41:53:432 生产数据12 消费线程Thread-2:2021-10-15 16:41:53:434 消费数据12 生产线程Thread-0:2021-10-15 16:41:53:503 生产数据15 消费线程Thread-3:2021-10-15 16:41:53:469 消费数据15 生产线程Thread-1:2021-10-15 16:41:53:511 生产数据57 消费线程Thread-2:2021-10-15 16:41:53:516 消费数据57 生产线程Thread-0:2021-10-15 16:41:53:571 生产数据48 消费线程Thread-2:2021-10-15 16:41:53:579 消费数据48 生产线程Thread-1:2021-10-15 16:41:53:654 生产数据37 消费线程Thread-3:2021-10-15 16:41:53:623 消费数据37 生产线程Thread-0:2021-10-15 16:41:53:719 生产数据95 消费线程Thread-2:2021-10-15 16:41:53:646 消费数据95 生产线程Thread-1:2021-10-15 16:41:53:803 生产数据70 消费线程Thread-3:2021-10-15 16:41:53:763 消费数据70 生产线程Thread-0:2021-10-15 16:41:53:834 被中断! 生产线程Thread-1:2021-10-15 16:41:53:834 被中断!
测试结束![]