Java多线程:线程通信:生产者 和 消费者 || 顺序打印

本文详细介绍了Java中线程通信的关键方法wait(),notify(),notifyAll(),解释了为何这些方法定义在Object而非Thread类中,并通过示例展示了它们与yield(),sleep()的区别。此外,还探讨了生产者/消费者模式,说明了如何利用这些方法实现线程间的协作和资源管理。
摘要由CSDN通过智能技术生成

Java多线程:线程通信:生产者 和 消费者 || 顺序打印

在这里插入图片描述


每博一文案

靠谱,是最高级的聪明:师父说:人生一回,道义一场。你对人对事的态度,藏着你一生的福报。
千金难买好人缘,人活的就是好口碑,别人怎么对你,是你的因果,你怎么对别人,是你的修行。
和谁在一起,决定了你是谁,而你是谁,就拥有怎样的人生。一个人为人处世最大的资本,不是才华也不是金钱,而是
看他靠不靠谱,良心,是做事的底线,人品,是做人的底牌,这世上从不缺优秀的人,难得的是靠谱的人,聪明的人
只适合聊聊天,但靠谱的人值得常伴心间,聪明只能让你走得快,但靠谱却能决定你走得多远。
所以,一个人最好的底牌就是“靠谱” 二字。
                                     ——————   一禅心灵庙语


1. 线程通信

1.1 wait(), notify(), notifyAll()等方法介绍

在Object.java中,定义了wait(), notify()和notifyAll()等方法。

  • wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁
  • notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;
  • notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
  • 这 wait(), notify(), notifyAll()方法的调用者必须是同代码块或是同步方法中的同步监视器 “锁” 对象一致,不然报错:IllegalMonitorStateException 异常
  • 这 三个wait(),notify(), notifyAll()方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常
  • 如下是图示说明

在这里插入图片描述

Object类中关于等待/唤醒的API详细信息如下:

  • notify() – 唤醒在此对象监视器上等待的单个线程。
  • notifyAll() – 唤醒在此对象监视器上等待的所有线程。
  • wait() – 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”)。
  • wait(long timeout) – 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
  • wait(long timeout, int nanos) – 让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入“就绪状态”)。

1.2 为什么notify(), wait()等函数定义在Object中,而不是Thread中

简单的解释的说明: 因为这三个方法必须有锁对象调用,而任意对象都可以作为 synchronized 的同步锁, 因此这三个方法只能在Object类中声明,因为所有的对象/任意对象就继承于 Object 类,才能对应上面的任意对象都可以作为 synchronized 的同步锁。

复杂逻辑解释:

Object中的wait(), notify()等函数,和synchronized一样,会对“对象的同步锁”进行操作。

wait()会使“当前线程”等待,因为线程进入等待状态,所以线程应该释放它锁持有的“同步锁”,否则其它线程获取不到该“同步锁”而无法运行!
OK,线程调用wait()之后,会释放它锁持有的“同步锁”;而且,根据前面的介绍,我们知道:等待线程可以被notify()或notifyAll()唤醒。
现在,请思考一个问题: notify()是依据什么唤醒等待线程的?或者说,wait()等待线程和notify()之间是通过什么关联起来的?答案是:依据“对象的同步锁”

负责唤醒等待线程的那个线程(我们称为 “唤醒线程” ),它只有在获取“该对象的同步锁”(这里的同步锁必须和等待线程的同步锁是同一个),并且调用notify()或notifyAll()方法之后,才能唤醒等待线程。虽然,等待线程被唤醒;但是,它不能立刻执行,因为唤醒线程还持有“该对象的同步锁”。必须等到唤醒线程释放了“对象的同步锁”之后,等待线程才能获取到“对象的同步锁”进而继续运行。

总之,notify(), wait()依赖于“同步锁”,而“同步锁”是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。

2. 顺序打印

使用两个线程打印 1-100。线程1, 线程2交替打印 : 线程1 打印偶数,线程2打印奇数

package blogs.blog4;

/**
 * 使用两个线程打印1-100。线程1, 线程2 交替打印  : 线程1 打印偶数,线程2打印奇数
 */
public class ThreadTest14 {
    public static void main(String[] args) {
        MyThread14 myThread14 = new MyThread14();

        Thread t1 = new Thread(myThread14);
        Thread t2 = new Thread(myThread14);

        t1.setName("线程一");
        t2.setName("线程二");

        t1.start();
        t2.start();

    }
}

/**
 * 打印偶数
 */
class MyThread14 implements Runnable {
    private int num = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                this.notify();  // 唤醒被 wait()阻塞的线程,打印偶数
                if (num <= 100) {
                    System.out.println(Thread.currentThread().getName() + "--->" + num++);
                    // 奇数打印完了,该线程进入阻塞状态,让另外一个线程进入,打印偶数
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                } else {
                    break;
                }
            }
        }
    }
}

