一、多线程
1.线程和进程
进程:是一个应用程序
线程:是一个进程中的执行场景/执行单元
一个进程可以启动多个线程
2.对于java程序
当在DOS命令窗口中输入:java HelloWorld回车后,辉县启动JVM,而JVM就是一个进程,
JVM会再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾
现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程
3.进程和线程的关系
例如:
阿里巴巴:进程
马云:阿里巴巴的一个线程
阿里前台:阿里巴巴的一个线程
京东:进程
强东:京东的一个线程
妹妹:京东的一个线程
进程可以看做公司,线程可以看做员工
注意:
进程A和进程B的内存独立不共享
在java语言中,线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈
4.多线程并发
假设启动10个线程,会有10个栈空间,每个栈之间互不干扰,各自执行
多线程机制,目的是为了提高程序的处理效率
使用多线程机制之后,main方法结束,程序也有可能结束吗?
答:main方法结束之时主线程结束,主栈空了,其他栈(线程)可能还在压栈、弹栈
什么是真正的多线程并发?
t1线程执行t1,t2执行t2,二者不会互相影响
对于单核CPU来说,真的可以做到真正的多线程并发吗?(对于多喝CPU,真正的多线程并发是没问题的)
单核CPU表示只有一个大脑:不能做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉
对于单核CPU,在某一时间点上实际上只能处理一件事情,但由于CPU处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情同时处理
//除了垃圾回收线程外,只要一个主线程
public class ThreadTest01 {
public static void main(String[] args) {
System.out.println("main begin");
m1();
System.out.println("main over");
}
private static void m1(){
System.out.println("m1 begin");
m2();
System.out.println("m1 over");
}
private static void m2(){
System.out.println("m2 begin");
m3();
System.out.println("m2 over");
}
private static void m3(){
System.out.println("m3 execute!");
}
}
一个栈中,自上而下的顺序依次逐行执行
5.实现线程的两种方式
第一种方式:
编写一个类,直接继承java.lang.Thread,重写run方法
怎么创建线程对象?new
怎么启动线程?调用线程对象的start()方法
start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码人去完成后,瞬间结束
public class ThreadTest02 {
public static void main(String[] args) {
//main方法在主线程中运行
//新建一个分支线程对象
MyThread myThread = new MyThread();
//启动线程
//myThread.run();//不会启动线程,不会分配新的分支栈(单线程)
//这段代码的任务只是为了开启一个新的栈空间,只要心的栈空间开出来,start()方法结束,线程启动成功
//启动成功的线程会自动调用run方法,且run方法在分支栈的栈底部(压栈)
//run方法在分支栈的栈底部,main方法在主栈的栈底部,run和main是平级的
myThread.start();
for (int i = 0;i < 1000;i++){
System.out.println("主线程--->" + i);
}
}
}
//定义线程类
class MyThread extends Thread{
@Override
public void run() {
//编写程序,这段程序运行在分支栈中
for (int i = 0;i < 1000;i++){
System.out.println("分支线程--->" + i);
}
}
}
第二种方式
编写一个类,实现java.lang.Runnable接口,实现run方法
public class ThreadTest03 {
public static void main(String[] args) {
//创建一个可运行的对象
//MyRunnable r = new MyRunnable();
//将可运行的嗯对象封装成一个线程对象
//Thread t = new Thread(r);
//合并代码
Thread t = new Thread(new MyRunnable());
//启动线程
t.start();
for (int i = 0;i < 100;i++){
System.out.println("主线程--->" + i);
}
}
}
//这不是一个线程类,是一个可运行的类,它不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0;i < 100;i++){
System.out.println("分支线程--->" + i);
}
}
}
注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更加灵活
匿名内部类
public class ThreadTest04 {
public static void main(String[] args) {
//创建线程对象,采用匿名内部类的方式
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 100;i++){
System.out.println("分支线程--->" + i);
}
}
});
//启动线程
t.start();
for (int i = 0;i < 100;i++){
System.out.println("main线程--->" + i);
}
}
}
6.线程的生命周期
新建状态
就绪状态
运行状态
阻塞状态
死亡状态
二、线程功能
1.获取线程名字
(1)获取当前线程对象
Thread t = Thread.cuttentThread();
返回值t就是当前线程
(2)获取线程对象的名字
String name = 线程对象.getName();
(3)修改线程对象的名字
线程对象.setName(“线程名字”);
(4)默认线程名字
当线程没有设置名字时,默认的名字的规律?
Thread-0、Thread-1…
public class ThreadTest05 {
public static void main(String[] args) {
//这个代码出现在main方法中,所以当前线程就是主线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread.getName());//main
//创建线程对象
MyThread2 t = new MyThread2();
//设置线程名字
t.setName("tttt");
//获取线程名字
String tName = t.getName();
System.out.println(tName);//若不设置线程名字,默认为Thread-0
MyThread2 t2 = new MyThread2();
t.setName("t2");
System.out.println(t2.getName());//Thread-1
t2.start();
//启动线程
t.start();
}
}
class MyThread2 extends Thread{
public void run(){
for (int i = 0;i <100;i++){
//当t1线程执行run方法,则这个当前线程是t1
//当t2线程执行run方法,则这个当前线程是t2
Thread currentThread = Thread.currentThread();
System.out.println("分支线程-->" + i);
}
}
}
2.线程的sleep方法
static void sleep(long millis)
1)静态方法:Thread.sleep(1000);
2)参数是毫秒
3)作用:让当前进程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用
这行代码出现在A线程中,A线程就会进入休眠
4)Thread.sleep()方法,可以做到这种效果:
间隔待定的时间,去执行一端待定的代码,每隔多久执行一次
public class ThreadTest06 {
public static void main(String[] args) {
//让当前线程进入休眠,睡眠5秒
//当前线程是主线程
/*try {
Thread.sleep(1000*5);
}catch (InterruptedException e){
e.printStackTrace();
}
//执行这里的代码
System.out.println("hello world!");*/
for (int i = 0;i < 10;i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
//睡眠1s
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
关于sleep的面试题
public class ThreadTest07 {
public static void main(String[] args) {
//创建线程对象
MyThhread3 t = new MyThhread3();
t.setName("t");
t.start();
//调用sleep方法
try{
t.sleep(1000*5);
}catch (InterruptedException e){
e.printStackTrace();
}
//5秒后输出
System.out.println("hello world!");
}
}
class MyThhread3 extends Thread{
public void run(){
for(int i = 0;i < 10000;i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
问题:
t.sleep(1000*5);这行代码会让线程t进入休眠状态吗?
答案:不会,因为sleep是静态方法,执行时会转换成Thread.sleep(1000*5);只会让当前线程进入休眠
3.终止线程的休眠
注意:不是中断线程的执行,是终止线程的睡眠
t.interrupt();//干扰
这种中断睡眠的方式依靠了java的异常处理机制
public class ThreadTest08 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
//希望5s后,t线程醒来
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止t线程的睡眠
t.interrupt();//干扰
}
}
class MyRunnable2 implements Runnable{
//重点:run()当中的异常不能throws,只能try catch
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "---> begin");
//睡眠一年
try {
Thread.sleep(1000*60*60*24*365);
} catch (InterruptedException e) {
//打印异常
e.printStackTrace();
}
//1年之后才会执行这行代码
System.out.println(Thread.currentThread().getName() + "---> end");
//调用doOther
//doOther();
}
/*//其他方法可以throws
public void doOther() throws Exception{
}*/
}
4.强行终止线程的执行
t.stop()方法已过时,缺点:容易丢失数据,此方法是直接讲线程杀死,先称没有保存的数据将会丢失
合理的终止一个线程的执行
public class ThreadTest10 {
public static void main(String[] args) {
MyRunnable4 r = new MyRunnable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程
//想要什么时候终止t的执行,就把标记修改为false,即可结束
r.run = false;
}
}
class MyRunnable4 implements Runnable{
//打一个布尔标记
boolean run = true;
@Override
public void run() {
for (int i = 0;i < 10;i++){
if (run){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//终止当前线程
return;
}
}
}
}
使用布尔标记法进行终止线程的执行
三、线程调度(了解)
1.常见的线程调度模型
抢占式调度模型
哪个线程的优先级较高,抢到的CPU时间片的概率就高
java采用的是抢占式调度模型
均分式调度模型
平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样
平均分配,一切平等
2.线程调度的方法
1)实例方法
void setPriority(int newPriority)//设置线程的优先级
int getPriority()//获取线程优先级
最低优先级:1
默认优先级:5
最高优先级:10
public class ThreadTest11 {
public static void main(String[] args) {
//设置主线程的优先级为1
Thread.currentThread().setPriority(1);
/*System.out.println("最高优先级" + Thread.MAX_PRIORITY);
System.out.println("最低优先级" + Thread.MIN_PRIORITY);
System.out.println("默认优先级" + Thread.NORM_PRIORITY);*/
//获取当前线程对象,获取当前线程的优先级
Thread currentThread = Thread.currentThread();
//System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());
Thread t = new Thread((new MyRunnable5()));
t.setPriority(10);
t.setName("t");
t.start();
//优先级较高的,只是抢到的CPU时间片相对多一些
for (int i = 0;i < 10000;i++){
System.out.println(Thread.currentThread().getName() +"--->" + i);
}
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
//获取线程优先级
//System.out.println(Thread.currentThread().getName() + "线程的默认优先级:" + Thread.currentThread().getPriority());
for (int i = 0;i < 10000;i++){
System.out.println(Thread.currentThread().getName() +"--->" + i);
}
}
}
2)让位方法
static void yield()//静态让位方法
暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”
注意:在回到就绪状态后,有可能会再次抢到
public class ThreadTest12 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable6());
t.setName("t");
t.start();
for (int i = 1;i <= 1000;i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
class MyRunnable6 implements Runnable{
@Override
public void run() {
for (int i = 1;i <= 1000;i++){
//每100个让位1次
if (i%100 == 0){
Thread.yield();//当前线程暂停一下,让给主线程
}
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
3)线程合并
void join()
class MyThread1 extends Thread{
public void soSome(){
MyThread2 t = new MyThread2();
t.join();//当前线程进入阻塞,t线程执行,直到t线程结束,当前线程才可以继续
}
}
class MyThread2 extends Thread{
}
public class ThreadTest13 {
public static void main(String[] args) {
System.out.println("main begin");
Thread t = new Thread(new MyRunnable7());
t.setName("t");
t.start();
//合并线程
try {
t.join();//t合并到当前线程中,当前线程受阻塞,t线程执行直到结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
class MyRunnable7 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 100;i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
}
四、线程安全(重点)
关于多线程并发环境下,数据的安全问题
为什么是重点?
以后在开发中,项目都是运行在服务器中,而服务器已经将线程的定义,线程对象的创建、线程的启动等,都已经实现完了,这些代码不需要自己编写
重点:需要知道,我们编写的程序需要放到一个多线程的环境下运行,更需要关注的是这些数据在多线程并发的环境下是否安全
1.线程不安全的条件
什么时候数据在多线程并发的环境下会存在安全问题?
条件1:多线程并发
条件2:有共享数据
条件3:共享数据有修改行为
满足以上3个条件后,就会存在线程安全问题
2.怎么解决线程安全问题
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,
怎么解决这个问题? 线程排队执行(不能并发)—这种机制叫做:线程同步机制
线程同步就是线程排队了,线程排队了就会牺牲一部分效率,数据安全第一位,只有数据安全后,才可以谈效率,数据不安全,就不会有效率
3.同步编程模型和异步编程模型
异步编程模型
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,—其实就是:多线程并发(效率较高)
同步编程模型
线程t1和线程t2,在线程t1执行时,必须等t2线程执行结束,或者在t2线程执行时,必须等待t1线程执行结束,两个线程之间发生了等待—线程排队(效率较低)
4.synchronized
线程同步机制的语法是:
synchronized(){
//线程同步代码块
}
synchronized后面小括号中传的数据相当关键
这个数据必须是多线程共享的数据,才能达到多线程排队
()中写什么?
那要看用户想让哪些线程同步,
假设t1,t2,t3,t4,t5有5个线程,只希望t1,t2,t3排队,t4,t5不排队,怎么办?
需要在()中写一个t1,t2,t3共享的对象,而这个对象对于t4,t5来说不是共享的
不一定this,只要是多线程共享的对象就可以
synchronized(this){}—这种写法最好
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记(只是把它叫做锁)
在实例方法上也可以使用synchronized
优点:
代码较少,较节俭
缺点:
synchronized出现在实例方法上,一定锁的是this,不能是其他对象,所以这种方式不灵活
synchronized出现在实例方法上,表示整个方法都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低
若共享的对象是this,且需要同步的代码块是整个方法体,建议使用这种方式
synchronized的三种写法
①同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
②在实例方法上使用
表示共享对象一定是this,且同步代码块是整个方法快
③在静态方法上使用(排它锁)
表示找类锁
类锁永远只要1把,就算创建了100个对象,类锁也只有1把
对象锁:1个对象1把锁,100个对象100把锁
类锁:100个对象,也可能只是1把类锁
synchronized面试题
package exam1;
//面试题1:doOther方法执行时需要等待doSome方法的结束吗?
//不需要,因为doOther方法没有synchronized
public class Exam01 {
public static void main(String[] args) throws InterruptedException {
MyClass mc = new MyClass();
Thread t1 = new MyThread(mc);
Thread t2 = new MyThread(mc);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行
t2.start();
}
}
//线程
class MyThread extends Thread{
private MyClass mc;
public MyThread(MyClass mc){
this.mc = mc;
}
public void run(){
if (Thread.currentThread().getName().equals("t1")){
mc.doSome();
}
if (Thread.currentThread().getName().equals("t2")){
mc.doOther();
}
}
}
class MyClass{
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
//面试题2:doOther方法执行时需要等待doSome方法的结束吗?
//需要,因为是同一把锁
class MyClass{
public synchronized void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
//面试题3:doOther方法执行时需要等待doSome方法的结束吗?
//不需要,因为MyClass对象是两个,两把锁
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new MyThread(mc1);
Thread t2 = new MyThread(mc2);
//面试题4:doOther方法执行时需要等待doSome方法的结束吗?
//需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把
class MyClass{
//synchronized出现在静态方法上是找类锁
public synchronized static void doSome(){
System.out.println("doSome begin");
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public synchronized static void doOther(){
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
5.线程安全问题(重要)
java中的三大变量:
实例变量:在堆中(堆只有1个)
静态变量:在方法区中(方法区只有1个)
局部变量:在栈中
以上三大变量中,局部变量永远不会存在线程安全问题—因为局部变量不共享(一个线程一个栈)
局部变量+常量:不会有线程安全问题
成员变量:可能会有线程安全问题
如果使用局部变量的话:
建议使用:StringBuilder—因为局部变量不存在线程安全问题,选择StringBuilder,StringBuffer效率较低
ArrayList是非线程安全的
Vector是线程安全的
HashMap HashSet是非线程安全的
Hashtable是线程安全的
6.扩大同步范围
synchronized(act){
act.withdraw(money);
}
这种方法会扩大同步范围,使得效率更低
五、死锁
死锁:不出现异常,也不出现错误,程序一直僵持在那里,这种错误最难调试
面试官要求会写死锁代码,只优惠些,才会在以后的开发中注意死锁问题,因为死锁很难调试
六、解决线程安全问题
并不是优先选择synchronized,因为synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低,用户体验差,在不得以的情况下再选择线程同步机制
第一种方案
尽量使用局部变量代替“实例变量和静态变量”
第二种方案
如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了(对象不共享,就不会存在数据安全问题了)
第三种方案
如果不能使用局部变量,对象也不能创建多个,此时只能选择synchronized线程同步机制了
七、线程其他内容
1.守护线程
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的是:垃圾回收线程(守护线程)
守护线程特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束
注意:主线程main方法是一个用户线程
守护线程用在什么地方?
public class ThreadTest14 {
public static void main(String[] args) {
Thread t = new BakDataThread();
t.setName("备份数据的线程");
//启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
//主线程:主线程是用户线程
for (int i = 0;i < 10;i++){
System.out.println(Thread.currentThread().getName() + "--->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BakDataThread extends Thread{
public void run(){
int i = 0;
//即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止
while(true){
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.定时器
作用:间隔特定的时间,执行特定的程序
定时器:java.util.Timer,可以直接拿来用(目前开发很少用)
目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) throws ParseException {
//创建定时器对象
Timer timer = new Timer();
//守护线程的方式
//Timer timer = new Timer(true);
//指定定时任务
//timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2020-12-23 22:39:20");
timer.schedule(null,firstTime,1000*10);
}
}
//编写一个定时任务类
//结社这是一个记录日志的定时任务
class LogTimerTask extends TimerTask{
@Override
public void run() {
//编写需要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String strTime = sdf.format(new Date());
System.out.println(strTime + ":成功完成了一次数据备份!");
}
}
3.实现线程的第三种方式:实现Callable接口
这种方式实现的线程可以获取线程的返回值,之前的两种方式无法获取线程返回值,因为run方法返回void
思考:
系统委派一个线程去执行一个任务,该线程执行完任务后,可能会有一个执行结果,我们怎么才能拿到这个执行结果?
实现Callable接口
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadTest15 {
public static void main(String[] args) throws Exception{
//第一步:创建一个“未来任务类”对象
//参数非常重要,需要给一个Callable接口实现类对象
FutureTask task = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {//call()方法就相当于run方法,只不过有返回值
//线程执行一个任务,执行之后可能会有一个执行结果
//模拟执行
System.out.println("call method begin");
Thread.sleep(1000*10);
System.out.println("call method end!");
int a = 100;
int b = 200;
return a+b;//自动装箱
}
});
//创建线程对象
Thread t = new Thread(task);
//启动线程
t.start();
//这是main方法,主线程中
//在主线程中,怎么获取t线程的返回结果?
//get()方法的执行会导致“当前线程阻塞”
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
//main方法这里的陈旭要想执行必须等get()方法的结束
//而get()方法可能需要很久,因为get()方法是为了拿另一个线程的执行结果,另一个线程执行是需要时间的
System.out.println("hello world!");
}
}
优点:可以获取线程的执行结果
缺点:效率较低,在获取t线程结果时,档期啊线程受阻塞
4.关于Object类中的wait和notify方法(生产者和消费者模式)
①这两个方法不是线程对象的方法,是Java中任何一个java对象都有的方法,因为这两个方式Object类中自带
②不是通过线程对象调用:t.wait()—错 t.notify()----错
wait()方法的作用?
Object o = new Object();
o.wait();//会让当前线程进入等待状态
表示:让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止
notify()方法的作用?
Object o = new Object();
o.notify();
表示:唤醒正在o对象上等待的线程
还有一个notifyAll()方法:唤醒o对象上处于等到的所有线程
wait方法和notify方法建立在synchronized线程同步的基础上
重点:
o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁
o.notify()方法只会通知,不会释放之前占有的o对象的锁
什么是“生产者和消费者模式”?
生产线程负责生产,消费线程负责消费
生产线程和消费线程需要达到均衡
这是一种业务需求,在这种特殊的情况下需要使用wait方法和notify方法
wait方法和notify方法不是线程对象的方法,是普通java对象都有的方法
wait方法和notify方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题
生产者:
消费者: