多线程.
一、程序、进程、线程.
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象
进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。 —— 生命周期
程序 是静态的; 进程 是动态的
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器(pc),线程切换的开销小
一个进程中的多个线程共享相同单元/内存地址空间 –> 它们从同一堆中分配对象,可以方访问相同的变量和对象。
这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能带来安全隐患
二、线程的创建(2)和使用.
创建.
创建多线程的方式一:继承Thread类.
1.继承java.lang.Thread
.
start()
作用:①启动当前线程;②调用当前线程的run()
这个方法只能执行一次
要想创建多个线程,必须创建多个子类对象,然后调用各自的
start()
面对不同进程内容,需要创建多个子类,重写出不同的
run()
使用匿名子类,则无需创建多个子类
/* 方式一: 继承Thread类,重写run方法
* 1. 创建子类
* 2. 重写run方法
* 3. 创建子类对象
* 4. 调用start方法
**/
//1.
public class ThreadTest01 extends Thread {
//2.
@Override
public void run() {
for (int i = 0; i < 1000; i++){
System.out.println(i + "-thread");
}
}
public static void main(String[] args) {
//3.
ThreadTest01 t = new ThreadTest01();
//4.
t.start();
for (int i = 0; i < 1000; i++){
System.out.println(i + "-main");
}
}
}
2.创建java.lang.Thread
的匿名子类.
相比于上一种创建的好处:无需创建子类的实体
缺点:无法复用代码
当要执行的线程,无需复用,可用这种创建方法
/**
* @decription 线程的创建
* 第一种方法延伸:匿名子类
* 1.创建匿名子类的非匿名对象
* 2.重写run()方法
* 3.start()即可
*
**/
public class ThreadTest02 {
public static void main(String[] args) {
//1.
Thread thread1 = new Thread(){
//2.
@Override
public void run() {
//打印 0 - 9
for (int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
//打印 0 2 4 6 8
for (int i = 0; i < 10; i+=2){
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
};
//3
thread1.start();
thread2.start();
}
}
创建多线程的方式二:实现Runnable接口.
Thread implements Runnable
/** 方式二:实现Runnable接口
* 1. 创建实现类
* 2. 重写抽象方法(run())
* 3. 创建实现类的对象
* 4. 将此对象作为参数,传到Thread的构造其中,创建Thread对象
* 5. 通过Thread对象,调用start()启动线程
**/
//1.
public class ThreadTest05 implements Runnable{
//2.
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
//3.
ThreadTest05 runnable = new ThreadTest05();
//4.
Thread t = new Thread(runnable);
//5.
t.start();
//main线程
System.out.println(Thread.currentThread().getName());
}
}
比较这两种创建方式.
- 开发中优先使用:实现Runnable接口的方式
- 实现没有,类的单继承性的局限性
- 实现更适合处理多个线程有共享数据的情况。(可以看补充中窗口卖票)
- 两种方式都需要重写
run()
方法
使用.
(常用方法).
1、start()
.
- 启动当前线程,调用当前线程的run()方法
2、run()
.
- 通常需要重写
Thread
类中的run()
方法,将创建的线程需要执行的操作声明在此方法中
3、currentThread()
.
- 静态方法,返回执行当前代码的线程
4、getName()
.
- 获取当前线程名
5、setName()
.
- 设置当前线程名
- 也可以使用父类的有参构造器,对线程命名
6、yield()
.
静态方法,释放当前线程对CPU的执行权
- 释放了执行权,不一定代表会被其他线程获取执行权,有可能还是当前线程又一次获取执行权
public class ThreadTest03{ public static void main(String[] args) { Thread thread = new Thread(){ @Override public void run() { for(int i = 0; i < 20; i++){ //this 表示 thread 这个对象 System.out.println(this.getName() + " " + i); } } }; //启动线程 thread.start(); //main线程执行 for(int i = 0; i < 20; i++){ System.out.println(Thread.currentThread().getName() + " " + i); //如果是4的被数就释放 CPU执行权 if(i % 4 == 0){ Thread.currentThread().yield(); } } } }
7、join()
.
在线程a中调用线程b的
join()
,此时线程a进行阻塞状态,直到线程b执行完,线程a才结束阻塞状态当然,a也不一定获取执行权(如果还有其他线程)
public class ThreadTest04 { public static void main(String[] args) { Thread t = new Thread(){ @Override public void run() { for(int i = 0; i < 20; i++){ System.out.println(this.getName() + " " + i); } } }; //开启线程 t.start(); //main线程执行 for (int i = 0; i < 20; i++){ System.out.println(Thread.currentThread().getName() + " " + i); //如果 i = 10 让 主线程阻塞 ,加入 thread-0 线程 直到其执行完 if(i == 10){ try { t.join(); //Interrupted 中断 } catch (InterruptedException e) { e.printStackTrace(); } } } } }
8、stop()
@Deprecated(since=”1.2”).
- 强制结束当前线程,已过时,不推荐使用
9、sleep(ms)
.
- 静态方法,休眠(阻塞)当前线程且指定休眠时间,单位ms
- 当然休眠结束,当前也不一定立即获取执行权
10、isAlive()
.
- 判断当前线程是否仍然存活
- 一般放在主线程里,
线程对象.isAlive()
(线程优先级).
线程调度请看补充
1、优先级等级(常量,整型).
MAX_PRIORITY: 10
最大优先级MIN_PRIORITY: 1
最小优先级NORM_PRIORITY: 5
正常(默认)优先级
2、方法.
getPriority()
.
- 获取当前线程的优先级
setPriority(1~10)
.
- 设置当前线程的优先级
优先级高会抢占优先级低的,是代表获取CPU的执行权概率高,并不是意味着,优先级高的线程执行完成后才执行优先级低
三、线程的生命周期.
JDK中用Thread.State
内部类定义了6种状态

操作系统五种状态.
新建: 当一个Thread
类或其子类的对象被声明并创建时,新生的线程对象处于新建
状态
就绪:处于新建状态的线程被start()
后,将进入线程队列等待CPU时间片,此时它已
具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()
方法定义了线
程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
Java线程的六种状态.
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程刚被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java系统系统中将操作系统中的就绪和运行两种状态笼统地称为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITTING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程做出一些特定动作(通知或者中断) |
TIME_WAITTING | 超时等待状态,该状态不同于等待状态,它可以在指定的时间后自行返回 |
TERMINATED | 中止状态,表示当前线程已经执行完毕 |
四、线程的同步(3).
1、同步代码块 synchronized
.
需要被同步的代码 : 操作共享数据的代码
共享数据 : 多个线程共同操作的变量
同步监视器,俗称锁 – 任何一个对象都可以充当锁 要求多个线程必须共用同一把锁(这个锁对应每个线程是唯一的 且 是同一个)
锁的三种形式
①在声明一个域对象(成员变量)充当锁(如果是继承Thread – static成员变量)
②当前对象this(只适用于实现Runnable)
③直接以该类充当锁 – 类名.class(推荐)
有共享数据(多线程同时操作) 才会有线程安全问题
synchronized(同步监视器){
//需要被同步的代码
}
//好处 -- 解决了线程安全问题
//但是 -- 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低
2、同步方法.
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
如果是实现
Runnable
接口需要使用非静态的同步方法(当然静态也可以,只不过同步数据也要是静态的),它的默认同步监视器:this
;不适用与继承Thread的如果是继承
Thread
需要使用static
修饰同步方法,它的默认同步监视器:当前类
//适用于 实现Runnable 接口产生的线程安全问题
private synchronized void method{// 默认的锁是:this -- 当前对象
//需要被同步的代码
}
//适用于 继承Thread类 产生的线程安全问题
private static synchronized void method{// 默认的锁是:当前类
//需要被同步的代码
}
3、Lock(锁) – 同步锁.
JDK5.0
:通过显示定义同步锁对象来实现同步。
java.util.concurrent.locks.Lock
接口 是控制多个线程对共享资源进行访问的工具
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应获得Lock对象
ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显示加锁,释放锁
private [static] ReentrantLock lock = new ReentrantLock();
//上锁
lock.lock();
try{
//需要同步的代码
}finally{
//释放锁
lock.unlock();
}
五、线程的通信.
wait()、notify()、notifyAll()
的使用
wait()
:一旦执行此方法,当前线程就进入阻塞状态,并且释放锁notify()
:一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个notifyAll()
:一旦执行此方法,就会唤醒所有被wait的线程
wait() , notify(), notifyAll()
三个方法必须使用在同步代码块或同步方法中这三个方法的调用者,必须是同步代码块或同步方法中的锁
wait和sleep都会阻塞进程,但是wait阻塞时会释放锁
// 两线程交替打印 0 - 99
public class ThreadCommunication implements Runnable{
private int i;
public static void main(String[] args) {
ThreadCommunication tc = new ThreadCommunication();
Thread t1 = new Thread(tc);
Thread t2 = new Thread(tc);
t1.setName("进程1");
t2.setName("进程2");
t1.start();
t2.start();
}
@Override
public void run() {
for(i = 0; i < 100; ){
synchronized (this) {
//唤醒wait
notify();
System.out.println(Thread.currentThread().getName() + " " + i);
i++;
try {
//等待,阻塞,并释放锁
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
六、JDK5.0新增线程创建方式(2).
创建多线程的方式三:实现Callable接口.
与
Runnable
接口相比相比于
run()
方法,call()
可以有返回值方法可以抛出异常
支持泛型的返回值
需要借助
Future
类,比如获取放回结果
Future
接口可以对具体
Runnable
、Callable
任务的执行结果进行取消、查询是否完成等
/**
* 方式三:实现Callable接口
* 1.编写实现Callable接口的实现类
* 2.重写call()方法
* 3.创建实现类对象
* 4.创建FutureTask对象,并将Callable实现类对象传入FutureTask
* 5.创建线程对象,并将FutureTask对象传入Thread
* 6.调用Thread对象的start方法开启线程
* 7.通过FutureTask对象的get()方法可以获取call()方法的返回值
**/
//1.
public class ThreadTest06 implements Callable {
//2.
//计算 1 - 100 的总和
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++){
System.out.println(Thread.currentThread().getName() + " " +i);
sum += i;
}
return sum;// int --> Integer --> Object
}
public static void main(String[] args) {
//3.
ThreadTest06 c = new ThreadTest06();
//4.
FutureTask<Integer> ft = new FutureTask<>(c);//可以使用泛型,泛型的类型就是get方法返回的类型
//5.
Thread t = new Thread(ft);
//6.
t.start();
//7.
try {
Integer sum = ft.get();
System.out.println("1 - 100的总和为 " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
创建多线程的方式四:使用线程池.
好处
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
- corePoolSize: 核心池的大小
- maximumPoolSize: 最大连接数
- keepAliveTime: 线程没有任务时最多保持多长时间后会终止
线程池相关API.
JDK 5.0起提供了线程池相关API:ExecutorService
和 Executors
ExecutorService
:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command)
:执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T> task)
:执行任务,有返回值,一般又来执行Callable
void shutdown()
:关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池Executors.newFixedThreadPool(n)
; 创建一个可重用固定线程数的线程池Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池Executors.newScheduledThreadPool(n)
:创建一个线程池,它可安排在给定延迟后运
行命令或者定期地执行。
/**
* 方式四:使用线程池
* 1. 创建 Runnable 或 Callable的实现类(也可是匿名实现类)
* 2. 使用Executors.newFixedThreadPool(n);创建固定的线程池
* 3. 可选:线程池的一些设置操作
* 4. 使用线程池对象,执行不同的线程对象
* excute 或 submit
* 5. 使用shutdown关闭线程池
**/
public class ThreadTest07 {
public static void main(String[] args) {
//1.
//runnable
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "runnable");
}
};
//callable
Callable c = new Callable() {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + "callable");
return null;
}
};
//2.
ThreadPoolExecutor es = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);//必须使用实现类,不然无法设置线程池
// ExecutorService --> ThreadPoolExecutor
// System.out.println(es.getClass()); 通过getClass 可以获取对象的创建类
//3.
// es.setCorePoolSize(1024);
//4.
es.execute(r);//runnable
es.submit(c);//runnable or callable
//5.
es.shutdown();
}
}
补充.
单核和多核CPU.
- 单核CPU:是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务,由于CPU主频很高,几乎感觉不出来,它是单线程的
- 多核CPU:能更好的发挥多线程的效率
一个Java程序java.exe,至少启动三个线程
- main() 主线程
- gc() 垃圾回收线程
- 异常处理线程 –> 影响主线程
并行和并发.
并行
- 多个cpu同时执行多个任务 —— 多个人做不同的事
并发
- 一个cpu(采用时间片)同时执行多个任务 —— 多个人做一件事
线程的调度.
调度策略
时间片
时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是十几毫秒(ms)。
抢占式
高优先级的线程抢占CPU
Java中的调度方法
- 同优先级线程组成先进先出(队列),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
窗口卖票.
1、使用继承Thread实现(存在线程安全问题).
public class Window extends Thread{
/*
解释一下 为什么要用 static
开了三个窗口(三个Window对象,每个对象一个线程),每个Window对象都有自己的ticket属性
所以 我们需要这三个对象 共用一套 ticket属性 --> 使用 static
*/
private static int ticket = 100;
@Override
public void run() {
//卖票
while(true){
if(ticket > 0){
System.out.println(this.getName() + "卖出" + ticket + "号票");
ticket--;
}else{
break;
}
}
}
public static void main(String[] args) {
//创建三个窗口 (三个线程对象)
Thread window1 = new Window();
Thread window2 = new Window();
Thread window3 = new Window();
//设置名字
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
//开启线程
window1.start();
window2.start();
window3.start();
}
}
2、使用实现Runnable接口实现(存在线程安全问题).
public class Window implements Runnable{
/*
解释一下 为啥没使用 static
(创建了一个Window对象,这个对象开了三个线程(窗口)) ticket只有一份
*/
private int ticket = 100;
@Override
public void run() {
//卖票
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "卖出" + ticket + "号票");
ticket--;
}else{
break;
}
}
}
public static void main(String[] args) {
//创建一个窗口对象, 它不卖票,让给线程卖
Window w = new Window();
//创建3个窗口(3个线程对象) w 作为三个线程的共享数据
Thread w1 = new Thread(w);
Thread w2 = new Thread(w);
Thread w3 = new Thread(w);
//设置名字
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
//开启线程
w1.start();
w2.start();
w3.start();
}
}
线程安全问题展示.
原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也操作了车票
- 重票 如上
- 错票
解决方法.
通过同步机制解决.
方式一:同步代码块 synchronized
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
如果是实现
Runnable
接口需要使用非静态的同步方法(当然静态也可以,只不过同步数据也要是静态的),它的默认同步监视器:this
;不适用与继承Thread的如果是继承
Thread
需要使用static
修饰同步方法,它的默认同步监视器:当前类
方式三:同步锁
线程分类.
Java中的线程分为两类:一种是守护线程,一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
Java垃圾回收就是一个典型的守护线程。
若JVM中都是守护线程,当前JVM将退出。
线程死锁问题.
- 死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 解决方法
- 专门的算法,原则
- 尽量减少同步资源(锁)的定义
- 尽量避免嵌套同步
public class DeadLock {
//两把锁
private static StringBuffer a = new StringBuffer();
private static StringBuffer b = new StringBuffer();
public static void main(String[] args) {
/*
有两个锁 ,a ,b
两个线程 A , B
正常情况下, 假如 A先获取执行权 A -> sync(a),a没上锁 --> a上锁 --> 代码操作 --> a锁释放
--> sync(b),b没上锁 --> b上锁 --> 代码操作 --> b锁释放 --> 打印操作 --> A释放执行权
--> B获取执行权 B -> sync(b) --> b上锁 --> 代码操作 --> b锁释放
--> sync(a),a没上锁 --> a上锁 --> 代码操作 --> a锁释放 --> 打印操作 --> 程序结束
死锁:A --> sync(a),a没上锁 --> a上锁 --> A阻塞, B --> sync(b),b没上锁 --> b上锁 --> B阻塞
--> A继续运行 --> sync(b),b已上锁 --> 等待b释放(阻塞) --> B继续运行 --> sync(a),a已上锁 --> 等待a释放(阻塞)
--> ........................................双方一直等待对方释放资源
*/
//线程1 : 采用实现Runnable接口的匿名实现类
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
synchronized (a){
a.append("1");
Thread.sleep(100);//阻塞 注释这段代码就能正常运行
synchronized (b){
b.append("a");
}
System.out.println(a);
}
}
}).start();
//线程2 : 采用继承Thread类的匿名子类
new Thread(){
@SneakyThrows
@Override
public void run() {
synchronized (b){
b.append("b");
Thread.sleep(100);//阻塞
synchronized (a){
a.append("2");
}
System.out.println(b);
}
}
}.start();
}
}
synchronized与lock的异同.
- 相同点
- 都可以解决线程安全问题
- 不同
- lock是显示锁,手动启动,释放锁 ; synchronized是隐式锁,执行完相应的代码后,自动释放锁
- Lock只有代码块锁; synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
使用的先后顺序 : Lock –> 同步代码块 –> 同步方法
sleep() 和 wait() 异同.
- 相同点
- 一旦执行方法,都可以使得当前的线程进入阻塞状态
- 不同点
- 两个方法声明的位置不同
- Thread类中声明sleep()
- Object类中声明wait()
- 调用的要求不同
- sleep()可以在任何需要的场景下调用
- wait()必须使用在同步代码块或同步方法中
- 关于释放锁
- sleep() 不会
- wait()会
- 两个方法声明的位置不同
释放锁的操作.
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、
该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导
致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的
wait()
方法,当前线
程暂停,并释放锁。
不会释放锁的操作.
- 线程执行同步代码块或同步方法时,程序调用
Thread.sleep()
、
Thread.yield()
方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程
挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用
suspend()
和resume()
来控制线程
生产者和消费者.
* @decription 生产者 消费者问题
* 生产的产品最多20个
*
* 包含线程同步(同步方法 ) --> 解决了线程安全问题
* 线程通信
*
**/
public class pc {
public static void main(String[] args) {
SalesClerk clerk = new SalesClerk();
Thread producer = new Thread(new Producer(clerk));
Thread consumer1 = new Thread(new Consumer(clerk));
Thread consumer2 = new Thread(new Consumer(clerk));
producer.setName("厂家1");
consumer1.setName("顾客1");
consumer2.setName("顾客2");
producer.start();
consumer1.start();
consumer2.start();
}
}
//生产者
class Producer implements Runnable{
private SalesClerk clerk;
public Producer(SalesClerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始生产");
while(true){
try {
clerk.produceProduct();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者
class Consumer implements Runnable{
private SalesClerk clerk;
public Consumer(SalesClerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始消费");
while (true){
try {
clerk.consumeProduct();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//售货员
class SalesClerk{
private int product = 0;
//生产产品 : < 20 进货
public synchronized void produceProduct() throws InterruptedException {
if(product < 20){
//厂家 每[1,10]ms生产一个产品 最多生产20个
Thread.sleep((int)(Math.random()*10 + 1));
System.out.println(Thread.currentThread().getName()+"生产了一个产品,产品库存: " + ++product);
//生产了就 让消费者购买
if (product == 1){
notifyAll();
}
}else{
//产品达到20个就停止生产
wait();
}
}
//消费产品 : > 0 售卖
public synchronized void consumeProduct() throws InterruptedException {
if(product > 0){
//厂家 没[1,100]ms售出一个产品
Thread.sleep((int)(Math.random()*100 + 1));
System.out.println(Thread.currentThread().getName()+"购买了一个产品,产品库存: " + --product);
// if(product %2 == 0) //库存不足 厂家生产
notifyAll();// 买了一个 厂家块生产
}else{
//库存没了,只能等
wait();
}
}
public int getProduct(){
return this.product;
}
}