1.Lock简介
JDK1.5之后,增加了lock接口,它提供了与synchronized一样的锁功能。拥有锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字不具备的同步特性。
使用lock的形式如下:
Lock lock = new ReentrantLock();
lock.lock();
try{
lock.lock(); //此时其他线程会进入自选状态而不是被阻塞,用synchronized其他线程则会被阻塞
//以下代码只有一个线程可以运行
......
}finally{
lock.unlock();
}
例:
class MyThread implements Runnable{
private int ticket = 200;
private Lock lock = new ReentrantLock();
@Override
public void run() {
for(int i=0; i<200; i++){
try {
lock.lock();
if(ticket > 0){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩下"+ticket--+"票");
}
} finally {
lock.unlock();
}
}
}
}
public class Test2{
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"黄牛A");
Thread thread2 = new Thread(myThread,"黄牛B");
Thread thread3 = new Thread(myThread,"黄牛C");
thread1.start();
thread2.start();
thread3.start();
}
}
2.lock常用API
lock体系拥有可中断的获取锁、超时获取锁以及共享锁等内建锁不具备的特性。synchronized具有的功能,lock体系都具备。
void lock(); //获取锁
void lockInterruptibly() throws InterruptedException(); //响应中断锁
boolean tryLock(); //获取锁返回true,反之返回false
boolean tryLock(long time, TimeUnit unit); //超时获取锁,在规定时间内未获取到锁返回false
Condition newCodition(); //获取与lock绑定的等待通知组件
void unlock(); //释放锁
ReentrantLock中 所有方法实际上都是调用了其静态内部类Sync中的方法,而Sync继承了AbstractQueueSynchronizer (AQS-简称同步器)
3.AQS-同步器
同步器是用来构建锁以及其他同步组件的基础框架,它的实现主要是依赖一个int状态变量以及通过一个FIFO队列共同构成同步队列。
子类必须重写AQS的用protected(继承权限)修饰的用来改变同步状态的方法,其方法主要是实现了排队与阻塞机制。int状态的更新使用getState()、setState()以及compareAndSetState()。
子类推荐使用静态内部类来继承AQS来实现自己的同步语义。同步既支持独占锁,也支持共享锁。
4.AQS的模板模式
AQS使用模板方法模式,将一些与状态相关的核心方法开放给子类重写,而后AQS会用子类重写的关于状态的方法进行线程的排队、阻塞以及唤醒等操作。
锁与AQS的关系:
锁面对使用者,定义了使用者与锁交互的接口
同步器面向锁的实现,简化了锁的实现方式,屏蔽了同步状态管理,线程排队。等待、唤醒等操作。
//自定义锁
class Mutex implements Lock{
private Sync sync = new Sync();
//自定义同步器
static class Sync extends AbstractQueuedSynchronizer{
//获取锁
@Override
protected boolean tryAcquire(int arg) {
if(arg != 1){
throw new RuntimeException("arg参数不为1!");
}
if(compareAndSetState(0,1)){
//此时线程成功获取同步状态
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//尝试释放锁
@Override
protected boolean tryRelease(int arg) {
if(getState() == 0){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//判断当前线程是否是持有线程
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,time);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
public class Test2{
public static void main(String[] args) {
Lock lock = new Mutex();
for(int i=0; i<10; i++){
Thread thread = new Thread(()->{
try {
lock.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
});
thread.start();
}
}
}
5.AQS详解
在同步组件中(就是锁),AQS是最核心的部分,同步组件的实现依赖AQS提供的模板方法来实现同步组件语义。
AQS实现了对同步状态的管理,以及对阻塞线程进行排队、等待通知等底层实现。
AQS核心组成:同步队列、独占锁的获取与释放、共享锁的获取与释放、可中断锁、超时锁。这一系列功能的实现依赖于AQS提供的模板方法。
5.1独占锁
1.void acquire(int arg); // 独占式获取同步状态,如果获取失败插入同步队列进行等待
2.void acquireInterruptibly(int arg); //在1的基础上,此方法可以在同步队列中进行响应中断
3.boolean tryAcquireNanos(int arg, long nanos TimeOut); //在2的基础上增加了超时等待功能,到了时间还未获得锁直接返回
4.boolean tryAcquire(int arg); //获取锁成功返回true,否则返回false
5.boolean release(int arg); //释放同步状态,该方法会唤醒在同步队列的下一个节点
5.2共享式锁
1.void acquireShared(int arg); //共享获取同步状态,同一时刻多个线程获取同步状态。
2.void acquireSharedInterruptibly(int arg); //在1的基础上增加响应中断
3.boolean tryAcquireSharedNanos(int arg,long nanosTimeOut); //在2的基础上增加超时等待。
4.boolean releaseShared(int arg); //共享式释放同步状态。
5.3同步队列
在AQS内部有个静态内部类Node,就是同步队列中每个具体的节点。
public class Test2{
public static void main(String[] args) {
Lock lock = new ReentrantLock();
for(int i=0; i<10; i++){
Thread thread = new Thread(()->{
try {
lock.lock();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
});
thread.start();
}
}
}
节点中有如下属性:
int waitStatus:节点状态
Node prev:前驱节点
Node next:后继节点
Thread thread:当前节点包装的线程对象
Node nextWaiter:等待队列中的下一个节点
节点状态值如下:
int INITIAL = 0; //初始状态
int CANCELLED = 1; //当前节点从同步队列中取消
int SIGNAL = -1; //后继节点处于阻塞(但还在同步队列中)。如果当前节点释放同步状态会通知后继节点,使后继节点继续运行。
int CONDITION = -2; //节点处于等待队列中。但其他线程对Condition调用signal()方法后,该节点会从等待队列中移到同步队列中
int PROPAGATE = -3; //共享式同步状态会无条件的传播
AQS同步队列采用带有头尾节点的双向链表
5.4独占锁的获取:acquire(int arg)
public void lock() {
sync.lock();
}
----------------------------------------------------------
final void lock() {
//判断当前CAS的状态,如果为0则更新为1
if (compareAndSetState(0, 1))
//将当前线程置为持有线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
获取锁失败后,调用AQS提供的acquire(int arg)模板方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg):再次尝试获取同步状态,成功直接方法退出,失败调用addWaiter();
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
addWaiter(Node.EXCLUSIVE):将当前线程以指定模式(独占式,共享式)封装为Node节点后置入同步队列
private Node addWaiter(Node mode) {
//将线程以指定模式(独占式或共享式)封装为Node节点
Node node = new Node(Thread.currentThread(), mode);
//获取当前队列的尾节点
Node pred = tail;
//若尾节点不为空
if (pred != null) {
node.prev = pred;
//使用CAS将当前节点尾插到同步队列中
if (compareAndSetTail(pred, node)) {
pred.next = node;
//CAS尾插成功,返回当前Node节点
return node;
}
}
//尾节点为空 || CAS尾插失败
enq(node);
return node;
}
问题:为什么会尾插失败?
CAS有V,N,O
当期望值与实际存储的值不相等时,就会尾插失败
enq(Node node):当前队列为空或者CAS尾插失败调用此方法来初始化队列或不断尾插
private Node enq(final Node node) {
//直到将当前节点插入到同步队列成功为止
for (;;) {
Node t = tail;
//初始化同步队列
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//不断CAS将当前节点尾插到同步队列中
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued()获取锁成功的条件:当前节点前驱为头结点并且再次获取同步状态成功。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋
for (;;) {
//获取当前节点的前驱节点
final Node p = node.predecessor();
//获取同步状态成功的条件
//前驱节点为头结点并且获取同步状态成功
if (p == head && tryAcquire(arg)) {
//将当前节点设置为头结点
setHead(node);
//删除原来的头结点
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//获取失败将当前节点取消
if (failed)
cancelAcquire(node);
}
}
节点在同步队列中获取锁失败后调用shouldParkAfterFailedAcquire(Node prev, Node node),此方法主要逻辑是使用CAS将前驱状态置为SIGNAL,表示需要将当前节点阻塞。如果CAS失败,不断自旋直到前驱节点状态置为SIGNAL为止。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的节点状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
//前驱节点已被取消
if (ws > 0) {
//不断重试直到找到前驱节点状态不为取消状态
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//前驱节点状态不是取消状态时,将前驱节点的状态置为-1,表示后继节点应该处于等待状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
acquireQueued():
1.如果当前的前驱节点为头结点并且能够成功获取同步状态,当前线程获得锁成功,方法执行结束。
2.如果获取锁失败,先不断自旋将前驱节点状态置为SIGNAL,而后调用LockSupport.park()方法将当前线程阻塞。
5.5独占锁的释放:release()
unlock()方法实际调用AQS提供的release()模板方法。
release()方法是unlock()方法的具体实现。首先获取头结点的后继节点,当后继节点不为null,会调用LockSupport.unpark()方法唤醒后继节点包装的线程。因此,每一次释放后就会唤醒队列中该节点的后继节点所包装的线程。
public final boolean release(int arg) {
//释放状态成功后
if (tryRelease(arg)) {
//获取当前同步队列的头结点
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
5.6独占式锁获取与释放总结:
1.线程获取锁失败,将线程调用addWaiter()封装成Node节点进行入队操作。addWaiter中方法enq()完成对同步队列的头结点初始化以及CAS尾插失败后的重试处理。
2.入队之后排队获取锁的核心方法acquireQueued(),节点排队获取锁是一个自旋过程。当且仅当当前节点的前驱节点是头结点并且获取同步状态时,节点出队并且该节点引用的线程获取锁。否则不满足条件时会不断自旋将前驱节点的状态置为SIGNAL而后调用LockSupport.park()将当前线程阻塞。
3.释放锁时会唤醒后继节点(后继节点不为null)
独占锁特性:
获取锁时响应中断,原理与acquire()几乎一样,唯一区别在于当parkAndCheckInterrupt()返回true时表示线程阻塞时被中断,抛出中断异常后线程退出。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException(); //区别在这
}
} finally {
if (failed)
cancelAcquire(node);
}
}
超时等待获取锁,tryAcquireNanos(),该方法在三种情况下回返回 :
I.在超时时间内,当前线程成功获取到锁
II.当前线程在超时时间内被中断
III.超时时间结束,仍未获取到锁,线程退出返回false。
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
// 实现超时等待的效果
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 1.根据超时时间和当前时间计算出截止时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 2.当前线程获得锁出队列
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 3.1 重新计算超时时间
nanosTimeout = deadline - System.nanoTime();
// 3.2 已经超时返回false
if (nanosTimeout <= 0L)
return false;
// 3.3 线程阻塞等待
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 3.4 线程被中断抛出被中断异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
超时获取锁逻辑与可中断获取锁基本一致,唯一区别在于获取锁失败后,增加了一个时间处理,如果当前时间超过截止时间,线程不在等待,直接退出,返回false。否则将线程阻塞置为等待状态排队获取锁。
6.再次理解ReentrantLock-独占式重入锁(使用频率最高的一种Lock实现)
重入:表示能够对共享资源重复加锁,即当前线程再次获取锁时不会被阻塞。
6.1重入如何实现?
如果该同步状态不为0,表示此时同步状态已被线程获取。再判断持有同步状态的线程是否为当前线程,如果是,同步状态再次加1,并返回true,表示持有线程重入同步块。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//判断当前线程是否是持有锁线程
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
释放过程:
当且仅当同步状态减为0并且持有线程为当前线程时,表示锁被正确释放。否则调用setState()将减1后的状态设置回去。
IllegalMoniyorStateException():非持有锁线程调用tryRelease()方法抛出此异常
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//将当前的c在仍回去,在重试释放
setState(c);
return free;
}
6.2公平锁与非公平锁
公平锁:锁的获取顺序符合时间上的顺序,即等待时间最长的线程最先获取锁。
public ReentrantLock() {
//默认为非公平锁
sync = new NonfairSync();
}
要使用公平锁,调用ReentrantLock有参构造传入true,获取内置的公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
非公平锁获取锁实现:上来先CAS,自旋一次,获取失败则进入等待队列
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
公平锁获取锁实现:上来就直接进入等待队列
获取同步状态之前先判断县当前节点是否有前驱节点,如果有,获取失败
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
公平锁保证了获取到锁的线程一定是等待时间最长的线程,保证了请求资源时间上的绝对顺序,需要频繁的进行上下文交换,性能开销较大。
非公平锁保证了系统有更大的吞吐量(效率较高),但是会造成线程“饥饿现象”(有的线程可能永远无法获取到锁)
release()方法唤醒当前节点最近的一个非空节点,最大可能保证公平性。
7.ReentrantReadWriterLock详解(可重入读写锁)
内建锁默认都是独占锁
读写锁:允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程以及其他写线程均会被阻塞。
写线程能够获取到锁的前提条件:没有任何读、写线程拿到锁。在任何写线程在写的时候,所有的读线程全被阻塞。
7.1写锁获取-WriterLock-独占锁
7.1.1写锁获取-模板方法tryAcquire()
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//判断当前同步状态
int c = getState();
//判断写锁的获取次数
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//获取写锁成功
setExclusiveOwnerThread(current);
return true;
}
同步状态的低16位表示写锁次数,高16位表示读锁次数。
写锁获取逻辑:
当读锁已被读线程获取或者写锁已被其他线程获取,则写线程获取失败;否则,当前同步状态没有任何读写线程获取,当前线程获取写锁成功并且支持重入。
写锁释放逻辑:
同独占锁的释放(release)逻辑
7.2读锁-共享式锁-tryAcquireShared()
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
7.3锁降级
写锁可以降级为读锁,但是读锁不能升级为写锁。
8.Condition接口的await、signal机制
8.1与内建锁wait、notify的区别
I.Object类提供的wait、notify方法(都是native方法)是与对象监视器monitor配合完成线程的等待与通知机制,属于JVM底层实现;而Condition与lock配合完成的等待通知机制属于java语言级别,具有更高的控制与扩展性。
II.Condition独有特性:
1.支持不响应中断,而Object不支持
2.支持多个等待队列,而Object只有一个
3.支持截止时间设置(在截止时间之前都有效),而Object不支持(可以设置等待时长)
等待方法:
1.void await() throws InterruptedException //同Object.wait(),直到被中断或唤醒
2.void awaitUninterruptibly() //不响应中断,直到被唤醒
3.boolean await(long time,TimeUnit unit) throws InterruptedException
//同Object.wait(long timeout),多了自定义时间单位,出现这三种情况返回(中断、超时、被唤醒)。
4.boolean awaitUntil(Date deadline) throws InterruptedException //支持设置截止时间;
唤醒方法:
signal() //唤醒一个等待在condition上的线程,将该线程由等待队列转移到同步队列中
signalAll() //将所有等待在condition上的线程全部转移到同步队列中。
8.2等待队列
等待队列与同步队列共享了Node节点类,等待队列是一个单向的带有头尾节点的队列。
public class Test3{
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
for(int i=0; i<5; i++){
Thread thread = new Thread(()->{
try {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
});
thread.start();
}
}
}
经过调试可看Node属性:
8.3Condition机制实现生产-消费者模型
class Goods{
private String name;
//当前商品数量
private int count;
private int maxCount;
private Lock lock = new ReentrantLock();
//消费者等待队列
private Condition consumerCondition = lock.newCondition();
//生产者等待队列
private Condition producerCondition = lock.newCondition();
public Goods(int maxCount) {
this.maxCount = maxCount;
}
public void setGoods(String name){
lock.lock();
try {
//商品数量达到最大值,生产者线程进入生产者等待队列
while(count == maxCount){
try {
System.out.println(Thread.currentThread().getName()+"还有很多商品,歇会~");
producerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
count++;
System.out.println(Thread.currentThread().getName()+"生产"+toString());
//唤醒处于消费者队列的线程
consumerCondition.signalAll();
} finally {
lock.unlock();
}
}
@Override
public String toString() {
return "Goods{" +
"name='" + name + '\'' +
", count=" + count+
'}';
}
public void getGoods(){
try {
lock.lock();
while(count == 0){
System.out.println(Thread.currentThread().getName()+"还没有商品");
try {
consumerCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"消费"+toString());
} finally {
lock.unlock();
}
}
}
class Producer implements Runnable{
private Goods goods;
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
while(true){
this.goods.setGoods("生产口红一套");
}
}
}
class Consumer implements Runnable{
private Goods goods;
@Override
public void run() {
while(true){
this.goods.getGoods();
}
}
public Consumer(Goods goods) {
this.goods = goods;
}
}
public class Test3{
public static void main(String[] args) {
Goods goods = new Goods(20);
Producer producer = new Producer(goods);
Consumer consumer = new Consumer(goods);
List<Thread> list = new ArrayList<>();
//启动10个消费者线程
for(int i=0; i<10; i++){
Thread thread = new Thread(consumer,"消费者"+i);
list.add(thread);
}
//启动5个生产者
for(int i=0; i<5; i++){
Thread thread = new Thread(producer,"生产者"+i);
list.add(thread);
}
for(Thread th : list){
th.start();
}
}
}