Bootstrap

JavaSE后阶(多线程)

目录

1.什么是线程,进程

1.1进程和线程的关系

1.2java实现线程的两种方式

1.3获取当前线程对象,获取线程对象名字,修改线程对象名字

1.4关于线程sleep方法

1.5run()方法知识点

1.6Java中合理结束一个进程的执行(常用)

2.常用的线程调度模型有哪些

2.1Java中提供了哪些方法和线程调度关系

2.2java进程的优先级

2.3yield和join方法

3.多并发条件下,数据的安全性(重点)

3.1怎样去解决线程安全问题呢?

3.2两个专业术语

4.线程安全

4.1代码执行原理

4.2缺点和优点

4.3线程安全问题和非线程安全问题

4.4总结

5.java中三大变量

6.synchronized锁

6.1在开发中应该怎样解决线程安全问题

6.2死锁

6.3守护线程

6.3.1Java中守护线程分为两大类

6.3.2守护线程特点

6.3.3守护线程用到什么地方呢?

6.3.4方法

7.定时器

1.定时器的作用

2.使用定时器实现日志备份

7.实现线程的第三种方式:实现Callable接口(JDK8新特性)

优点和缺点

8.关于Object类的wait(),notify(),notifyAll()方法

1.方法详解

2.总结


学到这里已经完成了JavaSE的基础了,现在来讲一下JavaSE最最最重要的地方,也是最常用的,同时也是JavaSE的特色!!!

1.什么是线程,进程

进程是一个应用程序(一个进程就是一个软件)

线程是一个进程中的执行单元和执行场景

一个进程可以有多个线程

对于java程序来说,当在dos命令窗口输入java Hello会先启动一个jvm,而jvm是一个进程,jvm再启动一个主线程调用main方法(main方法就是主线程)同时再启动一个垃圾回收线程负责看护,回收垃圾

1.1进程和线程的关系

1.2java实现线程的两种方式

1.编写一个类,直接继承java.lang.Thread,重写run方法

new继承线程的类

调用线程对象start()方法

public class Test {
    public static void main(String[] args) {
        thread thread = new thread();
        thread.run();//单线程
        thread.start();//多线程
        for (int i = 0; i <100 ; i++) {
            System.out.println("哈哈哈哈哈哈");
        }
    }
}
class thread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println("=============");
        }
    }
}

run不能启动线程,只是普通的调用而已 不会分配新的分支线

start启动一个分支线程,在JVM中开辟一个新的栈空间

2.第二种方式编写一个类实现java.lang.Runnable接口,实现run方法

new 线程类传入可运行的类/接口

调用线程对象的start方法

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.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 <10 ; i++) {
            System.out.println("副线程"+i);
        }
    }
}

注意:第二种实现接口最常见,因为一个类实现了接口,它还可以去继承其他的类

1.3获取当前线程对象,获取线程对象名字,修改线程对象名字

thread.getName();//获取线程名字

Thread thread = Thread.currentThread();//获取当前线程

thread1.setName("姜霁轩");

1.4关于线程sleep方法

  1. 静态方法Thread.sleep(1000);

  2. 参数是毫秒

  3. 作用:让当前线程进入休眠状态,进入"阻塞状态",放弃占有cpu的时间片,让给其他线程使用

中断sleep()的方法interrupt

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new thread());
        t.setName("t");
        t.start();
        for (int i = 0; i <1000 ; i++) {
            System.out.println(i);
        }
        t.interrupt();//终止线程休眠
        System.out.println("结束");
        for (int i = 0; i <1100 ; i++) {
            System.out.println("简介");
        }
    }
}
class thread implements Runnable {
    @Override
    public void run() {
        System.out.println("副线程开始");
        try {
            Thread.sleep(1000*100000);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName()+"end");
    }
}

1.5run()方法知识点

run方法在父类中没有抛出任何异常,子类不能比父类抛出更多异常

1.6Java中合理结束一个进程的执行(常用)

public class Test {
    public static void main(String[] args) {
        MyRunable4 r = new MyRunable4();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();
        // 模拟5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 终止线程
        // 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
        r.run = false;
    }
}
class MyRunable4 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就结束了,你在结束之前还有什么没保存的。
                // 在这里可以保存呀。
                //save....

                //终止当前线程
                return;
            }
        }
    }
}

2.常用的线程调度模型有哪些

  • 抢先式调度模型 哪个优先级高先调用那个java采用

  • 均分式调度 平均分配时间片

2.1Java中提供了哪些方法和线程调度关系

int getPriority()获得线程优先级

void setPriority(int newPriortity)设置线程优先级

最低是1默认是5最高是10

静态方法static void yield()让位方法,当前线程暂停,回到就绪状态,让给其他线程

此方法不是阻塞方法 让当前线程让位,让给其他线程使用,方法的执行会让当前线程从"运行状态"回到"就绪状态"

注意:回到就绪状态还有可能被抢到

void join()将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行到结束

public class Test {
    public static void main(String[] args) throws InterruptedException {
        thread thread = new thread();
        thread.start();
        thread.join();//当前线程进入阻塞状态,thread线程先执行,直到t线程结束  当前线程才可以继续
        for (int i = 0; i <100 ; i++) {
            System.out.println("主线程"+i);
        }
    }
}
class thread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {
            System.out.println("副线程"+i);
        }
    }
}

2.2java进程的优先级

注意:

  • main线程的默认优先级是5

  • 优先级高的,只是抢到cpu时间片时间相对于多一些 大概率方向更偏向优先级比较高的

static int MAX_PRIORITY最高优先级(10)
static int MIN_PRIORITY最高优先级(1)
static int NORM_PRIORITY最高优先级(5)
方法int getPriority()获得线程优先级
void setPriority(int newPriority)设置线程优先级
public class Test {
    public static void main(String[] args) {
        System.out.println("最高优先级:" + Thread.MAX_PRIORITY);//最高优先级:10
        System.out.println("最低优先级:" + Thread.MIN_PRIORITY);//最低优先级:1
        System.out.println("默认优先级:" + Thread.NORM_PRIORITY);//默认优先级:5
        // main线程的默认优先级是:5
        System.out.println(Thread.currentThread().getName() + "线程的默认优先级是:" + Thread.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() {
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

2.3yield和join方法

static void yield()让位,当前线程暂停,回到就绪状态

注意:并不是每次都能抢到时间片

void join()将一个线程合并到当前线程,当前线程受阻塞,加入的线程执行到结束

void join(long millis)接上条,等待该线程终止的时间最长为millis毫秒

void join(long millis ,int nanos)接第一条,等待该线程终止的时间最长为millis+nanos纳秒

public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable6());
        t.setName("t");
        t.start();
        for(int i = 1; i <= 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}
class MyRunnable6 implements Runnable {
    @Override
    public void run() {
        for(int i = 1; i <= 10000; i++) {
            //每100个让位一次。
            if(i % 100 == 0){
                Thread.yield(); // 当前线程暂停一下,让给主线程。
            }
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

3.多并发条件下,数据的安全性(重点)

  • 多线程并发

  • 有共享数据

  • 共享数据有修改行为

满足这三个问题就会存在线程安全问题

3.1怎样去解决线程安全问题呢?

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎样去解决这个问题?

线程排队执行(不能并发)用排队执行解决线程安全问题

这种机制被称为:线程同步机制

专业术语:线程同步,实际上就是线程不能并发了,线程必须排队进行

线程同步就是线程排队了,排队了就会牺牲掉一部分效率,数据安全第一位,只有数据安全了,我们才可以谈效率 数据不安全没有效率的事

3.2两个专业术语

异步编程模型:线程t1和t2互不干涉,自己执行自己的

其实就是多线程并发(效率极高)

异步就是并发

4.线程安全

线程同步机制语法

synchronized(){}

重点:synchronized后面扩中传过来的数据必须是"多线程共享的数据" 才能到线程排队有五个线程,如果你只希望t1,2,3排队

在()里面写他们三个共享的对象()不一定是this,只要是多线程共享的那个对象就行

注意:Java语言中,任何一个对象都有一把锁,这把锁就是标记

100个对象,100把锁

4.1代码执行原理

t1和t2线程并发,开始执行的时候,肯定有一个先一个后

假设t1先执行,遇到synchronized,这个时候自动找"后面共享对象"的对象锁找到之后占用去执行,直到程序结束才会释放锁

假设t1已经占有,t2遇到synchronized关键字,需要等待t1释放锁

如此就达到了线程排队执行

重中之重:

这个共享对象一定要选好了 这个共享对象一定是你需要排队

执行的这些线程对象所共享的

class Account {
    private String actno;
    private double balance; //实例变量。
    //对象
    Object o = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)
    public Account() {
    }
    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }
    public String getActno() {
        return actno;
    }
    public void setActno(String actno) {
        this.actno = actno;
    }
    public double getBalance() {
        return balance;
    }
    public void setBalance(double balance) {
        this.balance = balance;
    }
    //取款的方法
    public void withdraw(double money) {
        /**
         * 以下可以共享,金额不会出错
         * 以下这几行代码必须是线程排队的,不能并发。
         * 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。
         */
        synchronized (this) {//调用这个方法的对象,两个线程对象是同一个
            //synchronized(actno) {
            //synchronized(o) {
            /**
             * 以下不共享,金额会出错
             */
		  /*Object obj = new Object();
	        synchronized(obj) { // 这样编写就不安全了。因为obj2不是共享对象。
	        synchronized(null) {//编译不通过
	        String s = null;
	        synchronized(s) {//java.lang.NullPointerException*/
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
            //}
        }
    }
}
    class AccountThread extends Thread {
        // 两个线程必须共享同一个账户对象。
        private Account act;
        // 通过构造方法传递过来账户对象
        public AccountThread(Account act) {
            this.act = act;
        }
        public void run(){
            double money = 5000;
            act.withdraw(money);
            System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
        }
    }
    public class Test {
        public static void main(String[] args) {
            // 创建账户对象(只创建1个)
            Account act = new Account("act-001", 100000);
            // 创建两个线程,共享同一个对象
            Thread t1 = new AccountThread(act);
            Thread t2 = new AccountThread(act);
            t1.setName("t1");
            t2.setName("t2");
            t1.start();
            t2.start();
        }
    }

以上代码锁this,实例变量actno,实例变量o都可以 因为这三个是线程共享

synchronized出现在实例方法上,一定锁的是this

没得挑 只能是this 不能是其他的对象了 所以这种方式不太灵活

4.2缺点和优点

synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步范围,导致执行效率降低 所以这种方式不常用

代码写的少变得简介

4.3线程安全问题和非线程安全问题

如果使用局部变量建议使用stringbuilder 因为局部变量不存在线程安全问题 所以效率较高

反之使用stringbuffer

  • ArrayList 是非线程安全

  • Vector 是线程安全

  • HashMap HashSet 是非线程安全的

  • Hashtable 是线程安全的

4.4总结

如果共享的对象就是this,并且需要同步整个方法体,建议使用这种方法

 public synchronized void withdraw(double money){
        double before = this.getBalance();
        double after = before - money;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }

在方法调用处使用synchronized

这种方法也可以,只不过扩大了搜索范围

public void run(){
        double money = 5000;
        // 取款
        // 多线程并发执行这个方法。
        //synchronized (this) { //这里的this是AccountThread对象,这个对象不共享!
        synchronized (act) { // 这种方式也可以,只不过扩大了同步的范围,效率更低了。
            act.withdraw(money);
        }
        System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
    }

5.java中三大变量

  • 实例变量:在堆中

  • 静态变量:在方法区中

  • 局部变量:在栈中

局部变量永远不会存在线程安全问题 因为局部变量不共享(一个线程一个栈) 局部变量在栈中 所以局部变量不会被共享

  1. 实例变量在堆中,堆只有一个

  2. 静态变量在方法区中,方法区只有一个

堆和方法区都是多线程共享的,所以可能存在线程安全问题

总结:局部变量+常量 不会有线程安全问题

成员变量(实例+静态) 可能会有线程安全问题

6.synchronized锁

1.同步代码块 灵活

synchronized(线程共享对象){
		同步代码块;
}

在实例方法上使用synchronized

表示共享对象一定是this 并且同步代码是整个方法体

3.在静态方法上使用synchronized

表示找类锁 类锁永远只有一把

就算创建了100个对象,类锁也只有一把

6.1在开发中应该怎样解决线程安全问题

synchronized会让程序执行效率降低,用户体验不好

在不得已情况下才会使用

  1. 尽量使用局部变量代替"实例变量"和"静态变量"

  2. 如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就不会有数据安全问题)

  3. 如果不能使用局部变量,对象也不能创建多个,这个时候只能选择synchronized 线程同步机制

6.2死锁

/**
 * 比如:t1想先穿衣服在穿裤子
 *       t2想先穿裤子在传衣服
 * 此时:t1拿到衣服,t2拿到裤子;
 * 由于t1拿了衣服,t2找不到衣服;t2拿了裤子,t1找不到裤子
 * 就会导致死锁的发生!
 */
public class Test {
    public static void main(String[] args) {
        Dress dress = new Dress();
        Trousers trousers = new Trousers();
        //t1、t2共享dress和trousers。
        Thread t1 = new Thread(new MyRunnable1(dress, trousers), "t1");
        Thread t2 = new Thread(new MyRunnable2(dress, trousers), "t2");
        t1.start();
        t2.start();
    }
}
class MyRunnable1 implements Runnable{
    Dress dress;
    Trousers trousers;

    public MyRunnable1() {
    }
    public MyRunnable1(Dress dress, Trousers trousers) {
        this.dress = dress;
        this.trousers = trousers;
    }
    @Override
    public void run() {
        synchronized(dress){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (trousers){
                System.out.println("--------------");
            }
        }
    }
}
class MyRunnable2 implements Runnable{
    Dress dress;
    Trousers trousers;
    public MyRunnable2() {
    }
    public MyRunnable2(Dress dress, Trousers trousers) {
        this.dress = dress;
        this.trousers = trousers;
    }
    @Override
    public void run() {
        synchronized(trousers){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (dress){
                System.out.println("。。。。。。。。。。。。。。");
            }
        }
    }
}
class Dress{
}
class Trousers{
}

6.3守护线程

6.3.1Java中守护线程分为两大类

一类是守护线程(后台线程),一类是用户线程

其中具有代表的是:垃圾回收线程(守护线程)

6.3.2守护线程特点

一般守护线程就是一个死循环,所有的用户线程只要结束,守护线程就会自动结束

注意:主线程main方法就是一个用户线程

6.3.3守护线程用到什么地方呢?

每天00:00的时候系统数据自动备份 这个时候就需要使用到定时器并且我们可以将定时器设置为守护线程

6.3.4方法

void setDaemon(boolean on)on为true表示把线程设置为守护线程

public class Test {
    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(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7.定时器

1.定时器的作用

间隔特定的时间,执行特定的程序

在实际开发中,间隔多久执行一端特定的程序,这种需求是很常见的,那么java中其实可以采用多种方式实现:

  1. 可以使用sleep睡眠,设置睡眠时间,没到这个时间醒来,执行任务 这种方式是最原始的定时器(比较low)

  2. 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接来用 不过这种方式目前开发也很少用,因为有很多的高级框架可以使用

在实际开发中,目前使用最多的就是spring提供的springTask框架,这个框架只要进行简单的配置,就可以完成定时器任务

2.使用定时器实现日志备份

public class Test {
    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(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

7.实现线程的第三种方式:实现Callable接口(JDK8新特性)

这种方式实现的线程可以获取线程的返回值

之前的两种方式是无法获取线程的返回值的,因为run方法返回void

优点和缺点

可以获取到线程的执行结果

效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率极低

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Test{
    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; //自动装箱(300结果变成Integer)
            }
        });
        // 创建线程对象
        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!");
    }
}

8.关于Object类的wait(),notify(),notifyAll()方法

void wait 让活动在当前对象的线程无限等待(释放之前占有的锁)

void notify 唤醒当前对象正常等待的线程(只提醒唤醒,不会释放锁)

void notifyAll 唤醒当前对象全部正在等待的线程(只提示唤醒,不会释放锁)

1.方法详解

1.wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方法是object自带的

2.wait方法的作用

object o=new object();
o.wait();

表示:让正在o对象上活动的线程进入等待状态,无限等待,直到唤醒为止

3.notify()作用

表示:唤醒正在o对象上等待的线程

4.notifyall()作用

表示:唤醒o对象上处于等待的所有线程

2.总结

  1. wait和notify不是线程对象方法,是普通java对象都有的方法

  2. wait和notify建立在线程同步基础之上 因为多线程要同时操作一个仓库 有线程安全问题

  3. wait方法的作用 让正在o对象上活动的线程t进入等待状态,并释放掉t线程之前占有的o对象的锁

  4. notify的作用 让正在o对象上等待的线程唤醒,只要通知,不会释放o对象上之前占有的锁

import java.util.ArrayList;
import java.util.List;
public class Test{
    public static void main(String[] args) {
        // 创建1个仓库对象,共享的。
        List list = new ArrayList();
        // 创建两个线程对象
        // 生产者线程
        Thread t1 = new Thread(new Producer(list));
        // 消费者线程
        Thread t2 = new Thread(new Consumer(list));
        t1.setName("生产者线程");
        t2.setName("消费者线程");
        t1.start();
        t2.start();
    }
}
// 生产线程
class Producer implements Runnable {
    // 仓库
    private List list;
    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        // 一直生产(使用死循环来模拟一直生产)
        while(true){
            // 给仓库对象list加锁。
            synchronized (list){
                if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
                    try {
                        // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到这里说明仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒消费者进行消费
                list.notifyAll();
            }
        }
    }
}
// 消费线程
class Consumer implements Runnable {
    // 仓库
    private List list;
    public Consumer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        // 一直消费
        while(true){
            synchronized (list) {
                if(list.size() == 0){
                    try {
                        // 仓库已经空了。
                        // 消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到此处说明仓库中有数据,进行消费。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒生产者生产。
                list.notifyAll();
            }
        }
    }
}