首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >JavaSE超详细笔记-多线程篇-基于黑马

JavaSE超详细笔记-多线程篇-基于黑马

作者头像
超级苦力怕
发布2025-12-23 17:41:15
发布2025-12-23 17:41:15
500
举报

1. 多线程的基本概念

1.1 进程与线程基本概念【理解】
  • 进程:正在运行的程序实例。
    • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
    • 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
    • 并发性:任何进程都可以同其他进程一起并发执行。
  • 线程:进程中的一条执行路径,即单个顺序控制流。
    • 单线程:如果一个进程只有一条执行路径,则称为单线程程序。
    • 多线程:如果一个进程有多条执行路径,则称为多线程程序。
1.2 多线程的作用【理解】
  • 提高效率,使多个任务可以同时进行。
1.3 多线程的应用场景【理解】
  • 只要你想要多个事情同时运行就需要用到多线程
    • 拷贝或迁移大文件。
    • 加载大量的资源文件。
    • 所有的聊天软件和后台服务器应用。
1.4 并发与并行基本概念【理解】
  • 并发:在同一时刻,多个指令在单个CPU上交替执行,给人一种同时进行的错觉。
  • 并行:在同一时刻,多个指令在多个CPU上真正同时执行,实现真正的并行处理。

2. 多线程的实现方式

  • 2.1 通过继承 Thread 类的方式实现多线程【应用】
    • 书写步骤
      • 定义一个类并继承 Thread
      • 在子类中重写 run() 方法
      • 创建子类的对象,并调用 start() 方法启动线程
    • 成员方法

    方法名说明void run()线程启动后,此方法将被自动调用,用于定义线程的执行逻辑void start()启动线程,Java虚拟机会自动调用 run() 方法

代码语言:javascript
复制
public class MyThread extends Thread{
    @Override
    public void run(){
        //书写线程要执行代码
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+" "+ "100次循环");
        }
    }
}

public class main {
    public static void main(String[] args) {
        MyThread t1=new MyThread();
        MyThread t2=new MyThread();
        MyThread t3=new MyThread();
        MyThread t4=new MyThread();

        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        t4.setName("线程4");

        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}
  • 两个要点
    • 为什么要重写run()方法?
      • 因为run()是用来封装被线程执行的代码
    • run()方法和start()方法的区别?
      • run():封装线程执行的代码,直接调用,相当于普通方法的调用
      • start():启动线程;然后由JVM调用此线程的run()方法
  • 2.2 实现Runnable接口的方式进行实现【应用】

方法名

说明

void run()

在线程开启后,此方法将被调用执行

void start()

使此线程开始执行,Java虚拟机会调用run方法()

  • 书写步骤
    • 定义一个类实现Runnable接口
    • 在类中重写run()方法(表示多线程要完成的任务)
    • 创建类的对象
    • 创建Thread类的对象,并开启线程
  • 方法介绍
代码语言:javascript
复制
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //获取到当前线程的对象
            Thread t = Thread.currentThread();
            //书写线程执行代码
            System.out.println(t.getName()+"再来一百次");
        }
    }
}

public class main {
    public static void main(String[] args) {
        //表示多线程要执行的任务
        MyRunnable mr1=new MyRunnable();
        //创建线程对象
        Thread t1=new Thread(mr1);
        Thread t2=new Thread(mr1);
        //给线程设置名字
        t1.setName("线程1");
        t2.setName("线程2");
        //开启线程
        t1.start();
        t2.start();
    }
}

2.3 利用Callable接口和Future接口方式实现【应用】

  • 方法介绍 方法名说明V call()计算结果,如果无法计算结果,则抛出一个异常FutureTask(Callable<V> callable)创建一个 FutureTask,一旦运行就执行给定的 CallableV get()如有必要,等待计算完成,然后获取其结果
  • 书写步骤
    • 定义一个类MyCallable实现Callable接口
    • 在MyCallable类中重写call()方法[返回值为多线程运行结果]
    • 创建MyCallable类的对象[表示多线程要执行的任务]
    • 创建Future的实现类FutureTask对象[作用:管理多线程运行的结果],把MyCallable对象作为构造方法的参数
    • 创建Thread类的对象[表线程],把FutureTask对象作为构造方法的参数
    • 启动线程
    • 再调用get方法,就可以获取线程结束之后的结果。