在这里插入图片描述


  • 注意对于这 三个wait(), notify(), notifyAll()方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常

在这里插入图片描述

  • 这 wait(), notify(), notifyAll()方法的调用者必须是同代码块或是同步方法中的同步监视器 “锁” 对象一致,不然报错:IllegalMonitorStateException 异常

在这里插入图片描述


3. yield() 与 wait()的比较

我们知道,wait() 的作用是让当前线程由 “运行状态” 进入到 “等待(阻塞)状态”的同时,也会释放同步锁 。而 yield() 的作用是让步,它也会让当前线程离开 “运行状态”。它们的区别是:

  • wait() 是让线程由 “运行状态” 进入到 “等待(阻塞)状态”,而不是 yield() 是让线程由 “运行状态” 进入到 “就绪状态”
  • wait() 是会释放 它所持有的对象的同步锁,而 yield() 方法是不会 释放锁 的。
  • 两个方法的声明的位置不同,wait()必须在synchronized同步代码块/同步方法当中,而且调用的对象必须和同步监视器“锁”的对象一致,不然报,yield()任意
  • wait 是 Object 类的成员本地方法,sleep 是Thread 类的静态方法。

下面通过示例演示 yield() 是不会释放锁的

package blogs.blog4;

/**
 * yield() 与 wait()的比较
 */
public class ThreadTest15 {
    public static void main(String[] args) {
        MyThread15 myThread15 = new MyThread15();

        Thread t1 = new Thread(myThread15);
        Thread t2 = new Thread(myThread15);

        t1.setName("线程一");
        t2.setName("线程二");

        t1.start();
        t2.start();
    }
}


class MyThread15 implements Runnable {
    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
                if (i == 5) {
                    Thread.yield(); // 让当前线程进入 “就绪状态”,但不会释放线程中的锁.
                }
            }
        }
    }
}

在这里插入图片描述

解析:

主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(this)。在t1运行过程中,虽然它会调用Thread.yield() 让当前线程进入 阻塞状态,但是却不会释放手中的锁 ,导致 t2线程无法拿到 “锁” ,就无法进入 synchronized(this) {} 代码块中执行,这里只有等到 t1线程执行完后,释放手中的锁,t2线程才可以进入到 synchronized(this) {} 代码块中执行。所以就出现了上述的 一个线程一直在执行直到运行完。另外一个线程才可以执行。

下面对比演示:wait()是会释放锁的

package blogs.blog4;

/**
 * yield() 与 wait()的比较
 */
public class ThreadTest15 {
    public static void main(String[] args) {
        MyThread15 myThread15 = new MyThread15();

        Thread t1 = new Thread(myThread15);
        Thread t2 = new Thread(myThread15);

        t1.setName("线程一");
        t2.setName("线程二");

        t1.start();
        t2.start();
    }
}


class MyThread15 implements Runnable {
    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
                if (i == 5) {
                    try {
                        this.wait(); // 让当前线程进入 “等待/阻塞状态”,并释放线程中的锁.
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                this.notify();   // 唤醒被 wait()阻塞的线程
            }
        }
    }
}

在这里插入图片描述


4. sleep() 与 wait()的比较

我们知道,wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。而sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”。
但是,wait()会释放对象的同步锁,而sleep()则
不会释放锁

  • 相同点:一旦执行,都可以使得当前线程进入阻塞状态。以及都需要异常处理。
  • 不同点:
    1. 两个方法的声明的位置不同,两个方法的声明的位置不同,wait()必须在synchronized同步代码块/同步方法当中,而且调用的对象必须和同步监视器“锁”的对象一致,不然报,sleep()任意
    2. Wait 是 Object 类的成员方法,sleep 是Thread 类的静态本地方法
    3. sleep()阻塞不会释放手中的同步监视器“锁”,wait()阻塞的同时会释放手中的同步监视器"锁"

下面通过示例演示sleep()是不会释放锁的

package blogs.blog4;

/**
 * yield() 与 wait()的比较
 */
public class ThreadTest15 {
    public static void main(String[] args) {
        MyThread15 myThread15 = new MyThread15();

        Thread t1 = new Thread(myThread15);
        Thread t2 = new Thread(myThread15);

        t1.setName("线程一");
        t2.setName("线程二");

        t1.start();
        t2.start();
    }
}


class MyThread15 implements Runnable {
    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
                if (i == 5) {
                    try {
                        Thread.sleep(1000 * 1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

在这里插入图片描述

解析:

主线程main中启动了两个线程t1和t2。t1和t2在run()会引用同一个对象的同步锁,即synchronized(this)。在t1运行过程中,虽然它会调用Thread.sleep(1000 * 1) 让线程睡眠 1 s 进入阻塞状态,但是却不会释放手中的锁 ,导致 t2线程无法拿到 “锁” ,就无法进入 synchronized(this) {} 代码块中执行,这里只有等到 t1线程执行完后,释放手中的锁,t2线程才可以进入到 synchronized(this) {} 代码块中执行。所以就出现了上述的 一个线程一直在执行直到运行完。另外一个线程才可以执行。

下面对比演示:wait()是会释放锁的

package blogs.blog4;

/**
 * yield() 与 wait()的比较
 */
public class ThreadTest15 {
    public static void main(String[] args) {
        MyThread15 myThread15 = new MyThread15();

        Thread t1 = new Thread(myThread15);
        Thread t2 = new Thread(myThread15);

        t1.setName("线程一");
        t2.setName("线程二");

        t1.start();
        t2.start();
    }
}


class MyThread15 implements Runnable {
    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
                if (i == 5) {
                    try {
                        this.wait(); // 让当前线程进入 “等待/阻塞状态”,并释放线程中的锁.
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                this.notify();   // 唤醒被 wait()阻塞的线程
            }
        }
    }
}

在这里插入图片描述

5. 生产者/消费者模式

生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括“生产者”、“消费者”、“仓库”和“产品”。他们之间的关系如下:

  1. 生产者仅仅在仓储未满时候生产,仓满则停止生产。
  2. 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
  3. 当消费者发现仓储没产品可消费时候会通知生产者生产。
  4. 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
  • **对于生产方法produce()而言:**当仓库满时,生产者线程等待,需要等待消费者消费产品之后,生产线程才能生产;生产者线程生产完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“消费者线程”,即我们所说的“通知消费者进行消费”。
  • **对于消费方法consume()而言:**当仓库为空时,消费者线程等待,需要等待生产者生产产品之后,消费者线程才能消费;消费者线程消费完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“生产者线程”,即我们所说的“通知生产者进行生产”。

这是一种特殊的业务需求,在这种特殊的情况下需要使用 wait()方法和notify()方法/notifyAll() 方法

如下图示说明:

在这里插入图片描述

举例:先定义仓库最多只能存放 1 个产品,生产 1 个 产品,就消费 1 个产品

package blogs.blog4;

/**
 * 生产者和消费者模型
 */
public class ProductTest {
    public static void main(String[] args) {
        // 创建仓库的实例对象
        Clark clark = new Clark();

        // 创建两个线程:生产者/消费者
        Thread t1 = new Thread(new Product(clark));   // 生产线程,生产和消费是同一个仓库
        Thread t2 = new Thread(new Consumer(clark));  // 消费线程,生产和消费是同一个仓库

        // 设置线程名
        t1.setName("生产者");
        t2.setName("消费者");

        // 创建线程
        t1.start();
        t2.start();
    }
}


/**
 * 仓库
 */
class Clark {
    private int productCount = 0;

    public Clark() {

    }

    /**
     * 生产产品
     */
    public synchronized void produceProduct() {  // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
        // 仓库最多存放 1 个产品,仓库没有满生产
        if (productCount < 1) {
            // 生产 ++
            this.productCount++;
            System.out.println(Thread.currentThread().getName() + "-->开始生产第" + this.productCount + "个产品");
            // 生产完了,打开消费线程,消费
            this.notifyAll();  // 释放被 wait()阻塞的线程,优先高的优先
        } else {
            // 仓库满了,停止生产,停止生产线程 wait() 进入阻塞状态,并释放手中的锁。
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 消费产品
     */
    public synchronized void consumeProduct() { // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
        // 仓库中有产品,才可以消费
        if (this.productCount > 0) {
            // 消费
            System.out.println(Thread.currentThread().getName() + "-->开始消费第" + this.productCount + "个产品");
            this.productCount--;
            // 消费完了,打开生产线程,生产
            this.notifyAll();
        } else {
            // 仓库没有产品,停止消费,该消费线程进入 阻塞状态 wait(),并释放手中的锁
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 生产者
 */
class Product implements Runnable {
    // 定义仓库
    private Clark clark = null;

    public Product() {
        super(); // 调用父类中的构造器
    }

    public Product(Clark clark) {
        super(); // 调用父类中的构造器,必须首行(一次)
        this.clark = clark;
    }

    /**
     * 生产产品
     */
    @Override
    public void run() {
        while (true) {
            this.clark.produceProduct();
        }
    }
}


/**
 * 消费者
 */
class Consumer implements Runnable {
    private Clark clark = null;

    public Consumer() {
        super(); // 调用父类的构造器,首行(一次)
    }

    public Consumer(Clark clark) {
        super();  // 调用父类的构造器,首行(一行)
        this.clark = clark;
    }

    /**
     * 消费
     */
    @Override
    public void run() {
        while (true) {
            this.clark.consumeProduct();
        }
    }
}

在这里插入图片描述


举例:定义仓库最多只能存放 20 个产品,生产 20 个 产品,就消费 20 个产品

只需要将: 生产的仓库 if 判断修改一下就好了。

在这里插入图片描述

package blogs.blog4;

/**
 * 生产者和消费者模型
 */
public class ProductTest {
    public static void main(String[] args) {
        // 创建仓库的实例对象
        Clark clark = new Clark();

        // 创建两个线程:生产者/消费者
        Thread t1 = new Thread(new Product(clark));   // 生产线程,生产和消费是同一个仓库
        Thread t2 = new Thread(new Consumer(clark));  // 消费线程,生产和消费是同一个仓库

        // 设置线程名
        t1.setName("生产者");
        t2.setName("消费者");

        // 创建线程
        t1.start();
        t2.start();
    }
}


/**
 * 仓库
 */
class Clark {
    private int productCount = 0;

    public Clark() {

    }

    /**
     * 生产产品
     */
    public synchronized void produceProduct() {  // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
        // 仓库最多存放 1 个产品,仓库没有满生产
        if (productCount < 10) {
            // 生产 ++
            this.productCount++;
            System.out.println(Thread.currentThread().getName() + "-->开始生产第" + this.productCount + "个产品");
            // 生产完了,打开消费线程,消费
            this.notifyAll();  // 释放被 wait()阻塞的线程,优先高的优先
        } else {
            // 仓库满了,停止生产,停止生产线程 wait() 进入阻塞状态,并释放手中的锁。
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 消费产品
     */
    public synchronized void consumeProduct() { // 非静态方法 synchronized()默认同监视器"锁"是 this 对象锁,无法修改
        // 仓库中有产品,才可以消费
        if (this.productCount > 0) {
            // 消费
            System.out.println(Thread.currentThread().getName() + "-->开始消费第" + this.productCount + "个产品");
            this.productCount--;
            // 消费完了,打开生产线程,生产
            this.notifyAll();
        } else {
            // 仓库没有产品,停止消费,该消费线程进入 阻塞状态 wait(),并释放手中的锁
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 生产者
 */
class Product implements Runnable {
    // 定义仓库
    private Clark clark = null;

    public Product() {
        super(); // 调用父类中的构造器
    }

    public Product(Clark clark) {
        super(); // 调用父类中的构造器,必须首行(一次)
        this.clark = clark;
    }

    /**
     * 生产产品
     */
    @Override
    public void run() {
        while (true) {
            this.clark.produceProduct();
        }
    }
}


/**
 * 消费者
 */
class Consumer implements Runnable {
    private Clark clark = null;

    public Consumer() {
        super(); // 调用父类的构造器,首行(一次)
    }

    public Consumer(Clark clark) {
        super();  // 调用父类的构造器,首行(一行)
        this.clark = clark;
    }

    /**
     * 消费
     */
    @Override
    public void run() {
        while (true) {
            this.clark.consumeProduct();
        }
    }
}

在这里插入图片描述


4. 总结:

  1. wait() ,notify() ,notifyAll() 方法是定义在 Object 类当中的。
  2. wait() 是让当前线程进入阻塞状态,并释放当前线程占用的同步锁,notify() 唤醒被 wait() 阻塞的线程,优先级高的优先唤醒,仅仅只会唤醒一个线程,notifyAll() 唤醒被 wait() 阻塞的所有线程。
  3. wait() ,notify() ,notifyAll() 方法必须在 synchronized 代码块/方法中使用不然报java.lang.IllegalMonitorStateException 异常。
  4. wait(), notify(), notifyAll()方法的调用者必须是同代码块或是同步方法中的同步监视器 “锁” 对象一致,不然报:IllegalMonitorStateException 异常。
  5. 注意 yield() 与 wait() 的区别: yield () 让线程进入 ”就绪状态“,但是不会释放线程中的”锁“,wait() 让线程进入 ”阻塞状态“,并释放线程中的”锁“。
  6. 注意 sleep() 与 wait() 的区别:sleep() 让线程进入”等待/阻塞状态“,但是不会释放线程中的”锁“,wait() 让线程进入 ”阻塞状态“,并释放线程中的”锁“。
  7. 编写 生产者 / 消费者模型:达到 生产 和 消费的一个平衡。

5. 最后:

限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见 !!!


在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值