文章目录
- Java并发JUC锁
- 什么是AQS? 为什么它是核心?
- 通过wait/notify实现同步?
- 通过LockSupport的park/unpark实现同步?
- wait/notify与park/unpark区别?
- Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别?重点
- AQS底层使用了什么样的设计模式?
- 什么是可重入,什么是可重入锁?它用来解决什么问题?
- ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?
- ReentrantReadWriteLock底层实现原理?
- ReentrantReadWriteLock底层读写状态如何设计的?
- 读锁和写锁的最大数量是多少?
- 本地线程计数器ThreadLocalHoldCounter是用来做什么的?
- 写锁的获取与释放是怎么实现的?
- 读锁的获取与释放是怎么实现的?
- 什么是锁的升降级?
- ReentrantLock实现生产者消费者模式1(单生产者单消费者)
- ReentrantLock实现生产者消费者模式2(多生产者多消费者)
- ReentrantLock实现生产者消费者模式3(多生产者多消费者)
Java并发JUC锁
什么是AQS? 为什么它是核心?
AQS(Abstract Queued Synchronizer)是一个抽象的队列同步器,通过维护一个共享资源状态(Volatile Int State)和一个先进先出(FIFO)的线程等待队列来实现一个多线程访问共享资源的同步框架。
AQS原理
AQS 为每个共享资源都设置一个共享资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,则将该线程放入线程等待队列,等待下一次资源调度,具体的流程如下图所示。许多同步类的实现都依赖于AQS ,例如常用的ReentrantLock、Semaphore、CountDownLatch。
AQS共享资源的方式:独占式和共享式
独占式:只有一个线程能执行,具体的Java实现有ReentrantLock。根据是否按队列的顺序分为公平锁和非公平锁。
共享式:多个线程可同时执行,具体的Java实现有Semaphore和CountDownLatch。
ReentrantReadWriteLock可以看成是组合式,允许多个线程同时对某一资源进行读。
AQS只是一个框架,只定义了一个接口,具体资源的获取、释放都由自定义同步器去实现。不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等,AQS已经在顶层实现好,不需要具体的同步器再做处理。自定义同步器的主要方法如下图所示:
ReentrantLock对AQS的独占方式实现为:ReentrantLock中的state初始值为0表示无锁状态。在线程执行tryAcquire()获取该锁后ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过tryAcquire()获取锁时均会失败,直到该线程释放锁后state再次为0,其他线程才有机会获取该锁。该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1,因此ReentrantLock也属于可重入锁。但获取多少次锁就要释放多少次锁,这样才能保证state最终为0。如果获取锁的次数多于释放锁的次数,则会出现该线程一直持有该锁的情况;如果获取锁的次数少于释放锁的次数,则运行中的程序会报锁异常。
CountDownLatch对AQS的共享方式实现为:CountDownLatch将任务分为N个子线程去执行,将state初始化为 N,N与线程的个数一致,N个子线程是并行执行的,每个子线程都在执行完成后countDown()1次,state执行CAS操作并减1。在所有子线程都执行完成(state=O)时会unpark()主线程,然后主线程会从await()返回,继续执行后续的动作。
通过wait/notify实现同步?
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notify();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
synchronized (myThread) {
try {
myThread.start();
Thread.sleep(3000);// 主线程睡眠3s
System.out.println("before wait");
myThread.wait();// 阻塞主线程并释放锁
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 运行结果
before wait
before notify
after notify
after wait
具体的执行流程图如下:
注意:使用wait/notify实现同步时,必须先调用wait,后调用notify,如果先调用notify,再调用wait,将起不了作用。具体代码如下:
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notify();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
Thread.sleep(3000); // 主线程睡眠3s
synchronized (myThread) {
try {
System.out.println("before wait");
myThread.wait();// 阻塞主线程并释放锁
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 运行结果
before notify
after notify
before wait
// 由于先调用了notify,再调用的wait,此时主线程还是会一直阻塞。
注意事项:执行notify方法后,当前线程并不会立即释放锁,要等到程序执行完,即退出synchronized同步区域后。
通过LockSupport的park/unpark实现同步?
park,unpark这两个方法都是LockSupport类名下的方法,park用来暂停线程,unpark用来将暂停的线程恢复。
先park再unpark的方式是容易理解的。但还有一个场景,先unpark后再次执行park方法,也不会阻塞调用了park方法的线程。理解为park方法就是校验获取一个通行令牌,而unpark方法是获取到一个通行令牌的过程。先执行unpark方法,代表先获得了通行令牌。那么在另一个线程调用park方法时,校验到这个令牌存在,消耗掉这个令牌然后就可以继续往下走。
@Slf4j
public class ParkUnparkTest {
//写个park,unpark方法,没什么意思
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("暂停");
LockSupport.park();
log.debug("t1结束");
},"t1");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
log.debug("启用");
LockSupport.unpark(t1);
log.debug("t2结束");
},"t2");
t2.start();
}
}
// 运行结果
暂定
启用
t2结束
t1结束
原理:
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter,_cond和_mutex打个比喻:线程就像一个旅人,Parker就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter就好比背包中的备用干粮(0为耗尽,1为充足)。
调用park就是要看需不需要停下来歇息;
如果备用干粮耗尽,那么钻进帐篷歇息;
如果备用干粮充足,那么不需停留,继续前进;
调用unpark就好比令干粮充足;
如果这时线程还在帐篷,就唤醒让他继续前进;
如果这时线程还在运行,增加备用干粮,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进;
因为背包空间有限,多次调用unpark仅会补充一份备用干粮;
1、当前线程调用Unsafe.park()方法。
2、检查_counter,本情况为0,这时获得_mutex互斥锁。
3、线程进入_cond条件变量阻塞。
4、设置_counter=0。
1、调用Unsafe.unpark(Thread_0)方法,设置_counter为1。
2、唤醒_cond条件变量中的Thread_0。
3、Thread_0恢复运行。
4、设置_counter=0。
1、调用Unsafe.unpark(Thread_0)方法,设置_counter为1。
2、当前_cond条件变量中无阻塞线程。
3、当前线程调用Unsafe.park()方法。
4、检查_counter,本情况为1,这时线程无需阻塞,继续运行。
5、设置_counter=0。
wait/notify与park/unpark区别?
1、行为上的不同:
wait/notify组合使用代表了阻塞,唤醒操作,如果先调用notify,当前线程原本就是醒着,唤醒这个操作无效的,再次调用wait,那线程就阻塞了。
park/unpark可以按照通行令牌走,先执行unpark方法,代表先获得了通行令牌。那么在另一个线程调用park方法时,校验到这个令牌存在,然后消耗掉这个令牌就可以继续往下走。
2、wait/notify依赖于锁资源,所以只能在synchronized中来进行使用。park/unpark没有这个限制。
3、wait/notify的唤醒是随机的,不确定具体唤醒了哪个等待的线程,而park/unpark可以在线程层面上来对特定线程进行唤醒。
Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别?重点
1、Thread.sleep()和Object.wait()的区别
a、Thread.sleep()不会释放占有的锁,Object.wait()会释放占有的锁;
b、Thread.sleep()必须传入时间,Object.wait()可传可不传,不传表示一直阻塞下去;
c、Thread.sleep()到时间了会自动唤醒,然后继续执行;
d、Object.wait()如果不带时间的,则需要另一个线程使用Object.notify()唤醒;
e、Object.wait()如果带时间的,假如没有被notify,到时间了会自动唤醒,这时又分好两种情况,一是立即获取到了锁,线程自然会继续执行;二是没有立即获取锁,线程进入同步队列等待获取锁;
综上所述:最大的区别就是Thread.sleep()不会释放锁资源,Object.wait()会释放锁资源。
2、Object.wait()和Condition.await()的区别
Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。
3、Thread.sleep()和LockSupport.park()的区别
a、从功能上来说,Thread.sleep()和LockSupport.park()方法类似,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;
b、Thread.sleep()没法从外部唤醒,只能自己醒过来;LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;
c、Thread.sleep()方法声明上抛出了InterruptedException中断异常,所以调用者需要捕获这个异常或者再抛出;LockSupport.park()方法不需要捕获中断异常;
d、Thread.sleep()本身就是一个native方法;LockSupport.park()底层是调用的Unsafe的native方法;
4、Object.wait()和LockSupport.park()的区别
a、Object.wait()方法需要在synchronized块中执行;LockSupport.park()可以在任意地方执行;
b、Object.wait()方法声明抛出了中断异常,调用者需要捕获或者再抛出;LockSupport.park()不需要捕获中断异常;
c、Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;
AQS底层使用了什么样的设计模式?
模板, 共享锁和独占锁在一个接口类中。
什么是可重入,什么是可重入锁?它用来解决什么问题?
可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入锁:又名递归锁,是指同一个线程在外层方法获取锁之后,进入内层方法再次获取锁时,会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
代码示例:
// synchronized实现
public class WhatReentrant {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
// ReentrantLock实现
public class WhatReentrant2 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);
int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (index == 10) {
break;
}
} finally {
lock.unlock();
}
}
} finally {
lock.unlock();
}
}
}).start();
}
}
ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?
ReentrantLock实现了Lock接口,总共有三个内部类,并且三个内部类是紧密相关的。
ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
FairSync:公平锁
NonfairSync:非公平锁
ReentrantLock默认实现的是非公平锁
ReentrantReadWriteLock底层实现原理?
ReentrantReadWriteLock实现了ReadWriteLock接口,ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。
ReentrantReadWriteLock底层读写状态如何设计的?
高16位为读锁,低16位为写锁
读锁和写锁的最大数量是多少?
2的16次方-1
本地线程计数器ThreadLocalHoldCounter是用来做什么的?
本地线程计数器,与对象绑定(线程-》线程重入的次数)
写锁的获取与释放是怎么实现的?
tryAcquire/tryRelease
读锁的获取与释放是怎么实现的?
tryAcquireShared/tryReleaseShared
什么是锁的升降级?
RentrantReadWriteLock为什么不支持锁升级? RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
ReentrantLock实现生产者消费者模式1(单生产者单消费者)
生产者消费者模式简介:
1、生产者生产货物
2、消费者消费货物
3、生产者当货物充足时停止生产,通知消费者消费货物
4、消费者当货物不足时停止消费,通知生产者生产货物
5、生产者和消费者同时只有一方可以访问存储货物的仓库(即同一时间只有一个线程能够访问公共资源)。
公共资源代码实现:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Storage {
private static final int MAX_COUNT = 10; // 仓库的最大容量
private List<Production> mProductions = new ArrayList<Production>(); // 公共资源,需要互斥的地方
private ReentrantLock mReentrantLock = new ReentrantLock();
private Condition mCondition = mReentrantLock.newCondition();
private int mIndex; // 货物索引
public void produce() {
try {
mReentrantLock.lock(); // 获取锁,再访问公共资源
if (mProductions.size() >= MAX_COUNT) {
System.out.println("produce await");
mCondition.await(); // 货物充足时停止生产
}
Thread.sleep((long) (Math.random() * 1000)); // 生成的耗时
Production production = new Production(mIndex++);
System.out.println("producer produce: " + production.toString());
mProductions.add(production);
mCondition.signal(); // 发个信号告知消费者
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mReentrantLock.unlock(); // 放在finally块中保证一定会释放锁
}
}
public void consume() {
try {
mReentrantLock.lock(); // 获取锁,再访问公共资源
if (mProductions.size() <= 0) {
System.out.println("consume await");
mCondition.await(); // 货物不足时停止消费
}
Thread.sleep((long) (Math.random() * 1000)); // 消费的耗时
Production production = mProductions.remove(0);
System.out.println("consumer consume: " + production.toString());
mCondition.signal(); // 发个信号告知生产者
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mReentrantLock.unlock(); // 放在finally块中保证一定会释放锁
}
}
public static class Production {
public int index;
public Production(int index) {
this.index = index;
}
@Override
public String toString() {
return "Production [index=" + index + "]";
}
}
}
生产者线程:
public class Producer extends Thread {
private Storage mStorage;
public Producer(Storage storage) {
this.mStorage = storage;
}
@Override
public void run() {
while (!Thread.interrupted()) {
mStorage.produce(); // 不停的生产
}
}
}
消费者线程:
public class Consumer extends Thread {
private Storage mStorage;
public Consumer(Storage mStorage) {
this.mStorage = mStorage;
}
@Override
public void run() {
while (!Thread.interrupted()) {
mStorage.consume(); // 不停的消费
}
}
}
场景模拟:
public class Main {
public static void main(String[] args) {
Storage storage = new Storage(); // 创建一个仓库
Producer producer = new Producer(storage); // 创建生产者线程
Consumer consumer = new Consumer(storage); // 创建消费者线程
producer.start();
consumer.start();
}
}
ReentrantLock实现生产者消费者模式2(多生产者多消费者)
仓库类:
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Storage2 {
private final int MAX_SIZE = 10;
private LinkedList<Object> storage2 = new LinkedList<>();
private final Lock lock = new ReentrantLock();
private final Condition full = lock.newCondition();
private final Condition empty = lock.newCondition();
public void produce() {
lock.lock();
while (storage2.size() + 1 > MAX_SIZE) {
System.out.println("生产者" + Thread.currentThread().getName() + ": 仓库已满");
try {
full.await();//相当于thread中的wait
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage2.add(new Object());
System.out.println("生产者" + Thread.currentThread().getName() + "生产了一个产品,现存:" + storage2.size());
empty.signalAll();//相当于thread中的notifyall()
lock.unlock();
}
public void consume() {
lock.lock();
while (storage2.size() == 0) {
System.out.println("消费者" + Thread.currentThread().getName() + ": 仓库已空");
try {
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage2.remove();
System.out.println("消费者" + Thread.currentThread().getName() + "消费了一个产品,现存:" + storage2.size());
empty.signalAll();
lock.unlock();
}
}
生产者线程:
public class Producer2 implements Runnable {
private Storage2 storage;
public Producer2() {
}
public Producer2(Storage2 storage) {
this.storage = storage;
}
public void run() {
while (true) {
try {
Thread.sleep(1000);
storage.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者线程:
public class Consumer2 implements Runnable {
private Storage2 storage;
public Consumer2() {
}
public Consumer2(Storage2 storage) {
this.storage = storage;
}
public void run() {
while (true) {
try {
Thread.sleep(3000);
storage.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
模拟场景:
public class ProducerConsumerTest {
public static void main(String[] args) {
Storage2 storage = new Storage2();
Thread p1 = new Thread(new Producer2(storage), "1");
Thread p2 = new Thread(new Producer2(storage), "2");
Thread p3 = new Thread(new Producer2(storage), "3");
/*new Thread((new Producer(storage))).start();
new Thread((new Producer(storage))).start();
new Thread((new Producer(storage))).start();*/
Thread c1 = new Thread(new Consumer2(storage), "1");
Thread c2 = new Thread(new Consumer2(storage), "2");
Thread c3 = new Thread(new Consumer2(storage), "3");
/*new Thread((new Consumer(storage))).start();
new Thread((new Consumer(storage))).start();
new Thread((new Consumer(storage))).start();*/
p1.start();
p2.start();
p3.start();
c1.start();
c2.start();
c3.start();
}
}
// 运行结果
生产者3生产了一个产品,现存:1
生产者1生产了一个产品,现存:2
生产者2生产了一个产品,现存:3
生产者3生产了一个产品,现存:4
生产者2生产了一个产品,现存:5
生产者1生产了一个产品,现存:6
消费者1消费了一个产品,现存:5
消费者3消费了一个产品,现存:4
消费者2消费了一个产品,现存:3
生产者2生产了一个产品,现存:4
生产者3生产了一个产品,现存:5
生产者1生产了一个产品,现存:6
生产者3生产了一个产品,现存:7
生产者2生产了一个产品,现存:8
生产者1生产了一个产品,现存:9
生产者3生产了一个产品,现存:10
生产者2: 仓库已满
生产者1: 仓库已满
消费者2消费了一个产品,现存:9
消费者3消费了一个产品,现存:8
消费者1消费了一个产品,现存:7
生产者3生产了一个产品,现存:8
生产者3生产了一个产品,现存:9
生产者3生产了一个产品,现存:10
消费者2消费了一个产品,现存:9
消费者1消费了一个产品,现存:8
消费者3消费了一个产品,现存:7
一、仓库类中为什么判断是否为空的条件语句使用while而不是if?
理由:当生产者判断当前仓库为满之后,调用await()进入阻塞,当他重新拿到锁之后,代码会从await()方法之后开始执行,如果使用if,不对仓库库存再进行判断,此时如果有多个生产者处于await()后会被唤醒,那么就有可能导致会往满队列中再添加元素。
二、当前condition调用await之后,锁去了哪?
conditon的await()方法相当于Object的wait()方法,它主要实现三件事,先将当前线程封装成一个node,装进等待队列中,然后释放锁,唤醒同步队列中的下一个节点。
ReentrantLock实现生产者消费者模式3(多生产者多消费者)
示例代码:
public class ProviderConsumerDemo<E> {
private static int queueSize = 10;
private LinkedBlockingDeque<Node<E>> blockingDeque = new LinkedBlockingDeque<>(queueSize);
public LinkedBlockingDeque<Node<E>> getQueue() {
return blockingDeque;
}
private static ReentrantLock lock = new ReentrantLock();
// 定义信号量 notFull,给生产者使用,没有满,那么可以继续投放元素
private static Condition notFull = lock.newCondition();
// 定义信号量 notEmpty,给消费者使用,没有空,那么可以继续消耗元素
private static Condition notEmpty = lock.newCondition();
static class Node<E> {
private E item;
private Node<E> next;
public Node(E item) {
this.item = item;
}
public void setNext(Node<E> next) {
this.next = next;
}
public String traverse() {
String currentValue = this.item.toString();
return this.next == null ? currentValue : this.next.traverse();
}
@Override
public String toString() {
return "" + this.item;
}
}
// 队列没满就往队列里添加元素
static class Provider implements Runnable {
private LinkedBlockingDeque<Node<Integer>> blockingDeque;
public Provider(LinkedBlockingDeque<Node<Integer>> blockingDeque) {
this.blockingDeque = blockingDeque;
}
@Override
public void run() {
lock.lock();
try {
// 队列满了,那么就停止投放
while (blockingDeque.size() == queueSize) {
notFull.await();
}
Node<Integer> node = new Node<>((int) (Math.random() * 20));
blockingDeque.add(node);
System.out.println(Thread.currentThread().getName() + "往队列中投入一个元素:" + node.item.toString());
// 队列里有元素了,可以唤醒阻塞线程拿元素
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
// 队列里有元素,那么就从队列里拿出一个元素
static class Consumer implements Runnable {
private LinkedBlockingDeque<Node<Integer>> blockingDeque;
public Consumer(LinkedBlockingDeque<Node<Integer>> blockingDeque) {
this.blockingDeque = blockingDeque;
}
@Override
public void run() {
lock.lock();
try {
// 队列空了,那么就停止消耗
while (blockingDeque.isEmpty()) {
notEmpty.await();
}
Node<Integer> node = blockingDeque.poll();
System.out.println(Thread.currentThread().getName() + "从队列中拿到一个元素:" + node.item.toString());
// 可继续投放
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ProviderConsumerDemo<Integer> providerConsumerDemo = new ProviderConsumerDemo<>();
LinkedBlockingDeque<Node<Integer>> blockingDeque = providerConsumerDemo.getQueue();
for (int i = 0; i < 10; i++) {
new Thread(new Provider(blockingDeque)).start();
}
for (int j = 0; j < 10; j++) {
new Thread(new Consumer(blockingDeque)).start();
}
}
}
// 运行结果
Thread-0往队列中投入一个元素:2
Thread-2往队列中投入一个元素:4
Thread-3往队列中投入一个元素:8
Thread-1往队列中投入一个元素:15
Thread-4往队列中投入一个元素:14
Thread-5往队列中投入一个元素:16
Thread-6往队列中投入一个元素:0
Thread-7往队列中投入一个元素:2
Thread-8往队列中投入一个元素:3
Thread-9往队列中投入一个元素:3
Thread-10从队列中拿到一个元素:2
Thread-11从队列中拿到一个元素:4
Thread-12从队列中拿到一个元素:8
Thread-13从队列中拿到一个元素:15
Thread-14从队列中拿到一个元素:14
Thread-15从队列中拿到一个元素:16
Thread-16从队列中拿到一个元素:0
Thread-17从队列中拿到一个元素:2
Thread-19从队列中拿到一个元素:3
Thread-18从队列中拿到一个元素:3
Process finished with exit code 0