代码语言:javascript
复制
public class MyThread extends Thread{
    @Override
    public void run(){
        //书写线程要执行代码
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+" "+ "100次循环");
        }
    }
}

public class main {
    public static void main(String[] args) {
        MyThread t1=new MyThread();
        MyThread t2=new MyThread();
        MyThread t3=new MyThread();
        MyThread t4=new MyThread();

        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        t4.setName("线程4");

        t1.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

2.4 三种实现方式的对比【理解】

  • 实现Runnable、Callable接口
    • 好处: 扩展性强,实现该接口的同时还可以继承其他的类
    • 缺点: 编程相对复杂,不能直接使用Thread类中的方法
  • 继承Thread类
    • 好处: 编程比较简单,可以直接使用Thread类中的方法
    • 缺点: 可以扩展性较差,不能再继承其他的类


3. 多线程常见成员方法

  • 3.1 常见方法列表

方法名

说明

String getName()

返回此线程的名称

void setName(String name)

设置线程的名字(构造方法也可以设置名字)

static Thread currentThread()

获取当前线程的对象

static void sleep(long time)

让线程休眠指定的时间,单位为毫秒

setPriority(int newPriority)

设置线程的优先级

final int getPriority()

获取线程的优先级

final void setDaemon(boolean on)

设置为守护线程

public static void yield()

出让线程/礼让线程

public static void join

插入线程/插队线程

接下来,以线程基础,线程优先级,守护线程,礼让线程,插入线程的几个角度去拆分

  • 3.11 线程基础【应用】
    • 方法列表

方法名

说明

String getName()

返回此线程的名称

void setName(String name)

设置线程的名字(构造方法也可以设置名字)

static Thread currentThread()

获取当前线程的对象

  • 细节总和
    • 即使我们没有给线程设置名字,线程也是有默认名字 格式:Thread-X (从0开始) 原理:底层实际上给名字设置为"Thread-"+nextThreadNum( ) 【后者为自增变量】
    • 如果我们给线程设置名字,可以用set方法进行设置,也可以用构造方法设置
    • 当JVM虚拟机启动后,会自动启动多条线程,其中一条线程叫做main线程 他的作用就是去调用main方法,并执行里面的代码,之前在main方法里的代码,其实都是运行在main线程当中
代码语言:javascript
复制
public class MyThread extends Thread{
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 101; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}


public class Demo1 {
    public static void main(String[] args) {

        //创建线程对象
        MyThread t1=new MyThread();
        MyThread t2=new MyThread("飞机");
        //开启线程
        t1.start();
        t2.start();


    }
3.12 线程优先级【应用】
  • 两种调度方式
    • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
    • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
  • Java使用的是抢占式调度模型
  • 随机性 假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
  • 方法列表

方法名

说明

setPriority(int newPriority)

设置线程的优先级

final int getPriority()

获取线程的优先级

代码语言:javascript
复制
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 101; i++) {
            System.out.println(Thread.currentThread().getName()+"---"+i);
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        //创建线程要执行的参数对象
        MyRunnable mr=new MyRunnable();
        //创建线程对象
        Thread t1=new Thread(mr,"飞机");
        Thread t2=new Thread(mr,"坦克");

        t1.setPriority(1);
        t2.setPriority(10);


        System.out.println(Thread.currentThread().getPriority());

    }
}

注:此处优先级最高是10,最小是1,默认为5

代码语言:javascript
复制
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
3.13 守护线程【应用】
  • 方法列表

方法名

说明

void setDaemon(boolean on)

将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

  • 细节:当其他的非守护线程执行完毕之后,守护线程会陆续结束
  • 应用场景:例如QQ传输文件,当我们聊天结束的时候,传输文件也会断开,聊天可以看成非守护线程,传输文件可以看成守护线程
代码语言:javascript
复制
public class main {
    public static void main(String[] args) {
        // final void setDaemon(boolean on)     设置为守护线程
        MyThread1 mr1=new MyThread1("女神");
        MyThread2 mr2=new MyThread2("天沟");

//此处设置完,当mr1结束之后,mr2会短时间内终止进程,不管进程是否结束
        mr2.setDaemon(true);        
        mr1.start();
        mr2.start();

    }
}
3.14 礼让线程<了解>
  • 方法列表

方法名

说明

public static void yield( )

出让线程/礼让线程

代码语言:javascript
复制
public class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 101; i++) {
            System.out.println(getName()+"@"+i);
        }

        //出让当前CPU执行权
        Thread.yield();
    }
}

public class main {
    public static void main(String[] args) {
        //public static void yield()        出让线程/礼让线程

        MyThread t1=new MyThread();
        MyThread2 t2=new MyThread2();

        t1.setName("NPC1号");
        t2.setName("NPC2号");

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

注意点:这个方法只是尽可能的均匀一点,并非绝对,简单了解即可

3.15 插入线程<了解>
  • 方法列表

方法名

说明

public final void join( )

插入线程/插队线程

代码语言:javascript
复制
public class main {
    public static void main(String[] args) throws InterruptedException {
        MyThread t=new MyThread();
        t.setName("土豆");
        t.start();

        //把t线程插入到当前线程之前
        t.join();

        //执行在main线程当中的
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程"+i);
        }

    }
}

4. 线程安全的问题

4.1 卖票案例【实例】
  • 在多线程执行的过程中,会产生一些问题,下面是关于线程丢失cpu的执行权的案例

需求: 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

代码语言:javascript
复制
//Thread类
public class MyThread extends Thread{
    static int ticket =0;
    @Override
    public void run(){
        while (true){
            if (ticket<100){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println(getName()+"正在卖"+ticket+"张票!");
            }else {
                break;
            }
        }
    }

}

//主入口
public class main {
    public static void main(String[] args) {
        MyThread mr1=new MyThread();
        MyThread mr2=new MyThread();
        MyThread mr3=new MyThread();

        //起名字
        mr1.setName("窗口1");
        mr2.setName("窗口2");
        mr3.setName("窗口3");

        //开启线程
        mr1.start();
        mr2.start();
        mr3.start();
    }
}
4.2 卖票案例的问题【理解】
  • 卖票出现了问题
    • 相同的票出现了多次
    • 出现了负数的票
  • 问题产生原因
    • 线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题
4.3 解决数据安全问题【理解】
  • 安全问题出现的条件
    • 是多线程环境
    • 有共享数据
    • 有多条语句操作共享数据
  • 如何解决多线程安全问题呢?
    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢?
    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
    • Java提供了同步代码块的方式来解决
4.4 同步代码块【应用】
  • 概念:把操作共享数据的代码锁起来
  • 格式:

synchronized(任意对象) { 多条语句操作共享数据的代码 }

  • 特点
    • 锁默认打开,有一个线程进去了,锁自动关闭
    • 里面的代码全部执行完毕,线程出来,锁自动打开
  • 优缺点
    • 优点:解决了多线程的数据安全问题
    • 缺点:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
代码语言:javascript
复制
//Thread类
public class MyThread extends Thread{
    static int ticket =0;



    @Override
    public void run(){
            while (true){
            synchronized(MyThread.class){
                if (ticket<100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(getName()+"正在卖"+ticket+"张票!");
                }else {
                    break;
                }
            }
        }
    }

}

//测试类
public class main {
    public static void main(String[] args) {
        MyThread mr1=new MyThread();
        MyThread mr2=new MyThread();
        MyThread mr3=new MyThread();

        //起名字
        mr1.setName("窗口1");
        mr2.setName("窗口2");
        mr3.setName("窗口3");

        //开启线程
        mr1.start();
        mr2.start();
        mr3.start();
    }
}
  • 细节
    • 锁不能放在循环外面,否则线程只能等到循环结束的时候才能结束
    • 锁的对象一定是唯一的,否则无意义,一般为当前类的字节码文件
4.5 同步方法【应用】
  • 概念
    • 就是把synchronized关键字加到方法
  • 格式:

修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体; }

  • 特点
    • 同步方法是锁住方法里面所有的代码
    • 锁对象不能自己指定
      1. 非静态:this
      2. 静态:当前类的字节码文件对象
  • 书写步骤
    • 先写循环
    • 再写同步代码块(利用快捷键一键放到同步方法中)
    • 判断共享数据是否到了末尾,如果到了末尾
    • 判断共享数据是否到了末尾,如果没到末尾
代码语言:javascript
复制
//Runnable类
public class MyRunnable implements Runnable {
    int ticket = 0;

    @Override
    public void run() {
        while (true) {

            if (method()){
                break;
            }
        }
    }
    private synchronized boolean method() {
        if (ticket == 100) {
            return true;
        }
        if (ticket < 100) {

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket++;
        }
        return false;
    }
}

//测试类
public class main {
    public static void main(String[] args) {
        MyRunnable mr=new MyRunnable();
        Thread t1=new Thread(mr);
        Thread t2=new Thread(mr);
        Thread t3=new Thread(mr);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  • 拓展知识点StringBuilder和StringBuffer的选择
    • 通常我们会把StringBuilder作为单线程的使用,把StringBuffer作为多线程的使用
    • 原因是StringBuffer都使用了同步方法,安全指数比较高,但运行效率相对较低
4.6 Lock锁【应用】
  • 概念:提供了获得锁和释放锁的方法
  • 方法

方法

说明

void lock( )

获得锁

void unlock( )

释放锁

  • 特点
    • Lock是接口,不能实例化,采用它的实现类ReentrantLock实例化
    • ReentrantLock的构造方法
      • ReentrantLock():创建一个ReentrantLock的实例
代码语言:javascript
复制
//Thread类
public class MyThread extends Thread {
    static int ticket = 0;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (ticket == 100) {
                    break;
                } else {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    ticket++;
                    System.out.println(getName() + "正在卖" + ticket + "张票!");

                }
            } catch (RuntimeException e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }

        }
    }
}
//测试类
public class main {
    public static void main(String[] args) {
        /*
        某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票
        请设计一个程序模拟该电影院卖票
         */
        MyThread mr1=new MyThread();
        MyThread mr2=new MyThread();
        MyThread mr3=new MyThread();


        //起名字
        mr1.setName("窗口1");
        mr2.setName("窗口2");
        mr3.setName("窗口3");

        //开启线程
        mr1.start();
        mr2.start();
        mr3.start();
    }
}
  • 细节:
  • lock从获得锁开始,用try...catch括起来,直到finally后释放锁
    • 注:如果按正常流程,循环里面的break;会直接跳转到循环外面,从而错过释放锁,导致一直执行。

5. 死锁【理解】

注:死锁不是知识点,是错误,为了防止以后犯的错误

  • 概述
    • 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
  • 产生死锁的条件
    • 资源有限:系统中可用的资源数量不足以满足所有进程的需求。
    • 同步嵌套:多个同步操作嵌套在一起,导致进程互相等待。
代码语言:javascript
复制
public class MyThread extends Thread {

    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        //1.循环
        while (true) {
            if ("线程A".equals(getName())) {
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");//A
                    synchronized (objB) {
                        System.out.println("线程A拿到了B锁,顺利执行完一轮");
                    }
                }
            } else if ("线程B".equals(getName())) {
                if ("线程B".equals(getName())) {
                    synchronized (objB) {
                        System.out.println("线程B拿到了B锁,准备拿A锁");//B
                        synchronized (objA) {
                            System.out.println("线程B拿到了A锁,顺利执行完一轮");
                        }
                    }
                }
            }
        }
    }
}
  • 报错原因
    • 当线程A拿到了A锁的时候,线程B拿到了B锁,导致 objA objB 都被占用,A和B拿不到下一个锁,于是都卡住了[代码中//A和//B对应的是卡住位置],代码不会自动结束
  • 总结:
    • 以后写多线程的时候,尽量不要写锁嵌套

6. 生产者和消费者(等待唤醒机制)

6.1 基本概念【理解】
  • 概述
    • 生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。
  • 所谓生产者消费者问题,实际上主要是包含了两类线程:
    • 一类是生产者线程用于生产数据
    • 一类是消费者线程用于消费数据
  • 为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库[厨师=生产者,客人=消费者,菜=数据,桌上=共享数据区]
    • 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为[厨师做好菜放在桌上]
    • 消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为[客人把桌上的菜吃了]
    • 理解:[仍以厨师和客人为例]
      • 生产者与消费者的理想情况
        • 厨师抢到CPU执行权做一道菜,放桌子上,让客人吃,客人拿到CPU执行权吃完,桌上没有食物,厨师拿到CPU执行权继续做,周而复始
      • 可能出现的问题
        • 消费者等待
          • 客人抢到CPU执行权,因为桌上没菜[共享数据库没有数据],只能等着(wait),于是厨师抢到CPU执行权,做完菜,叫客人吃饭(notify)
        • 生产者等待
          • 厨师抢到CPU执行权,因为桌上没菜,做完菜,叫醒等待的客人(notify),但仍是厨师抢到CPU执行权,因为桌上有菜,继续等待,
  • Object类的等待和唤醒方法

方法名

说明

void wait()

导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法

void notify()

唤醒正在等待对象监视器的单个线程

void notifyAll()

唤醒正在等待对象监视器的所有线程

6.2 生产者和消费者案例【理解】
  • 案例需求
    • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量
    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务 1.判断是否有包子,决定当前线程是否执行 2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子 3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子
    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务 1.判断是否有包子,决定当前线程是否执行 2.如果没有包子,就进入等待状态,如果有包子,就消费包子 3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子
    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下 创建生产者线程和消费者线程对象 分别开启两个线程
代码语言:javascript
复制
public class Desk {

    //定义一个标记
    //true 就表示桌子上有汉堡包的,此时允许吃货执行
    //false 就表示桌子上没有汉堡包的,此时允许厨师执行
    public static boolean flag = false;

    //汉堡包的总数量
    public static int count = 10;

    //锁对象
    public static final Object lock = new Object();
}

public class Cooker extends Thread {
//    生产者步骤:
//            1,判断桌子上是否有汉堡包
//    如果有就等待,如果没有才生产。
//            2,把汉堡包放在桌子上。
//            3,叫醒等待的消费者开吃。
    @Override
    public void run() {
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{
                    if(!Desk.flag){
                        //生产
                        System.out.println("厨师正在生产汉堡包");
                        Desk.flag = true;
                        Desk.lock.notifyAll();
                    }else{
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

public class Foodie extends Thread {
    @Override
    public void run() {
//        1,判断桌子上是否有汉堡包。
//        2,如果没有就等待。
//        3,如果有就开吃
//        4,吃完之后,桌子上的汉堡包就没有了
//                叫醒等待的生产者继续生产
//        汉堡包的总数量减一

        //套路:
            //1. while(true)死循环
            //2. synchronized 锁,锁对象要唯一
            //3. 判断,共享数据是否结束. 结束
            //4. 判断,共享数据是否结束. 没有结束
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{
                    if(Desk.flag){
                        //有
                        System.out.println("吃货在吃汉堡包");
                        Desk.flag = false;
                        Desk.lock.notifyAll();
                        Desk.count--;
                    }else{
                        //没有就等待
                        //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }

    }
}

public class Demo {
    public static void main(String[] args) {
        /*消费者步骤:
        1,判断桌子上是否有汉堡包。
        2,如果没有就等待。
        3,如果有就开吃
        4,吃完之后,桌子上的汉堡包就没有了
                叫醒等待的生产者继续生产
        汉堡包的总数量减一*/

        /*生产者步骤:
        1,判断桌子上是否有汉堡包
        如果有就等待,如果没有才生产。
        2,把汉堡包放在桌子上。
        3,叫醒等待的消费者开吃。*/

        Foodie f = new Foodie();
        Cooker c = new Cooker();

        f.start();
        c.start();

    }
}
  • 总结步骤
    1. 循环
    2. 同步代码块
    3. 判断共享数据是否到了末尾(到了末尾的状况)
    4. 判断共享数据是否到了末尾(没到末尾的状况)
6.3 阻塞队列的继承结构【理解】
  • 示意图
  • 实现类
    • ArrayBlockingQueue :底层为数组,有界
    • LinkedBlockingQueue:底层是链表,无界[无界指没有长度的界限,最大为int的最大值]
  • BlockingQueue的核心方法:

方法

说明

put(anObject)

将参数放入队列,如果放不进去会阻塞

take()

取出第一个数据,取不到会阻塞

6.4 阻塞队列实现等待唤醒机制 案例【理解】
  • 案例需求
    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务 1.构造方法中接收一个阻塞队列对象 2.在run方法中循环向阻塞队列中添加包子 3.打印添加结果
    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务 1.构造方法中接收一个阻塞队列对象 2.在run方法中循环获取阻塞队列中的包子 3.打印获取结果
    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下 1.创建阻塞队列对象 2.创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象 3.分别开启两个线程
代码语言:javascript
复制
public class Cooker extends Thread {

    private ArrayBlockingQueue<String> bd;

    public Cooker(ArrayBlockingQueue<String> bd) {
        this.bd = bd;
    }
//    生产者步骤:
//            1,判断桌子上是否有汉堡包
//    如果有就等待,如果没有才生产。
//            2,把汉堡包放在桌子上。
//            3,叫醒等待的消费者开吃。

    @Override
    public void run() {
        while (true) {
            try {
                bd.put("汉堡包");
                System.out.println("厨师放入一个汉堡包");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Foodie extends Thread {
    private ArrayBlockingQueue<String> bd;

    public Foodie(ArrayBlockingQueue<String> bd) {
        this.bd = bd;
    }

    @Override
    public void run() {
//        1,判断桌子上是否有汉堡包。
//        2,如果没有就等待。
//        3,如果有就开吃
//        4,吃完之后,桌子上的汉堡包就没有了
//                叫醒等待的生产者继续生产
//        汉堡包的总数量减一

        //套路:
        //1. while(true)死循环
        //2. synchronized 锁,锁对象要唯一
        //3. 判断,共享数据是否结束. 结束
        //4. 判断,共享数据是否结束. 没有结束
        while (true) {
            try {
                String take = bd.take();
                System.out.println("吃货将" + take + "拿出来吃了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

public class Demo {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);

        Foodie f = new Foodie(bd);
        Cooker c = new Cooker(bd);

        f.start();
        c.start();
    }
}

细节: 生产者和消费者必须使用同一个阻塞队列

6.5 线程的六种状态【拓展,理解】
  • NEW (新建)---->创建线程对象 至今尚未启动的线程处于这种状态。
  • RUNNABLE (就绪)---->start方法 正在 Java 虚拟机中执行的线程处于这种状态。
  • BLOCKED (阻塞)---->无法获得锁对象 受阻塞并等待某个监视器锁的线程处于这种状态。
  • WAITING (等待)---->wait方法 无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
  • TIMED_WAITING (计时等待)---->sleep方法 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
  • TERMINATED (死亡)---->全部代码运行完毕 已退出的线程处于这种状态

7. 综合案例

几个案例代码不一定和黑马一样,仅供参考

  • 7.1 卖电影票
    • 一共有1000张电影票,可以在两个窗口领取,假设每次领取的时间为3000毫秒
    • 要求:请用多线程模拟卖票过程并打印剩余电影票的数量
代码语言:javascript
复制
//Thread类
public class MyThread extends Thread{
    static int ticket = 1000;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            lock.lock();
            if (ticket==0){
                break;
            }else {
                try {
                    sleep(3000);
                    ticket--;
                    System.out.println(getName()+"卖了一张票,还剩下"+ticket+"张票");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    lock.unlock();
                }

            }
        }

    }
}

//测试类
public class main {
    public static void main(String[] args) {
        //一共有1000张电影票,可以在两个窗口领取,假设每次领取时间为3000毫秒
        //要求:用多线程模拟卖票过程并打印剩余电影票数量

        MyThread mr1=new MyThread();
        MyThread mr2=new MyThread();

        mr1.setName("窗口1");
        mr2.setName("窗口2");

        mr1.start();
        mr2.start();
    }
}
  • 7.2 送礼品
    • 有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不再送出
    • 要求:利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来
代码语言:javascript
复制
//MyThread类
public class MyThread extends Thread{
    static int gift = 100;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            lock.lock();
            if (gift<10){
                break;
            }else {
                try {
                    sleep(3);
                    gift--;
                    System.out.println(getName()+"送了一个礼物,还剩下"+gift+"个礼物");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally {
                    lock.unlock();
                }

            }
        }

    }
}

//测试类
public class main {
    public static void main(String[] args) {
        MyThread mr1=new MyThread();
        MyThread mr2=new MyThread();

        mr1.setName("小明");
        mr2.setName("小红");

        mr1.start();
        mr2.start();
    }
}
  • 7.3 打印奇数数字
    • 同时开启两个线程,共同获取1-100之间的所有数字
    • 要求:将输出所有的奇数。
代码语言:javascript
复制
//MyThread类
public class MyThread extends Thread{
    static int i=0;
    @Override
    public void run() {
        while (true){
            synchronized (MyThread.class){
                if (i==100){
                    break;
                }else {
                    if (i%2==1){
                        System.out.println(i);
                        i++;
                    }else {
                        i++;
                    }
                }
            }
        }
    }
}

//测试类
public class main {
    public static void main(String[] args) {
        MyThread mr1=new MyThread();
        MyThread mr2=new MyThread();

        mr1.start();
        mr2.start();
    }
}
  • 7.4 抢红包
    • 需求:
      • 抢红包也用到了多线程。
      • 假设:100块,分成了3个包,现在有5个人去抢。
      • 其中,红包是共享数据。
      • 5个人是5条线程。
      • 打印结果如下:

XXX抢到了XXX元 XXX抢到了XXX元 XXX抢到了XXX元 XXX没抢到 XXX没抢到

代码语言:javascript
复制
//Thread类
public class Person extends Thread {
    static int redPacket = 3;
    static double money = 100;

    @Override
    public void run() {
            synchronized (Person.class) {
                if (redPacket == 0) {
                    System.out.println(getName() + "没抢到");
                } else if (redPacket == 1) {
                    redPacket--;
                    System.out.println(getName() + "抢到了" + money + "元,当前红包以抢完");
                } else {
                    double s = Math.random() * money;
                    money = money - s;
                    redPacket--;
                    System.out.println(getName() + "抢到了" + s + "元,当前还剩" + redPacket + "个红包");

                }
            }
        }
    }

//测试类
public class main {
    public static void main(String[] args) {
        Person t1=new Person();
        Person t2=new Person();
        Person t3=new Person();
        Person t4=new Person();
        Person t5=new Person();
        t1.setName("用户1");
        t2.setName("用户2");
        t3.setName("用户3");
        t4.setName("用户4");
        t5.setName("用户5");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}
  • 7.5 抽奖箱抽奖
  • 需求:
    • 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
    • 创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
    • 随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:

每次抽出一个奖项就打印一个(随机) 抽奖箱1 又产生了一个 10 元大奖 抽奖箱1 又产生了一个 100 元大奖 抽奖箱1 又产生了一个 200 元大奖 抽奖箱1 又产生了一个 800 元大奖 抽奖箱2 又产生了一个 700 元大奖

代码语言:javascript
复制
//Thread类
public class LuckyDraw extends Thread{
    static ArrayList<Integer> number=new ArrayList<>();

    public LuckyDraw(ArrayList a) {
        number=a;
    }

    @Override
    public void run() {
        while (true){
            synchronized (LuckyDraw.class){
                if (number.size()==0){
                    break;
                }else {
                    Collections.shuffle(number);
                    Integer remove = number.remove(0);
                    System.out.println(getName()+"又产生了一个"+remove+"元大奖");
                }
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

//测试类
public class main {
    public static void main(String[] args) {
        ArrayList<Integer> list=new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        LuckyDraw t1=new LuckyDraw(list);
        LuckyDraw t2=new LuckyDraw(list);

        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");

        t1.start();
        t2.start();
    }
}
  • 7.6 抽奖箱抽奖(多线程统计并求最大值)
  • 需求:
    • 在上一题基础上继续完成如下需求:
    • 每次抽的过程中,不打印,抽完时一次性打印(随机)
    • 在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
    • 分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
    • 在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
    • 分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
代码语言:javascript
复制
//Thread类
public class LuckyDraw extends Thread {
    static ArrayList<Integer> number = new ArrayList<>();
    int sum = 0;

    public LuckyDraw(ArrayList a) {
        number = a;
    }

    @Override
    public void run() {
        ArrayList<Integer> box = new ArrayList<>();
        while (true) {
            synchronized (LuckyDraw.class) {
                if (number.size() == 0) {
                    Collections.sort(box);
                    StringBuilder sb1 = new StringBuilder();
                    for (int i = 0; i < box.size(); i++) {
                        sum = sum + box.get(i);
                        if (i == (box.size() - 1)) {
                            sb1.append(box.get(i));
                            System.out.println("在此抽奖过程中" + getName() + "总共产生了6个选项,分别为:" + sb1 + "最高奖项为" + Collections.max(box) + "总金额为" + sum + "元");
                            break;
                        } else {
                            sb1.append(box.get(i) + ",");
                        }
                    }
                    break;
                } else {
                    Collections.shuffle(number);
                    Integer remove = number.remove(0);
                    box.add(remove);
                }
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

//测试类
public class main {
    public static void main(String[] args) {
        ArrayList<Integer> list=new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        LuckyDraw t1=new LuckyDraw(list);
        LuckyDraw t2=new LuckyDraw(list);

        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");

        t1.start();
        t2.start();
    }
}
  • 7.7 抽奖箱抽奖(多线程之间的比较)
  • 需求:
    • 在上一题基础上继续完成如下需求:
    • 在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为:10,20,100,500,2,300
  • 最高奖项为300元,总计额为932元
    • 在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为:5,50,200,800,80,700
  • 最高奖项为800元,总计额为1835元
    • 在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元
代码语言:javascript
复制
//Callable类
public class LuckyDraw implements Callable<Integer> {
    static ArrayList<Integer> number = new ArrayList<>();
    int sum = 0;
    int max=0;

    public LuckyDraw(ArrayList a) {
        number = a;
    }

    @Override
    public Integer call() throws Exception {
        ArrayList<Integer> box = new ArrayList<>();
        while (true) {
            synchronized (LuckyDraw.class) {
                if (number.size() == 0) {
                    Collections.sort(box);
                    StringBuilder sb1 = new StringBuilder();
                    if (box.size()==0){
                        return null;
                    }
                    for (int i = 0; i < box.size(); i++) {
                        sum = sum + box.get(i);
                        if (i == (box.size() - 1)) {
                            sb1.append(box.get(i));
                            max=Collections.max(box);
                            System.out.println("在此抽奖过程中" + Thread.currentThread().getName() + "总共产生了6个选项,分别为:" + sb1 + "最高奖项为" + max + "总金额为" + sum + "元");
                            break;
                        } else {
                            sb1.append(box.get(i) + ",");
                        }
                    }
                    break;
                } else {
                    Collections.shuffle(number);
                    Integer remove = number.remove(0);
                    box.add(remove);
                }
            }
            Thread.sleep(10);
        }
        return max;
    }
}

//测试类
public class main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ArrayList<Integer> list=new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        LuckyDraw l1=new LuckyDraw(list);

        FutureTask<Integer> ft1=new FutureTask<>(l1);
        FutureTask<Integer> ft2=new FutureTask<>(l1);

        Thread t1=new Thread(ft1);
        Thread t2=new Thread(ft2);

        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");

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

        Integer max1=ft1.get();
        Integer max2=ft2.get();

        System.out.println(max1);
        System.out.println(max2);

    }
}

8. 线程池【拓展】

  • 8.1 以前写多线程的弊端:
    • 用到线程的时候就创建
    • 用完之后线程消失
  • 8.2 线程池的核心原理
    • 创建一个池子,池子中是空的
    • 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不再需要创建新的线程,直接复用已有的线程即可
    • 但如果提交任务时,池子没有空闲线程,也无法创建新的线程,任务就会排队等待
  • 8.3 线程池的代码实现
    • 创建线程池
    • 提交任务
    • 所有任务执行完毕,关闭线程池

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象

方法名

说明

public static ExecutorService newCachedThreadPool( )

创建一个没有上限的线程池

public static ExecutorService newCachedThreadPool(int nThreads)

创建有上限的线程池

代码语言:javascript
复制
//Runnable类
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

//测试类
public class main {
    public static void main(String[] args) {

        //获得线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();
        //提交任务
        pool1.submit(new MyRunnable());
        //销毁线程池[一般不会销毁]
        pool1.shutdown();
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 多线程的基本概念
    • 1.1 进程与线程基本概念【理解】
    • 1.2 多线程的作用【理解】
    • 1.3 多线程的应用场景【理解】
    • 1.4 并发与并行基本概念【理解】
  • 2. 多线程的实现方式
  • 3. 多线程常见成员方法
    • 3.12 线程优先级【应用】
    • 3.13 守护线程【应用】
    • 3.14 礼让线程<了解>
    • 3.15 插入线程<了解>
  • 4. 线程安全的问题
    • 4.1 卖票案例【实例】
    • 4.2 卖票案例的问题【理解】
    • 4.3 解决数据安全问题【理解】
    • 4.4 同步代码块【应用】
    • 4.5 同步方法【应用】
    • 4.6 Lock锁【应用】
  • 5. 死锁【理解】
  • 6. 生产者和消费者(等待唤醒机制)
    • 6.1 基本概念【理解】
    • 6.2 生产者和消费者案例【理解】
    • 6.3 阻塞队列的继承结构【理解】
    • 6.4 阻塞队列实现等待唤醒机制 案例【理解】
    • 6.5 线程的六种状态【拓展,理解】
  • 7. 综合案例
  • 8. 线程池【拓展】
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档