Java并发JUC锁

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值