线程之synchronized/lock的condition接口
一.线程之synchronized
二.Java并发编程系列之Condition接口
三.死磕Java并发-----J.U.C之Condition
--------
1.线程之Synchronized
1.Synchronized关键字
1. 定义:Java中1个关键字
2. 作用:
保证同一时刻最多只有1个线程执行。被Synchronized修饰的方法 /代码,其他线程必须等待当前线程执行完该方法 /代码块后才能执行该方法/代码块。
3. 应用场景:
保证线程安全,解决多线程中的并发同步问题(实现的是阻塞型并发),具体场景如下:
修饰 实例方法 / 代码块时,(同步)保护的是同一个对象方法的调用 & 当前实例对象
修饰 静态方法 / 代码块时,(同步)保护的是 静态方法的调用 & class 类对象
4. 原理:
依赖JVM实现同步,底层通过一个 监视器对象(monitor)完成, wait()、notify() 等方法也依赖于 monitor 对象。
监视器锁(monitor)本质依赖于底层操作系统的互斥锁(Mutex Lock)实现
5. 具体使用
synchronized用于修饰 代码块、类的实例方法 & 静态方法
5.1 使用规则
5.2 锁的类型 & 等级
由于Synchronized 会修饰 代码块、类的实例方法 & 静态方法,故分为不同锁的类型
具体如下
之间区别:
5.3 使用方式:
public class Test{
// 对象锁:形式1(方法锁)
public synchronized void Method1(){
System.out.println("我是对象锁也是方法锁");
try{
Thread.sleep(500);
}
}
// 对象锁:形式2(代码块形式)
public void Method2(){
synchronized (this){
System.out.println("我是对象锁");
try{
Thread.sleep(500);
}
} } }
// 方法锁(即对象锁中的形式1)
public synchronized void Method1(){
System.out.println("我是对象锁也是方法锁");
try{
Thread.sleep(500);
} }
public class Test{
// 类锁:形式1 :锁静态方法
public static synchronized void Method1(){
System.out.println("我是类锁一号");
try{
Thread.sleep(500);}
}
// 类锁:形式2 :锁静态代码块
public void Method2(){
synchronized (Test.class){
System.out.println("我是类锁二号");
try{
Thread.sleep(500);
}
} }}
5.4 特别注意
Synchronized修饰方法时存在缺陷:若修饰1个大的方法,将会大大影响效率
示例:
若使用Synchronized关键字修饰线程类的run(),由于run()在线程的整个生命期内一直在运行,因此将导致它对本类任何Synchronized方法的调用都永远不会成功。
解决方案: 使用 Synchronized关键字 声明代码块。该解决方案灵活性高:可针对任意代码块 & 任意指定上锁的对象
synchronized(syncObject) {
// 访问或修改被锁保护的共享状态
// 上述方法 必须 获得对象 syncObject(类实例或类)的锁
}
6. 特点:
注:原子性、可见性、有序性的定义
7. 其他控制并发 / 线程同步方式
7.1 Lock、ReentrantLock
区别:
7.2 CAS
定义: Compare And Swap,即比较并交换,是一种解决并发操作的乐观锁。
synchronized锁住的代码块:同一时刻只能由一个线程访问,属于悲观锁
7.2.2 原理
// CAS的操作参数
内存位置(A)
预期原值(B)
预期新值(C)
// 使用CAS解决并发的原理:
// 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;
// 2. 通过死循环,以不断尝试尝试更新的方式实现并发
// 伪代码如下
public boolean compareAndSwap(long memoryA, int oldB, int newC){
if(memoryA.get() == oldB){
memoryA.set(newC);
return true;
}
return false;
}
7.2.3 优点
资源耗费少:相对于synchronized,省去了挂起线程、恢复线程的开销,但,若迟迟得不到更新,死循环对CPU资源也是一种浪费
7.2.4 具体实现方式
使用CAS有个“先检查后执行”的操作,而这种操作是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的
具体过程
// 1. CAS在Java中的体现为Unsafe类
// 2. Unsafe类会通过C++直接获取到属性的内存地址
// 3. 接下来CAS由C++的Atomic::cmpxchg系列方法实现
7.2.5 典型应用:AtomicInteger
对i++ 与i--,通过compareAndSet & 一个死循环实现。而compareAndSet函数内部 = 通过jni操作CAS指令。直到CAS操作成功跳出循环
private volatile int value; // Gets the current value. public final int get() { return value; } public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } } public final int getAndDecrement() { for (;;) { int current = get(); int next = current - 1; if (compareAndSet(current, next)) return current; } }
二.Java并发之J.U.C之Condition
所有源码均来自JDK 1.8。
在没有Lock之前,使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。
在Java SE5后,Java提供Lock接口,Lock提供条件Condition,相对于Synchronized而言,对线程的等待、唤醒操作更加详细和灵活。
Condition是一种广义上的条件队列。为线程提供了一种更为灵活的等待/通知模式,线程在调用await()后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
Condition提供了一系列的方法来对阻塞和唤醒线程:
- await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
- await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
- awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
- awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
- awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
- signal(): 唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
- signal()All:唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
下图是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):
Condtion的实现
获取一个Condition必须要通过Lock的newCondition()。该方法定义在接口Lock下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。
Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,
而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。定义如下:
public class ConditionObject implements Condition, java.io.Serializable {}
Condition基本内容:
1.1 介绍
Condition是在java 1.5才出现的,它用来替代传统Object的wait()、notify()实现线程间协作,相比Object的wait()、notify(),
使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
简单说,它的作用是使得某些线程一起等待某个条件(Condition),只有当该条件具备(signal 或者signalAll方法被调用)时,这些等待线程才会被唤醒,从而重新争夺锁。
1.2 使用
任何一个Java对象,都拥有一组监视器方法,主要包括wait()、notify()、notifyAll()方法,这些方法与synchronized关键字配合使用可以实现等待/通知机制。
Condition接口也提供类似的Object的监视器的方法,主要包括await()、signal()、signalAll()方法,这些方法与Lock锁配合也可以实现等待/通知机制。
相比Object实现监视器方法,Condition接口监视器方法具有一些Object所没有的特性:
Condition接口可以支持多个等待队列,在前面已经提到一个Lock实例可以绑定多个Condition,所以自然可以支持多个等待队列了;
Condition接口支持响应中断;
Condition接口支持当前线程释放锁并进入等待状态到将来的某个时间,也就是支持定时功能;
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
1.3 原理
Lock的本质是AQS,AQS自己维护的队列是当前等待资源的队列,AQS会在资源被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。
而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列。但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:
1. 线程1调用reentrantLock.lock时,尝试获取锁。如果成功,则返回,从AQS的队列中移除线程;否则阻塞,保持在AQS的等待队列中。
2. 线程1调用await方法被调用时,对应操作是被加入到Condition的等待队列中,等待signal信号;同时释放锁。
3. 锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。
4. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。
5. signal方法执行完毕,线程2调用unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。
所以:
发送signal信号只是将Condition队列中的线程加到AQS的等待队列中。只有到发送signal信号的线程调用reentrantLock.unlock()释放锁后,这些线程才会被唤醒。
可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。
1.4 使用Condition接口配合Lock锁的实例如下:
一般而言,都会将Condition变量作为成员变量。当调用await方法后,当前线程会释放锁并进入Condition变量的等待队列,而其他线程调用signal方法后,通知正在Condition变量等待队列的线程从await方法返回,并且在返回前已经获得了锁。
总结:
一个线程获取锁后,通过调用Condition的await()方法,会将当前线程先加入到条件队列中,然后释放锁,
最后通过isOnSyncQueue(Node node)不断自检看节点是否已经在CLH同步队列了?如果是则尝试获取锁,否则一直挂起。
当线程调用signal()后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法唤醒CLH同步队列的首节点。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()竞争同步状态。
与使用Object的监视器方法达到了同样的效果,也许看不出Condition配合Lock锁的优势何在。但是在复杂多线程编程中,这种方式可以体现出其优势。
所以一般使用仍然是Object监视器方法居多。
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); public void conditionWait() throws InterruptedException { lock.lock(); try { //.... condition.await(); }finally { lock.unlock(); } } public void conditionSignal(){ lock.lock(); try { //... condition.signal(); }finally { lock.unlock(); } }
1.5 其他
等待队列
每个Condition对象都包含着一个FIFO队列,该队列是Condition对象通知/等待功能的关键。在队列中每一个节点都包含着一个线程引用,该线程就是在该Condition对象上等待的线程。我们看Condition的定义就明白了:
public class ConditionObject implements Condition, java.io.Serializable { private static final long serialVersionUID = 1173984872572414699L; //头节点 private transient Node firstWaiter; //尾节点 private transient Node lastWaiter; public ConditionObject() {} /** 省略方法 **/ }
从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:
Node里面包含了当前线程的引用。Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。
Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。
等待
调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。
public final void await() throws InterruptedException { // 当前线程中断 if (Thread.interrupted()) throw new InterruptedException(); //当前线程加入等待队列 Node node = addConditionWaiter(); //释放锁 long savedState = fullyRelease(node); int interruptMode = 0; /** * 检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待 * 直到检测到此节点在同步队列上 */ while (!isOnSyncQueue(node)) { //线程挂起 LockSupport.park(this); //如果已经中断了,则退出 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //竞争同步状态 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; //清理下条件队列中的不是在等待条件的节点 if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
此段代码的逻辑是:首先将当前线程新建一个节点同时加入到条件队列中,然后释放当前线程持有的同步状态。然后则是不断检测该节点代表的线程释放出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。
加入条件队列(addConditionWaiter())源码如下:
private Node addConditionWaiter() { Node t = lastWaiter; //尾节点 //Node的节点状态如果不为CONDITION,则表示该节点不处于等待状态,需要清除节点 if (t != null && t.waitStatus != Node.CONDITION) { //清除条件队列中所有状态不为Condition的节点 unlinkCancelledWaiters(); t = lastWaiter; } //当前线程新建节点,状态CONDITION Node node = new Node(Thread.currentThread(), Node.CONDITION); /** * 将该节点加入到条件队列中最后一个位置 */ if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清楚所有状态不为Condition的节点。
fullyRelease(Node node),负责释放该线程持有的锁。
final long fullyRelease(Node node) { boolean failed = true; try { //节点状态--其实就是持有锁的数量 long savedState = getState(); //释放锁 if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; } }
isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true
final boolean isOnSyncQueue(Node node) { //状态为Condition,获取前驱节点为null,返回false if (node.waitStatus == Node.CONDITION || node.prev == null) return false; //后继节点不为null,肯定在CLH同步队列中 if (node.next != null) return true; return findNodeFromTail(node); }
unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除
private void unlinkCancelledWaiters() { Node t = firstWaiter; Node trail = null; while (t != null) { Node next = t.nextWaiter; if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else trail = t; t = next; } }
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。
public final void signal() { //检测当前线程是否为拥有锁的独 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); //头节点,唤醒条件队列中的第一个节点 Node first = firstWaiter; if (first != null) doSignal(first); //唤醒 }
该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。
doSignal(Node first):唤醒头节点
private void doSignal(Node first) { do { //修改头结点,完成旧头结点的移出工作 if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); }
doSignal(Node first)主要是做两件事:1.修改头节点,2.调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。transferForSignal(Node first)源码如下:
final boolean transferForSignal(Node node) { //将该节点从状态CONDITION改变为初始状态0, if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点 Node p = enq(node); int ws = p.waitStatus; //如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
整个通知的流程如下:
- 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
- 如果线程已经获取了锁,则将唤醒条件队列的首节点
- 唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中
- 最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。
三. condition源码
await源码:
整个await过程如下:
1.将当前线程加入Condition锁队列。特别说明的是,这里不同于AQS的队列,这里进入的是Condition的FIFO队列。
2.释放锁。这里可以看到将锁释放了,否则别的线程就无法拿到锁而发生死锁。
3.自旋(while)挂起,直到被唤醒(signal把他重新放回到AQS的等待队列)或者超时或者CACELLED等。
4.获取锁(acquireQueued)。并将自己从Condition的FIFO队列中释放,表明自己不再需要锁(我已经拿到锁了)。
signal就是唤醒Condition队列中第一个非CANCELLED节点线程,而signalAll就是唤醒所有非CANCELLED节点线程,本质是将节点从Condition队列中取出来一个还是所有节点放到AQS的等待队列。
尽管所有Node可能都被唤醒,但是要知道的是仍然只有一个线程能够拿到锁,其它没有拿到锁的线程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。
- public final void await() throws InterruptedException {
- // 1.如果当前线程被中断,则抛出中断异常
- if (Thread.interrupted())
- throw new InterruptedException();
- // 2.将节点加入到Condition队列中去,这里如果lastWaiter是cancel状态,那么会把它踢出Condition队列。
- Node node = addConditionWaiter();
- // 3.调用tryRelease,释放当前线程的锁
- long savedState = fullyRelease(node);
- int interruptMode = 0;
- // 4.为什么会有在AQS的等待队列的判断?
- // 解答:signal操作会将Node从Condition队列中拿出并且放入到等待队列中去,在不在AQS等待队列就看signal是否执行了
- // 如果不在AQS等待队列中,就park当前线程,如果在,就退出循环,这个时候如果被中断,那么就退出循环
- while (!isOnSyncQueue(node)) {
- LockSupport.park(this);
- if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
- break;
- }
- // 5.这个时候线程已经被signal()或者signalAll()操作给唤醒了,退出了4中的while循环
- // 自旋等待尝试再次获取锁,调用acquireQueued方法
- if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
- interruptMode = REINTERRUPT;
- if (node.nextWaiter != null)
- unlinkCancelledWaiters();
- if (interruptMode != 0)
- reportInterruptAfterWait(interruptMode);
- }
四.Condition接口的事例(生产者-消费者模式)
3.案例3 :
public class Main { public static void main(String[] args) { final ReentrantLock reentrantLock = new ReentrantLock(); final Condition condition = reentrantLock.newCondition(); new Thread(new Runnable() { @Override public void run() { reentrantLock.lock(); System.out.println(Thread.currentThread().getName() + "拿到锁了"); System.out.println(currentThread().getName() + "等待信号"); try { condition.await(); } System.out.println(currentThread().getName() + "拿到信号"); reentrantLock.unlock(); } }, "线程1").start(); new Thread(new Runnable() { @Override public void run() { reentrantLock.lock(); System.out.println(currentThread().getName() + "拿到锁了"); try { Thread.sleep(3000); } System.out.println(currentThread().getName() + "发出信号"); condition.signalAll(); reentrantLock.unlock(); } }, "线程2").start(); } } 运行结果: 线程1拿到锁了 线程1等待信号 线程2拿到锁了 线程2发出信号 线程1拿到信号
2. 案例2:用Condition实现生产者消费者问题:
public class ConditionTest { private Lock lock; private Condition fullCondition; private Condition notFullCondition; private LinkedList<String> buffer; //容器 private int maxSize ; //容器最大 ConditionTest(int maxSize){ this.maxSize = maxSize; buffer = new LinkedList<String>(); lock = new ReentrantLock(); fullCondition = lock.newCondition(); notFullCondition = lock.newCondition(); } public void set(String string) throws InterruptedException { lock.lock(); //获取锁 try { while (maxSize == buffer.size()){ notFullCondition.await(); //满了,添加的线程进入等待状态 } buffer.add(string); fullCondition.signal(); } finally { lock.unlock(); //记得释放锁 } } public String get() throws InterruptedException { String string; lock.lock(); try { while (buffer.size() == 0){ fullCondition.await(); } string = buffer.poll(); notFullCondition.signal(); } finally { lock.unlock(); } return string; } }
1.案例1:知道如何配合Condition和Lock锁实现等待/通知机制,使用这种方式实现生产者-消费者模式:
public class ConditionProducerConsumerDemo { private static DateFormat format = new SimpleDateFormat("HH:mm:ss"); static class Info{ private String author; private String title; //是否开始生产的标志 private boolean produce = true; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public Info(){} public Info(String author, String title) { this.author = author; this.title = title; } /** * 生产者执行的生产方法 */ public void set(String author,String title) throws InterruptedException { lock.lock(); try { //没有开始生产就等待 while (!produce){ condition.await(); } //如果已经开始生产 this.setAuthor(author); TimeUnit.SECONDS.sleep(1); this.setTitle(title); //表示已经停止了生产可以取数据了 produce = false; condition.signal();// 通知消费者 }finally { lock.unlock(); } } /** * 消费者执行的消费方法 */ public void get() throws InterruptedException { lock.lockInterruptibly(); try { //如果已经开始生产就等待 while (produce){ condition.await(); } //如果没有在生产就就可以取数据 System.out.println(Thread.currentThread().getName() + ":" + this.getAuthor() + "=" + this.getTitle() + " at " + format.format(new Date())); //表示我已经取了数据,生产者可以继续生产 produce = true; condition.signal();//通知生产者 }finally { lock.unlock(); } } } static class Producer implements Runnable{ private Info info; public Producer(Info info) { this.info = info; } public void run() { boolean flag = true; for (int i = 0; i < 5; i++){ if (flag){ try { info.set("authorA","titleA"); System.out.println(Thread.currentThread().getName() + ":" + info.getAuthor() + "=" + info.getTitle() + " at " + format.format(new Date())); TimeUnit.SECONDS.sleep(1); } flag = false; }else { try { info.set("authorB","titleB"); System.out.println(Thread.currentThread().getName() + ":" + info.getAuthor() + "=" + info.getTitle() + " at " + format.format(new Date())); TimeUnit.SECONDS.sleep(1); } flag = true; } } } } static class Consumer implements Runnable{ private Info info; public Consumer(Info info) { this.info = info; } public void run() { for (int i = 0; i < 5; i++){ try { info.get(); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }}} public static void main(String[] args) throws InterruptedException { Info info = new Info(); Thread producer = new Thread(new Producer(info),"Producer"); Thread consumer = new Thread(new Consumer(info),"Consumer"); producer.start(); TimeUnit.SECONDS.sleep(1); consumer.start(); }}
运行结果如下: