多线程

Posted wffrzh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程相关的知识,希望对你有一定的参考价值。

(一)基本概念

程序-进程-线程

  • 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程(process)是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。
    • 如:运行中的QQ,运行中的MP3播放器
    • 程序是静态的,进程是动态的
  • 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
    • 若一个程序可同一时间执行多个线程,就是支持多线程的

进程与多进程

技术分享图片

何时需要多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  • 需要一些后台运行的程序时。

(二)线程的创建和启动

//单线程
public class Simple {
    public static void main(String[] args) {
        method2("atguigu.com");
    }

    public static void method2(String str) {
        System.out.println("method2...");
        method1(str);
    }

    public static void method1(String str) {
        System.out.println("method1...");
        System.out.println(str);
    }
}

----------运行结果-----------
method2...
method1...
atguigu.com

(三)多线程的创建和启动

  • Java语言的JVM允许程序运行多个线程,它通过==java.lang.Thread==类来实现。
  • Thread类的特性
    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
    • 通过该Thread对象的start()方法来调用这个线程

Thread类

  • 构造方法
    • ==Thread()==:创建新的Thread对象
    • ==Thread(String threadname)==:创建线程并指定线程实例名
    • ==Thread(Runnable target)==:指定创建线程的目标对象,它实现了Runnable接口中的run方法
    • ==Thread(Runnable target, String name)==:创建新的Thread对象

创建线程的两种方式

  1. 继承Thread类
    1. 定义子类继承Thread类。
    2. ==子类中重写Thread类中的run方法。==
    3. 创建Thread子类对象,即创建了线程对象。
    4. 调用线程对象start方法:启动线程,调用run方法。
  2. 实现Runnable接口
    1. 定义子类,实现Runnable接口。
    2. ==子类中重写Runnable接口中的run方法。==
    3. 通过Thread类含参构造器创建线程对象。
    4. ==将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。==
    5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。

继承Thread类

/**
 * 创建一个子线程,完成1-100之间自然数的输出,同样的,主线程完成同样的操作
 * 创建多线程的第一种方式:继承java.lang.Thread类
 */

//1.创建一个继承Thread的子类
class SubThread extends Thread {
    //2.重写Thread类中的run()方法,方法内实现子线程要完成的功能
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

public class TestThread {
    public static void main(String[] args) {
        //3.创建一个子类的对象
        SubThread subThread = new SubThread();
        //4.调用线程的start()方法:启动此线程,调用相应的run()方法
        //一个线程只能够执行一次start()
        //不能通过Thread实现类的run()方法去启动线程
        subThread.start();
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

运行效果:(两个线程交叉运行)

技术分享图片

Thread的常用方法

  1. start():启动线程并执行相应的run()方法
  2. run():子线程要执行的代码放入run()方法中
  3. currentThread():静态的,调取当前的线程
  4. getName():获取此线程的名字
  5. setName():设置此线程的名字
  6. yield():调用此方法的线程释放当前的CPU的执行权
  7. join():在A线程中调用B线程的join()方法,表示,当执行到此方法时,A线程执行停止,知道B线程自行完毕,A线程在接着join()之后的代码执行
  8. isAlive():判断此线程是否还存活
  9. sleep(long time):显示的让当前线程睡眠time毫秒
  10. 线程通信:wait() notify() notifyAll()
  11. 线程的优先级
    • 线程的优先级控制
      • ==MAX_PRIORITY(10);==
      • ==MIN _PRIORITY (1);==
      • ==NORM_PRIORITY(5);==
    • 涉及的方法:
      • ==getPriority()==:返回线程优先值
      • ==setPriority(int newPriority)== :改变线程的优先级
      • 线程创建时继承父线程的优先级

创建两个子线程,一个输出1-100之间的偶数,另一个输出1-100之间的奇数

class SubThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0)
                System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

class SubThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 1)
                System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

public class ThreadExer {
    public static void main(String[] args) {
        SubThread1 subThread1 = new SubThread1();
        SubThread2 subThread2 = new SubThread2();

        subThread1.start();
        subThread2.start();
    }
}

模拟火车站售票窗口

//模拟火车站售票窗口,开启三个售票窗口,总票数为20张
//存在线程的安全问题
class Window extends Thread {
    private static int ticket = 20;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0)
                System.out.println(Thread.currentThread().getName() + 
                                   "售票,票号为:" + ticket-- + "号");
            else
                break;
        }
    }
}

public class TestWindow {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}

------------运行结果---------------
窗口1售票,票号为:20号
窗口3售票,票号为:18号
窗口2售票,票号为:19号
窗口3售票,票号为:16号
窗口1售票,票号为:17号
窗口3售票,票号为:14号
窗口3售票,票号为:12号
窗口2售票,票号为:15号
窗口3售票,票号为:11号
窗口1售票,票号为:13号
窗口3售票,票号为:9号
窗口2售票,票号为:10号
窗口3售票,票号为:7号
窗口1售票,票号为:8号
窗口3售票,票号为:5号
窗口2售票,票号为:6号
窗口3售票,票号为:3号
窗口1售票,票号为:4号
窗口3售票,票号为:1号
窗口2售票,票号为:2号

实现Runnable接口

创建两个子线程,一个输出1-100之间的偶数,另一个输出1-100之间的奇数

//创建多线程的方式二:通过实现的方法
//1.创建一个实现Runnable接口的类
class PrintNum1 implements Runnable {
    //实现接口的抽象方法
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++)
            if (i % 2 == 1)
                System.out.println(Thread.currentThread().getName() + ":" + i);
    }
}

class PrintNum2 implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++)
            if (i % 2 == 0)
                System.out.println(Thread.currentThread().getName() + ":" + i);
    }
}

public class TestRunnable {
    public static void main(String[] args) {
        //3.创建一个Runnable接口实现的对象
        PrintNum1 printNum1 = new PrintNum1();
        PrintNum2 printNum2 = new PrintNum2();

        //要启动多线程,必须使用start()方法
        //4.将此对象最为形参传递给Thread类的构造器中,创建Thread类分对象,此对象即为一个线程
        Thread thread1 = new Thread(printNum1);
        //5.调用start()方法,启动线程并执行run()方法
        thread1.start();
        Thread thread2 = new Thread(printNum2);
        thread2.start();
    }
}

模拟火车站售票窗口

//使用Runnable接口的方法
//存在线程的安全问题,打印车票时,会出现重票、错票
class Window1 implements Runnable {
    private int ticket = 20;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0)
                System.out.println(Thread.currentThread().getName() + 
                                   "售票,票号为:" + ticket-- + "号");
            else
                break;
        }
    }
}

public class RunnableWindow {
    public static void main(String[] args) {
        Window1 window1 = new Window1();

        Thread thread1 = new Thread(window1);
        Thread thread2 = new Thread(window1);
        Thread thread3 = new Thread(window1);

        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

------------运行结果---------------
窗口1售票,票号为:20号
窗口1售票,票号为:17号
窗口1售票,票号为:16号
窗口3售票,票号为:18号
窗口3售票,票号为:14号
窗口2售票,票号为:19号
窗口3售票,票号为:13号
窗口1售票,票号为:15号
窗口3售票,票号为:11号
窗口2售票,票号为:12号
窗口3售票,票号为:9号
窗口1售票,票号为:10号
窗口3售票,票号为:7号
窗口2售票,票号为:8号
窗口3售票,票号为:5号
窗口1售票,票号为:6号
窗口3售票,票号为:3号
窗口2售票,票号为:4号
窗口3售票,票号为:1号
窗口1售票,票号为:2号

继承方式和实现方式的联系与区别

  • 联系
    • ==public class Thread extends Object implements Runnable==
  • 区别
    • 继承Thread:线程代码存放Thread子类run方法中。
    • 实现Runnable:线程代码存在接口的子类的run方法。
  • 实现方法的好处
    • 避免了单继承的局限性
    • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

使用多线程的优点

背景:只使用单个线程完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

线程分类

Java中的线程分为两类:一种是==守护线程==,一种是==用户线程==。

  • 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
  • 守护线程是用来服务用户线程的,通过在start()方法前调用==thread.setDaemon(true)==可以把一个用户线程变成一个守护线程。
  • Java垃圾回收就是一个典型的守护线程。
  • 若JVM中都是守护线程,当前JVM将退出。

(四)线程的生命周期

JDK中用Thread.State枚举表示了线程的几种状态

  • 要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
    • ==新建==: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
    • ==就绪==:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
    • ==运行==:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
    • ==阻塞==:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态死亡:线程完成了它的全部工作或线程被提前强制性地中止

技术分享图片

(五)线程的同步

  • 问题的提出
    • 多个线程执行的不确定性引起执行结果的不稳定
    • 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。

技术分享图片

技术分享图片

  1. 多线程出现了安全问题

  2. 问题的原因:

    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

  3. 解决方法

    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

  4. Java实现线程安全的方式

    • 同步代码块
    • 同步方法
  5. 线程同步的缺点

    • 由于同一个时间只能有一个线程访问共享数据,效率变低了

Synchronized的使用方法

  • 同步代码块

      synchronized (同步监视器){
              // 需要被同步的代码;(即为操作共享数据的代码)
        }
    1. 共享数据:多个线程共同操作的同一个数据(变量)

    2. 同步监视器:由一个类的对象来充当。那个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁

    3. 要求:所有的线程必须使用同一把锁!

      注:在实现的方式中,考虑同步的话,可以使用this来充当锁,但是在继承的方式中,慎用this

  • 同步方法

      public synchronized void show (String name){ 
                ····
        }
    1. 将操作共享数据的方法声明为synchronized。即此方法为同步方法,能够保证当每一个线程执行此方法时,其他线程在外等待直至此线程执行完成此方法。
    2. 同步方法的锁是当前对象this,没有显示写出

模拟火车站售票窗口,并解决线程安全问题

继承方式使用同步代码块

    static int ticket = 100;
    static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
//                    try {
//                        Thread.sleep(10);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
                    System.out.println(Thread.currentThread().getName() + 
                            "售票,票号为:" + ticket-- + "号");
                } else
                    break;
            }
        }
    }

实现方式使用同步代码块

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this){
                if (ticket > 0) {
//                    try {
//                        Thread.sleep(10);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
                    System.out.println(Thread.currentThread().getName() + 
                                       "售票,票号为:" + ticket-- + "号");
                }else
                    break;
            }
        }
    }

实现方式使用同步方法

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    public synchronized void show(){//同步方法
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + 
                               "售票,票号为:" + ticket-- + "号");
        }
    }

(六)互斥锁

在Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。

  • 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,==只能有一个线程访问该对象==。
  • 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。
  • 同步的局限性:导致程序的执行效率要降低
  • 同步方法(非静态的)的锁为this。
  • 同步方法(静态的)的锁为当前类本身。

懒汉式

解决单例模式中懒汉式的问题

//关于懒汉式的线程安全问题:使用同步机制
//对于一般的方法,使用同步代码块,可以考虑使用this
//对于静态方法而言,使用当前类本身充当锁
class Singleton {
    private Singleton() {
    }

    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
  • 当前线程在同步代码块、同步方法中执行了线程对象的==wait()==方法,当前线程暂停,并释放锁。

不释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用==Thread.sleep()、Thread.yield()==方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
    • 应尽量避免使用suspend()和resume()来控制线程

例题:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

/**
 * 1.是否涉及到多线程? 是,有两个储户
 * 2.是否有共享数据?   有,同一个储户
 * 3.考虑线程的同步
 */
class Account {
    double balance; //余额

    public Account() {

    }

    //存钱
    public synchronized void deposit(double amt) {
        balance += amt;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":" + balance);
    }
}

class Customer extends Thread {
    Account account;
    static Object object = new Object();

    public Customer(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            account.deposit(1000);
        }
    }
}

public class TestAccount {
    public static void main(String[] args) {
        Account acc = new Account();
        Customer c1 = new Customer(acc);
        Customer c2 = new Customer(acc);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

------运行结果-------
    甲:1000.0
    乙:2000.0
    乙:3000.0
    乙:4000.0
    甲:5000.0
    甲:6000.0

(七)死锁

线程的死锁问题

  • 死锁
    • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 解决方法
    • 专门的算法、原则
    • 尽量减少同步资源的定义

同步代码块的死锁

public class TestDeadLock {
    static StringBuffer sb1 = new StringBuffer();
    static StringBuffer sb2 = new StringBuffer();

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                synchronized (sb1) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("A");
                    synchronized (sb2) {
                        sb2.append("B");

                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
        
        new Thread() {
            @Override
            public void run() {
                synchronized (sb2) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sb1.append("C");
                    synchronized (sb1) {
                        sb2.append("D");

                        System.out.println(sb1);
                        System.out.println(sb2);
                    }
                }
            }
        }.start();
    }
}

同步方法的死锁

class A {
    public synchronized void foo(B b) {
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了A实例的foo方法"); // ①
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用B实例的last方法"); // ③
        b.last();
    }

    public synchronized void last() {
        System.out.println("进入了A类的last方法内部");
    }
}

class B {
    public synchronized void bar(A a) {
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 进入了B实例的bar方法"); // ②
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                + " 企图调用A实例的last方法"); // ④
        a.last();
    }

    public synchronized void last() {
        System.out.println("进入了B类的last方法内部");
    }
}

public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();

    public void init() {
        Thread.currentThread().setName("主线程");
        // 调用a对象的foo方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }

    public void run() {
        Thread.currentThread().setName("副线程");
        // 调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        new Thread(dl).start();
        dl.init();
    }
}

(八)线程通信

  • ==wait() 与 notify() 和 notifyAll()==
    • wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
    • notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
    • notifyAll ():唤醒正在排队等待资源的所有线程结束等待
  • Java.lang.Object提供的这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报==java.lang.IllegalMonitorStateException==异常

wait()

  • 在当前线程中调用方法: 对象名.wait()
  • 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  • 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
  • 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行

notify()/notifyAll()

  • 在当前线程中调用方法: 对象名.notify()
  • 功能:唤醒等待该对象监控权的一个线程。
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

使用两个线程打印1-100,线程1,线程2交替打印

//线程通信:如下的三个关键字使用的话,都得在同步代码块或同步方法中
//wait():一旦一个线程执行到wait(),就释放当前的锁
//notify()/notifyAll():唤醒wait的一个或多个线程

//使用两个线程打印1-100,线程1,线程2交替打印
class PrintNum implements Runnable {
    int num = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (num <= 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                } else
                    break;
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class TestCommunication {
    public static void main(String[] args) {
        PrintNum printNum = new PrintNum();
        Thread t1 = new Thread(printNum);
        Thread t2 = new Thread(printNum);

        t1.setName("甲");
        t2.setName("乙");

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

经典例题:生产者/消费者问题

  • 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
  • 这里可能出现两个问题:
    • 生产者比消费者快时,消费者会漏掉一些数据没有取到。
    • 消费者比生产者快时,消费者会取相同的数据。
/**
 * 1.是否涉及到多线程的问题? 是,生产者、消费者
 * 2.是否涉及到共享数据? 有,考虑数据的数量
 * 3.共享数据:产品数量
 * 4.是否涉及到线程的通信? 存在着生产者与消费者的通信
 */
class Clerk {
    int product;

    public synchronized void addProduct() { //生产产品
        if (product >= 20){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            product++;
            System.out.println(Thread.currentThread().getName() +
                    ":生产了第" + product + "件产品");
            notifyAll();
        }
    }

    public synchronized void consumeProduct() { //消费产品
        if (product <= 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println(Thread.currentThread().getName() +
                    ":消费了第" + product + "件产品");
            product--;
            notifyAll();
        }
    }
}

class Producer implements Runnable {
    Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("生产者开始生产产品");
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();
        }
    }
}

class Consumer implements Runnable {
    Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("消费者消费产品");
        while (true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}

public class ProduceConsume {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer1 = new Producer(clerk);
        Producer producer2 = new Producer(clerk);
        Consumer consumer1 = new Consumer(clerk);
        Consumer consumer2 = new Consumer(clerk);

        Thread pro1 = new Thread(producer1);
        Thread pro2 = new Thread(producer2);
        Thread con1 = new Thread(consumer1);
        Thread con2 = new Thread(consumer2);

        pro1.setName("生产者1");
        pro2.setName("生产者2");
        con1.setName("消费者1");
        con2.setName("消费者2");

        pro1.start();
        pro2.start();
        con1.start();
        con2.start();
    }
}

以上是关于多线程的主要内容,如果未能解决你的问题,请参考以下文章

线程学习知识点总结

多个请求是多线程吗

python小白学习记录 多线程爬取ts片段

多线程编程

多线程编程

python多线程