黑马程序员——Java基础:多线程及其应用

来源:互联网 发布:手机淘宝5.6.0 编辑:程序博客网 时间:2024/06/07 05:03

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-
6.1进程和线程
6.1.1 进程与线程的区别:
进程:正在进行中的程序,其实进程就是一个应用程序运行时的内存分配空间。
线程:其实就是进程中一个独立控制单元,一条执行路径。
1.进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。
2.进程有独立的进程空间(堆空间和栈空间);线程的堆空间是共享的,栈空间是独立的。线程消耗的资源也比进程小,相互之间可以影响的。
3.一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序。
例如:jvm在启动时,会有一个进程java.exe,该进程中有一个主线程调用main函数执行代码。同时还会执行垃圾回收的线程。
多线程下载软件:此时线程可以理解为下载的通道,一个线程就是一个文件的下载通道,多线程也就是同时开起好几个下载通道.当服务器提供下载服务时,下载者共享带宽,在优先级相同的情况下,总服务器会对总下载线程进行平均分配。如果你线程多的话,那下载的越快。

6.1.2多线程特性:随机性
发现程序每一次结果都不一致。因为CPU在瞬间不断切换去处理各个线程而导致的。多线程是为了同步完成多项任务,不是为了提供运行效率,通过提高资源使用效率来提高系统的效率.
6.2 创建线程方式
java以及提供了对线程这类事物的描述,就是Thread类和Runnable接口
6.2.1第一种方式:继承Thread ,由子类复写run方法。
步骤:
1.定义类继承Thread类
2,覆写Thread类中的run方法,目的是将自定义的代码存储在run()方法中,让线程运行。(Thread类中的run()方法用于存储线程要运行的代码,主线程的代码存放在main中)
3,通过创建Thread类的子类对象,创建线程对象;
4,调用线程的d.start(),开启线程,并执行该线程的run方法,是多线程运行;(而d.run()仅仅是对象调用方法,线程创建了,并没有运行,仍然是单线程运行。)

6.2.2第二种方式:实现Runnable接口
步骤:
1,定义类实现Runnable接口。
2,覆盖接口中的run方法(用于封装线程要运行的代码)。
3,通过Thread类创建线程对象;
4,将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。
为什么要传递呢?要让线程去执行指定对象的run方法,就必须明确run方法所属的对象,而run方法所属的对象是Runnable接口的子类,所以要传递Runnable接口的子类对象。
5,调用Thread对象的start方法。开启线程,并运行Runnable接口子类中的run方法。

package july7;//继承Thread类,创建线程class MyThread extends Thread{    private String name;    public MyThread(String name) {        super(name);    }    public void run(){        System.out.println(name+"启动!");    }}//实现Runnable接口class YourThread implements Runnable{    private String name;    public YourThread(String name) {        this.name = name;    }    public void run() {        for (int i = 0; i < 3; i++) {            System.out.println(Thread.currentThread().getName()+"第"+i+"次启动!");        }    }}public class Demo1 {    public static void main(String[] args) {        for (int i = 0; i < 100; i++) {            if(i == 50){                new MyThread("刘昭").start();                new Thread(new YourThread(""),"章泽天").start();            }        }    }}

为什么要有Runnable接口的出现?
1:通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类,因为java只能单继承。
可是该类中的还有部分代码需要被多个线程同时执行。这时怎么办呢?
只有对该类进行额外的功能扩展,java就提供了一个接口Runnable。这个接口中定义了run方法,将线程要执行的任务封装到run方法中。再用Thread创建对象,在创建对象的同时,明确线程的任务。
因为实现Runnable接口可以避免单继承的局限性,所以通常创建线程都用第二种方式

2:将不同类中需要被多线程执行的代码进行抽取封装,变成一个Runnable接口的子类对象,为其他类进行功能扩展提供了前提。
继承Thread,成为线程的子类,对Thread类中的run方法覆写,然后封将任务封装成线程对象。Runnable接口将线程直接将要执行的任务封装到run方法中,变成一个Runnable接口的子类对象,不需要线程的出现。该任务如何执行?用Thread创建对象,在创建对象的同时,明确线程的任务
//面试
new Thread(new Runnable(){ //匿名
public void run(){
System.out.println(“runnable run”);
}
})
{
public void run(){
System.out.println(“subthread run”);
}

}.start(); //结果:subthread run

我的总结:
Thread类中run()和start()方法的区别如下:
run()方法:在本线程内调用该Runnable对象的run()方法,可以重复多次调用;
start()方法:启动一个线程,调用该Runnable对象的run()方法,不能多次启动一个
线程;
6.2.3 进程创建方式比较
A extends Thread:
简单
1不能再继承其他类了(Java单继承)
2同份资源不共享 (创建一个线程对象,就有一份数据)
3线程代码存放在Thread子类run方法中
A implements Runnable:(推荐)
1.多个线程共享一个目标资源,适合多线程处理同一份资源。
2.避免了单继承的局限性,可以继承其他类,也可以实现其他接口。
3.实现Runnable,线程代码存放在接口的子类的run方法中

package july7;//线程卖票的例子class YourSell extends Thread{    private String name;    private int num = 50;    public SellTicket(String name) {        super(name);//继承Thread类的构造函数    }    public void run(){        for (int i = 1; i <= num; i++) {            System.out.println(this.getName()+"卖出了第"+i+"张票!");        }    }}class MySell implements Runnable{    private int num = 50;    public void run() {        for (int i = 1; i <= num; i++) {            System.out.println(Thread.currentThread().getName()+"卖出了第"+i+"张票!");        }    }}public class Demo2 {    public static void main(String[] args) throws Exception {        new YourSell("A").start();        new YourSell("B").start();        new YourSell("C").start();        new Thread(new MySell(),"D").start();        new Thread(new MySell(),"E").start();        new Thread(new MySell(),"F").start();        for (int i = 10; i > 0; i--) {            System.out.println(i);            Thread.sleep(1000);        }    }}

6.2 线程的生命周期
线程状态:
Thread类内部有个public的枚举Thread.State,里边将线程的状态分为:
被创建:start()
运行:具备执行资格,同时具备执行权;
冻结:sleep(time)-时间到,wait()—notify()唤醒;没有cpu执行资格;
TERMINATED消亡——-已退出的线程处于这种状态stop()
临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
这里写图片描述
线程名称:
线程有自己默认的名称Thread-编号,从0开始
线程类有初始化名称的函数,子类可以用super语句来覆盖;run()方法中获取名字用this.getName(),也可以用Thread.currentThread.getName()—–标准通用方法

static Thread currentThread():获取当前线程对象
getName():获取线程名称
setName()或者构造函数:设置线程名称
6.3控制线程

join方法:调用join方法的线程对象强制运行,该线程强制运行期间,其他线程无法运行,必须等到该线程结束后其他线程才可以运行。
有人也把这种方式成为联合线程

join方法的重载方法:
join(long millis):
join(long millis,int nanos):
通常很少使用第三个方法:
程序无须精确到一纳秒;
计算机硬件和操作系统也无法精确到一纳秒;
后台线程:处于后台运行,任务是为其他线程提供服务。也称为“守护线程”或“精灵线程”。JVM的垃圾回收就是典型的后台线程。
特点:若所有的前台线程都死亡,后台线程自动死亡。
设置后台线程:Thread对象setDaemon(true);
setDaemon(true)必须在start()调用前。否则出现IllegalThreadStateException异常;
前台线程创建的线程默认是前台线程;
判断是否是后台线程:使用Thread对象的isDaemon()方法;
并且当且仅当创建线程是后台线程时,新线程才是后台线程。

sleep
线程休眠:
让执行的线程暂停一段时间,进入阻塞状态。
sleep(long milllis) throws InterruptedException:毫秒
sleep(long millis,int nanos)
throws InterruptedException:毫秒,纳秒
调用sleep()后,在指定时间段之内,该线程不会获得执行的机会。

控制线程之优先级

每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。
并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度;
默认情况下main线程具有普通的优先级,而它创建的线程也具有普通优先级。
Thread对象的setPriority(int x)和getPriority()来设置和获得优先级。
MAX_PRIORITY : 值是10
MIN_PRIORITY : 值是1
NORM_PRIORITY : 值是5(主方法默认优先级)

yield

线程礼让:
暂停当前正在执行的线程对象,并执行其他线程;
Thread的静态方法,可以是当前线程暂停,但是不会阻塞该线程,而是进入就绪状态。所以完全有可能:某个线程调用了yield()之后,线程调度器又把他调度出来重新执行。
6.4多线程安全问题
多线程安全问题的原因:
通过图解:发现一个线程在执行多条语句时,并运算同一个数据时,在执行过程中,由于多个线程访问出现延迟,以及线程随机性,使得其他线程参与进来,并操作了这个数据。导致到了错误数据的产生。
我们可以通过Thread.sleep(long time)方法来简单模拟延迟情况。
Try {
Thread.sleep(10);
}catch(InterruptedException e){}// 当刻意让线程稍微停一下,模拟cpu切换情况

涉及到两个因素:
1,多个线程在操作共享数据。
2,有多条语句对共享数据进行运算。

解决安全问题的原理:
只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他线程不能进来执行就可以解决这个问题。

Eg:在前面的卖票例子上,在每次卖票的前面加上模拟延时的语句!

package july7;class SellDemo implements Runnable{    private int num = 50;    public void run() {        for (int i = 0; i < 200; i++) {                if(num > 0){                        try {                        Thread.sleep(10);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }    //因为它不可以直接调用getName()方法,所以必须要获取当前线程。    System.out.println(Thread.currentThread().getName()+"卖出第"+num--+"张票!");            }        }    }}public class Demo3 {    public static void main(String[] args) {        SellDemo s = new SellDemo();        new Thread(s,"A").start();        new Thread(s,"B").start();        new Thread(s,"C").start();    }}

输出:会出现买了第0,甚至-1张票的情况!
6.5 多线程安全问题的解决方法
6.5.1 同步
1同步好处与弊端
好处:解决了线程安全问题。
弊端:多个线程需要判断,较为消耗资源
2定义同步的前提
a必须要有两个或者两个以上的线程,才需要同步。
b多个线程必须保证使用的是同一个锁。
6.5.2同步的表现形式:
a同步代码块:
synchronized(对象) // 任意对象都可以,这个对象就是锁
{
需要被同步的代码;
}
b同步函数
在方法上加上synchronized修饰符即可。(一般不直接在run方法上加!)
synchronized 返回值类型 方法名(参数列表)
{
需要同步的代码;
}
同步方法的锁是 this
c静态方法的同步
Static synchronized 返回值类型 方法名(参数列表)
{
需要同步的代码;
}
同步方法的锁是 类名.class
静态与非静态同步函数使用的锁分别是什么?
非静态同步函数都有自己所属的对象this,所以同步函数所使用的锁就是this锁。
静态同步函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,静态函数使用的锁该类的字节码文件对象。这个对象就是 类名.class

6.5.3同步代码块和同步函数的区别?
同步代码块使用的锁可以是任意对象。
同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。

在一个类中只有一个同步,可以使用同步函数。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。

我的总结:
1.明确哪些代码时多线程运行代码 add方法和run方法
2.明确共享数据sum和 b
3.明确多线程运行代码中的那些语句操作共享数据

6.5.4考点问题:请写一个延迟加载的单例模式?写懒汉式;当出现多线程访问时怎么解决?加同步,解决安全问题;效率高吗?不高;怎样解决?通过双重判断的形式解决。
//懒汉式:延迟加载方式。
当多线程访问懒汉式时,因为懒汉式的方法内对共性数据进行多条语句的操作。所以容易出现线程安全问题。加入同步机制,解决安全问题,但是却带来了效率降低。
为了效率问题,通过双重判断的形式解决。

class Single{    private static Single s = null;    private Single(){}    public static Single getInstance(){ //锁是谁?字节码文件对象;        if(s == null){            synchronized(Single.class){                if(s == null)                    s = new Single();            }        }        return s;    }}//饿汉式class Single{    private static final Single s = new Single();    private Single(){}    public static Single getInstance()    {        return s;    }}

6.5.5同步死锁:同步进行嵌套,锁却不同。
例如:同步函数中有同步代码块,同步代码块中还有同步函数。
//一个死锁程序

class Test implements Runnable{    private boolean flag;//创建一个标记号    Test(boolean flag)    {        this.flag = flag;    }    public void run()    {        if(flag)        {            while(true)                synchronized(MyLock.locka)                  {                    System.out.println(Thread.currentThread().getName()+"if locka");//a锁只能运行到这一步                    synchronized(MyLock.lockb)                    {                        System.out.println(Thread.currentThread().getName()+"if lockb");                    }                }        }        else        {            while(true)                    synchronized(MyLock.lockb)//b锁只能运行到这一步                    {                        System.out.println(Thread.currentThread().getName()+"else lockb");                        synchronized(MyLock.locka)                            {                            System.out.println(Thread.currentThread().getName()+"else locka");                            }                    }        }    }}class MyLock//定义两个锁{    public static final Object locka = new Object();    static Object lockb = new Object();}class  DeadLockTest{    public static void main(String[] args)     {        Thread t1 = new Thread (new Test(true));        Thread t2 = new Thread(new Test(false));        t1.start();        t2.start();    }}

6.5.6同步锁
jkd1.5后的另一种同步机制:
通过显示定义同步锁对象来实现同步,这种机制,同步锁应该使用Lock对象充当。
在实现线程安全控制中,通常使用ReentrantLock(可重入锁)。使用该对象可以显示地加锁和解锁。
具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

public class X {    private final ReentrantLock lock = new ReentrantLock();    //定义需要保证线程安全的方法    public void  m(){        //加锁        lock.lock();        try{            //... method body        }finally{            //在finally释放锁            lock.unlock();        }    }}修改后的例子://同步代码块package july7;class SellDemo implements Runnable{    private int num = 50;    @Override    public void run() {        for (int i = 0; i < 200; i++) {            synchronized (this) {                if(num > 0){                        try {                    //因为它不可以直接调用getName()方法,所以必须要获取当前线程。                        Thread.sleep(10);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                System.out.println(Thread.currentThread().getName()+"卖出第"+num--+"张票!");                }            }        }    }}public class Demo3 {    public static void main(String[] args) {        SellDemo s = new SellDemo();        new Thread(s,"A").start();        new Thread(s,"B").start();        new Thread(s,"C").start();    }}//同步方法package july7;//同步方法class FinalDemo1 implements Runnable {    private int num = 50;    @Override    public void run() {        for (int i = 0; i < 100; i++) {            gen();        }    }    public synchronized void gen() {        for (int i = 0; i < 100; i++) {            if (num > 0) {                try {                    Thread.sleep(10);                } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread().getName() + "卖出了第"                        + num-- + "张票!");            }        }    }}public class Demo6 {    public static void main(String[] args) {        FinalDemo1 f = new FinalDemo1();        new Thread(f, "A").start();        new Thread(f, "B").start();        new Thread(f, "C").start();    }}//线程同步锁package july7;import java.util.concurrent.locks.ReentrantLock;//同步锁class FinalDemo2 implements Runnable {    private int num = 50;    private final ReentrantLock lock = new ReentrantLock();    @Override    public void run() {        for (int i = 0; i < 100; i++) {            gen();        }    }    public void gen() {        lock.lock();        try{            //for (int i = 0; i < 100; i++) {                if (num > 0) {                    try {                        Thread.sleep(10);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName() + "卖出了第"                            + num-- + "张票!");                }            //}        }finally{            lock.unlock();        }    }}public class Demo7 {    public static void main(String[] args) {        FinalDemo2 f = new FinalDemo2();        new Thread(f, "A").start();        new Thread(f, "B").start();        new Thread(f, "C").start();    }}

6.6线程通信
线程间通信:思路:多个线程在操作同一个资源,但是操作的动作却不一样。
1:将资源封装成对象。
2:将线程执行的任务(任务其实就是run方法。)也封装成对象。

等待唤醒机制:涉及的方法:
wait:将同步中的线程处于冻结状态。释放了执行权,释放了资格。同时将线程对象存储到线程池中。
notify:唤醒线程池中某一个等待线程。
notifyAll:唤醒的是线程池中的所有线程。

注意:
1:这些方法都需要定义在同步中。
2:因为这些方法必须要标示所属的锁。
你要知道 A锁上的线程被wait了,那这个线程就相当于处于A锁的线程池中,只能A锁的notify唤醒。
3:这三个方法都定义在Object类中。为什么操作线程的方法定义在Object类中?
因为这三个方法都需要定义同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在Object类中。

wait和sleep区别: 分析这两个方法:从执行权和锁上来分析:
wait:可以指定时间也可以不指定时间。不指定时间,只能由对应的notify或者notifyAll来唤醒。
sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。
wait:线程会释放执行权,而且线程会释放锁。
Sleep:线程会释放执行权,但不是不释放锁。

线程的停止:通过stop方法就可以停止线程。但是这个方式过时了。
停止线程:原理就是:让线程运行的代码结束,也就是结束run方法。
怎么结束run方法?一般run方法里肯定定义循环。所以只要结束循环即可。
第一种方式:定义循环的结束标记。
第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过Thread类中的interrupt方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。

———< java.lang.Thread >———-
interrupt():中断线程。
setPriority(int newPriority):更改线程的优先级。
getPriority():返回线程的优先级。
toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。
setDaemon(true):将该线程标记为守护线程或用户线程。将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
join:临时加入一个线程的时候可以使用join方法。

当A线程执行到了B线程的join方式。A线程处于冻结状态,释放了执行权,B开始执行。A什么时候执行呢?只有当B线程运行结束后,A才从冻结状态恢复运行状态执行。

Lock接口:
多线程在JDK1.5版本升级时,推出一个接口Lock接口。
解决线程安全问题使用同步的形式,(同步代码块,要么同步函数)其实最终使用的都是锁机制。

到了后期版本,直接将锁封装成了对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。
在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。

所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。

在之前的版本中使用Object类中wait、notify、notifyAll的方式来完成的。那是因为同步中的锁是任意对象,所以操作锁的等待唤醒的方法都定义在Object类中。

而现在锁是指定对象Lock。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装。并提供了功能一致的方法 await()、signal()、signalAll()体现新版本对象的好处。

class BoundedBuffer {   final Lock lock = new ReentrantLock();   final Condition notFull  = lock.newCondition();    final Condition notEmpty = lock.newCondition();    final Object[] items = new Object[100];   int putptr, takeptr, count;   public void put(Object x) throws InterruptedException {     lock.lock();     try {       while (count == items.length)          notFull.await();       items[putptr] = x;        if (++putptr == items.length) putptr = 0;       ++count;       notEmpty.signal();     }     finally {       lock.unlock();     }   }   public Object take() throws InterruptedException {     lock.lock();     try {       while (count == 0)          notEmpty.await();       Object x = items[takeptr];        if (++takeptr == items.length) takeptr = 0;       --count;       notFull.signal();       return x;     } finally {       lock.unlock();     }   }  }

有一个数据存储空间,划分为两部分,一部分用于存储人的姓名,另一部分用于存储人的性别;
我们的应用包含两个线程,一个线程不停向数据存储空间添加数据(生产者),另一个线程从数据空间取出数据(消费者);
因为线程的不确定性,存在于以下两种情况:
若生产者线程刚向存储空间添加了人的姓名还没添加人的性别,CPU就切换到了消费者线程,消费者线程把姓名和上一个人的性别联系到一起;
生产者放了若干数据,消费者才开始取数据,或者是消费者取完一个数据,还没等到生产者放入新的数据,又重复的取出已取过的数据;

生产者和消费者

wait():让当前线程放弃监视器进入等待,直到其他线程调用同一个监视器并调用notify()或notifyAll()为止。
notify():唤醒在同一对象监听器中调用wait方法的第一个线程。
notifyAll():唤醒在同一对象监听器中调用wait方法的所有线程。

这三个方法只能让同步监听器调用:
在同步方法中: 谁调用
在同步代码块中: 谁调用

wait()、notify()、notifyAll(),这三个方法属于Object 不属于 Thread,这三个方法必须由同步监视对象来调用,两种情况:
1.synchronized修饰的方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中调用这三个方法;
2.synchronized修饰的同步代码块,同步监视器是括号里的对象,所以必须使用该对象调用这三个方法;
可要是我们使用的是Lock对象来保证同步的,系统中不存在隐式的同步监视器对象,那么就不能使用者三个方法了,那该咋办呢?
此时,Lock代替了同步方法或同步代码块,Condition代替了同步监视器的功能;
Condition对象通过Lock对象的newCondition()方法创建;
里面方法包括:
await(): 等价于同步监听器的wait()方法;
signal(): 等价于同步监听器的notify()方法;
signalAll(): 等价于同步监听器的notifyAll()方法;

例子:设置属性
容易出现的问题是:
名字和性别不对应!

线程通信,很好!

package july7;class Person{    private String name;    private String sex;    private Boolean isimpty = Boolean.TRUE;//内存区为空!    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public String getSex() {        return sex;    }    public void setSex(String sex) {        this.sex = sex;    }    public void set(String name,String sex){        synchronized (this) {            while(!isimpty.equals(Boolean.TRUE)){//不为空的话等待消费者消费!                try {                    this.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            this.name = name;//为空的话生产者创造!            this.sex = sex;            isimpty = Boolean.FALSE;//创造结束后修改属性!            this.notifyAll();        }    }    public void get(){        synchronized (this) {            while(!isimpty.equals(Boolean.FALSE)){                try {                    this.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            System.out.println("姓名"+getName()+ ",  "+"性别"+getSex());            isimpty = Boolean.TRUE;            this.notifyAll();        }    }}class Producer implements Runnable{    private Person p;    public Producer(Person p) {        super();        this.p = p;    }    @Override    public void run() {        for (int i = 0; i < 100; i++) {            if( i % 2 == 0){                p.set("刘昭", "男");            }else{                p.set("章泽天", "女");            }        }    }}class Consumer implements Runnable{    private Person p;    public Consumer(Person p) {        super();        this.p = p;    }    @Override    public void run() {        for (int i = 0; i < 100; i++) {            p.get();        }    }}public class Demo9 {    public static void main(String[] args) {        Person p = new Person();        new Thread(new Producer(p)).start();        new Thread(new Consumer(p)).start();    }}
0 0