java学习day29(juc)

黑马javaSE中的多线程

1.实现多线程

1.1简单了解多线程【理解】

是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。

1.2并发和并行【理解】

  • 并行:在同一时刻,有多个指令在多个CPU上同时执行。

  • 并发:在同一时刻,有多个指令在单个CPU上交替执行。

其实这个说法有点问题那句定义里的"在同一时刻"用词不够严谨。正确的说法应该是:在"同一时间段"内,有多个指令在单个CPU上交替执行。

对于单个纯粹的物理核心(不考虑超线程等特殊技术)来说,在绝对的、微观的"同一时刻"(比如某一个微秒),它确实只能执行一条指令。

1.3进程和线程【理解】

  • 进程:是正在运行的程序

独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的并发性:任何进程都可以同其他进程一起并发执行

说白了就是一个软件运行之后,他就是一个进程

  • 线程:是进程中的单个顺序控制流,是一条执行路径

单线程:一个进程如果只有一条执行路径(也就是一个软件里面功能,比如这个木马查杀),则称为单线程程序

多线程:一个进程如果有多条执行路径,则称为多线程程序

多线程就是比如你在执行木马查杀过程这个线程是时候会有等待时间,然后把这个等待时间拿来去做电脑清理另一个线程,cpu可以来回切换,把等待时间重复利用起来

用这个就可以理解进程和线程了,360这个软件就可以看作是一个进程,然后这个软件里面的木马查杀,电脑清理,系统修复,优化加速都可以看作是这个进程里面的四个线程

多线程是什么,这个概念并不重要,我们只需要知道多线程就是同时让程序去做多件事情就行了

而多线程更多的是体现在并发,而不是并行 ,其实死扣定义,有两个cpu同时并行的执行了两个线程其实也叫多线程

比如我们看到电脑里面不是只有一个cpu,其实现在的cpu有2核4线程 ,4核8线程

后面这个线程就是你的电脑能同时并行执行多少个线程

针对"2核4线程"的电脑,用你刚才总结的知识来划分,情况确实就是你说的这样:

1. 真正的"并行":只能有 2 个

因为物理上只有 2 个核心 (也就是只有 2 个独立的办公室),所以在绝对的同一微秒内,只有 2 个任务能做到 完全互不干扰地同时执行 。核心A跑一个,核心B跑一个。

2. "4个线程"整体来看:本质上是"并发"

既然只有2个物理核心,要处理4个线程的任务,那必定有2个线程是被"折叠"进同一个核心里的。在同一个核心(同一个办公室)里运行的这 2 个线程,它们必须要共享这个核心内的资源。

  • 从宏观的操作系统层面看: 系统以为有 4 个核心,把 4 个任务扔过去"同时"跑,这是一种 并发 的宏观效果。
  • 从微观的物理硬件层面看: 能够做到 100% 独立、不抢资源的 并行 ,永远只有 2 个 (等于物理核心数)。同一个核心里的那另外两个线程,其实是在玩一种 极其高级、极其快速的"见缝插针式"并发

这就是为什么"2核4线程"的性能,绝对比不上真正的"4核4线程"。因为前者的打工人要抢工具(并发),后者的打工人每人都有自己独立的办公室和全套工具(真并行)

下面是ai的理解

复制代码
1. new Thread() 只是"招人",并没有"干活"
你在 Java 里写下 new Thread(),确实是创建了一个线程对象。
但此时,它绝对不会运行!
这就好比你作为餐厅老板,刚刚在人才市场签了一个新厨师(创建了对象),但他现在只是站在你面前,并没有进厨房炒菜。
要想让他真正跑起来,你必须手动调用 .start() 方法(老板大喊一声:"去炒菜!"),他才会真正进入执行状态。

2. JVM 一次性能运行几个线程?
这里要用到你上一问彻底搞懂的"并发"和"并行"概念了:

并行(真正同时干活): 取决于你电脑的物理 CPU 核心数。如果你是 8 核电脑,那 JVM 实际上在同一微秒内,最多只能真正"并行" 8 个线程。

并发(来回切换着干活): JVM 能同时管理和承载多少个线程?答案是:成千上万个! 只要你的内存足够大(Java 里一个线程默认占用大概 1MB 的栈内存),你开几千个线程都没问题。操作系统会安排这几千个线程在几个 CPU 核心上疯狂切换(并发)。

3. 核心错觉:我怎么"回到"我创建前的线程?
这是多线程里最重要的一次"观念洗礼":你从来就没有离开过原来的线程!根本不需要"回去"!

很多人的直觉是:程序是一条线往下走的,我 new 了一个新线程,程序就"跳"到新线程里去执行了,执行完我得想办法"跳回来"。大错特错!

真实的场景是**"分身术(岔路口)":
在 Java 程序启动时,默认就有一个"主线程(Main Thread)"**在执行你的代码。

当主线程(老板)走到 tt.start() 这一行时,它瞬间召唤出了一个"新线程(新厨师)"。

老板并没有变成新厨师! 新厨师端着他的菜谱(run 方法里的代码)去旁边的新灶台炒菜了。

而主线程(老板)会立刻、马上、毫不犹豫地继续往下执行 tt.start() 后面的代码!

也就是说,从 start() 调用的那一瞬间开始,你的程序兵分两路,变成了两条同时往前走的线! 它们互不干涉(除非你写代码强行让它们互相等待)。

下面关于线程冲突的两段理解

复制代码
照这么说并发应该是不会产生你说的资源互斥这种冲突的呀,因为在运行过程中,线程之间是交替执行,不可能在同时发生,你说的这种情况只可能在并行时候才会发生吧

Gemini 说
你这个问题问得太绝了!你的直觉非常敏锐,逻辑也非常严密。很多人学了好几年多线程,都没有像你这样深入思考过"交替执行"和"同时执行"对共享资源到底有什么不同影响。

我必须先肯定你的直觉:如果一件事是一气呵成的,那么并发(交替执行)确实绝对不会产生冲突。

但现实是残酷的,这也是无数程序员掉坑的地方------你以为的"一件事",在 CPU 眼里其实是"好几件事"!

并发之所以依然会产生严重的冲突,是因为 CPU 的"交替切换(上下文切换)"极其冷酷无情,它根本不会等你把一行 Java 代码执行完再切换。

我们还是用那个"最后一只烤鸭"的例子,我带你放慢一万倍镜头,看看在**纯单核 CPU(绝对没有并行,只有交替并发)**的情况下,悲剧是怎么发生的。

🔪 罪魁祸首:一行代码其实是三步操作(非原子性)
在 Java 里,你写了一行扣除库存的代码:
duck--;

但在 CPU 底层的机器码眼里,这根本不是一个动作,而是三个独立的步骤:

读(Read): 抬头看一眼黑板上的数字(把内存里的 duck=1 读到 CPU 寄存器里)。

算(Modify): 在脑子里把读到的数字减一(计算 1 - 1 = 0)。

写(Write): 拿起粉笔,把算好的结果写回黑板上(把 0 写回内存)。

🎬 灾难重现:单核并发下的"时间片"抢夺
现在只有一个单核 CPU,服务员 A 和 B 在交替执行(并发)。CPU 给每个人分配了微小的"时间片"(比如每人执行 10 毫秒就被强行暂停,换另一个人)。

时刻 1(A 在运行): 服务员 A 开始执行 duck--。他执行了第 1 步(读),看到黑板上写着 1。

⚠️ 致命切换(时间片耗尽): 就在 A 刚看完黑板,还没来得及在脑子里计算的时候,CPU 叮的一声:"A,你的时间到了,冻结!换 B 上场!"

时刻 2(B 在运行): 服务员 B 上场,也开始执行 duck--。他执行了第 1 步(读),因为 A 刚才没改黑板,所以 B 看到的也是 1。

时刻 3(B 在运行): B 继续执行第 2 步(算)和第 3 步(写)。脑子里算 1-1=0,然后擦掉黑板上的 1,写上了 0。此时,B 成功卖出一只烤鸭。B 的时间片结束,被冻结。

时刻 4(切换回 A): CPU 切回 A。A 瞬间解冻,他不会重新去读黑板,而是接着自己刚才被打断的记忆往下走!他的记忆停留在"刚才读到的是 1"。

时刻 5(A 在运行): A 继续执行第 2 步(算)和第 3 步(写)。他在脑子里算 1-1=0,然后擦掉黑板上的 0,重新又写了一遍 0。

💥 结果: 黑板上的数字从 1 变成了 0。系统以为只卖出去了 1 只烤鸭。
但是,服务员 A 和 B 各自都成功下了一单,给两个顾客端上了烤鸭!后厨少了两只鸭子!

💡 核心总结
这就是多线程并发最恐怖的地方:指令交错(Interleaving)。

并行(多核): 是 A 和 B 两个人同时抬头看黑板,同时算,同时写,导致冲突。

并发(单核): 是 A 看了一半被定住了,B 跑过去看、算、写完,然后 A 解冻接着写,导致了一模一样的冲突。

所以,无论是并发还是并行,只要大家去碰同一个共享数据,只要这个动作不是"不可分割的最小单元(原子操作)",就一定会引发线程安全问题!

那既然主线程 和我创建的线程可以并行运行,如果不行,也可以并发运行,那java里面的线程冲突怎么会产生呢

Gemini 说
这个问题问得简直太有深度了!你完全触及到了多线程编程最核心、最让人头疼的灵魂命题:线程安全(Thread Safety)。

既然主线程和新线程可以各跑各的(并发或并行),井水不犯河水,那为什么还会打架呢?

答案只有四个字:共享资源。

如果两个线程完全独立,自己玩自己的数据,那1万年也不会冲突。但现实业务中,它们往往需要操作同一份数据。我们继续用"餐厅"的例子,给你演示一场极其经典的"线程冲突灾难"。

🦆 灾难现场:最后一只烤鸭(超卖问题)
假设你们餐厅的后台系统里有一个变量,记录着烤鸭的库存:int duck = 1;(只剩最后1只了)。

现在,前台有两个服务员(线程 A 和 线程 B)同时在用手机点单系统,并且刚好在同一秒钟,都有顾客要点这最后一只烤鸭。

正常人的逻辑是:谁先点到,鸭子归谁,另一个人应该看到库存为 0,点单失败。但因为并发/并行的存在,可怕的事情发生了:

第一步(检查):

线程 A(服务员A)看了一眼系统:duck > 0 成立吗?成立,是一只。

⚠️ 致命瞬间(CPU 发生切换或并行执行): 就在服务员 A 刚确认完有鸭子,还没来得及扣库存的时候,CPU 突然去执行线程 B 了(或者在多核上同时执行)。

线程 B(服务员B)也看了一眼系统:duck > 0 成立吗?因为 A 还没扣除,所以 B 看到的也是 1。成立!

第二步(执行扣减):

线程 B 兴高采烈地下单,把系统里的鸭子扣掉:duck = 0;

CPU 切换回线程 A: 线程 A 的记忆还停留在刚才"确认有鸭子"的那一刻,它才不管现在到底是多少,直接按刚才的流程强行往下走,也执行了下单和扣减:duck = -1;

💥 砰!冲突产生了!
厨房里只有1只鸭子,系统里却卖出了2只,库存变成了 -1。这就叫经典的线程安全问题(或者叫竞态条件 Race Condition)。

💻 底层原因:动作不是"一气呵成"的(非原子性)
你可能会觉得,扣库存不就是一句代码 duck-- 吗?怎么会被打断?

在 Java 和计算机底层,一句极其简单的 duck--,其实分成了三个独立的步骤:

读取:把现在的 duck 值(1)从内存读到 CPU 里面。

计算:在 CPU 里执行 1 - 1 = 0。

写入:把算出来的 0 重新写回内存里。

因为多线程是"并发交替"或"并行同时"运行的,所以在这三个微小的步骤之间,CPU 随时可能被切走,去跑另一个线程。只要步骤被打断,数据就全乱套了。

🛡️ 怎么解决这个冲突?(剧透一下)
既然冲突是因为大家"不排队、乱抢共享资源"导致的,那解决办法也很简单粗暴------加锁(Lock)!

在 Java 里有个极其出名的关键字叫 synchronized(同步锁)。
这就好比给装烤鸭的冰箱加了一把实体锁。服务员 A 去拿鸭子的时候,咔嚓一下把冰箱锁死。这时候不管 CPU 怎么切换,服务员 B 就算想拿,也只能在冰箱门外干瞪眼(线程阻塞等待)。直到 A 拿完鸭子、改完库存、打开锁,B 才能去检查冰箱。

继承Thread类

1.4实现多线程方式一:继承Thread类【应用】

  • 方法介绍

|--------------|----------------------------|
| 方法名 | 说明 |
| void run() | 在线程开启后,此方法将被调用执行 |
| void start() | 使此线程开始执行,Java虚拟机会调用run方法() |

  • 实现步骤
    • 定义一个类MyThread继承Thread类
    • 在MyThread类中重写run()方法
    • 创建MyThread类的对象
    • 启动线程
  • 代码演示

    public class MyThread extends Thread {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println(i);
    }
    }
    }
    public class MyThreadDemo {
    public static void main(String[] args) {
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();

    复制代码
          //        my1.run();
          //        my2.run();
    
          //void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
          my1.start();
          my2.start();
          
      }

    }

  • 两个小问题

    • 为什么要重写run()方法?

因为run()是用来封装被线程执行的代码

    • run()方法和start()方法的区别?

run():封装线程执行的代码,在这里如果想要开启线程,不能直接调用run方法,相当于普通方法的调用

必须得用这个start():启动线程;然后由JVM调用此线程的run()方法

还需要注意的是上面代码中的my1线程和my2线程是交替执行

准备代码:主线程与子线程的赛跑

我们写一段最简单的代码。主线程打印"A",新诞生的子线程打印"B"。

Java

复制代码
public class ThreadRace {
    public static void main(String[] args) {
        System.out.println("步骤 1:主线程启动了!");

        // 主线程创建了一个新线程对象(但此时还没启动)
        MyThread my1 = new MyThread();

        System.out.println("步骤 2:主线程准备按动 start 开关!");
        my1.start(); // 🚀 见证奇迹的时刻!

        // start 执行完毕后,主线程继续做自己的事
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程在干活: A");
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        // 这是子线程的活儿
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程在干活: B");
        }
    }
}

2. 慢动作回放: **start()**到底发生了什么?

想象你的电脑里有一个 包工头(操作系统)

第一阶段:主线程孤独地工作

  1. JVM 启动,自动创建了 主线程(Main)
  2. 主线程开始按顺序执行代码。打印出"步骤 1",然后像捏泥人一样,在内存里捏了一个 my1****对象。
  3. 打印出"步骤 2"。此时, 世界上依然只有一个主线程在干活。

第二阶段:拨通 **start()**电话

  1. 主线程执行到了 my1.start()****。
  2. 注意!主线程 并没有 跳进 **run()**方法里去执行代码。
  3. 相反, **start()**就像是主线程给包工头(操作系统)打了个电话:"喂,老唐!马上给我招一个新工人,让他去执行 **run()**里面的图纸(代码),搞快点!"
  4. 打完电话,主线程 根本不关心 新工人什么时候来,也不等他,主线程立刻挂断电话,继续往下跑,去打印属于自己的"A"了。

第三阶段:两条平行线(多线程启动!)

  1. 包工头(操作系统)接到电话后,在后台紧急招募了一个新工人( 子线程 诞生!)。
  2. 包工头把这个新工人塞进 CPU 的等待队列里。
  3. 突然,CPU 空闲了(或者轮到子线程的时间片了),新工人猛地睁开眼睛,他自动找到了 **run()**方法,开始疯狂打印属于自己的"B"。

3. 最核心的真相:谁先谁后全靠"抢"

一旦 **my1.start()**这个电话打完, 主线程和子线程就成了完全平等的竞争对手!

它们会一起在包工头那里排队,疯狂地抢夺 CPU(医生)的注意力。因为 CPU 切换得太快了,所以最终在控制台打印出来的结果是 交替出现的、且每次运行都不一样的

可能是这样的:

主线程在干活: A 主线程在干活: A 子线程在干活: B 主线程在干活: A 子线程在干活: B子线程在干活: B

实现Runnable接口

1.5实现多线程方式二:实现Runnable接口【应用】

  • Thread构造方法

|--------------------------------------|----------------|
| 方法名 | 说明 |
| Thread(Runnable target) | 分配一个新的Thread对象 |
| Thread(Runnable target, String name) | 分配一个新的Thread对象 |

  • 实现步骤
    • 定义一个类MyRunnable实现Runnable接口
    • 在MyRunnable类中重写run()方法
    • 创建MyRunnable类的对象
    • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
    • 启动线程
  • 代码演示

    public class MyRunnable implements Runnable {
    @Override
    public void run() {
    for(int i=0; i<100; i++) {
    System.out.println(Thread.currentThread().getName()+":"+i);
    //因为getname是thread特有的方法,之前继承thread类可以直接调用父类的方法
    //但是在这里不行,要获取到当前tread类对象
    }
    }
    }
    public class MyRunnableDemo {
    public static void main(String[] args) {
    //创建MyRunnable类的对象
    MyRunnable my = new MyRunnable();

    复制代码
          //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
          //Thread(Runnable target)
          //        Thread t1 = new Thread(my);
          //        Thread t2 = new Thread(my);
          //Thread(Runnable target, String name)
          Thread t1 = new Thread(my,"坦克");
          Thread t2 = new Thread(my,"飞机");
    
          //启动线程
          t1.start();
          t2.start();
      }

    }

实现Callable接口

1.6实现多线程方式三: 实现Callable接口【应用】

这个方法有什么不一样的,前面两种方法我们发现run方法根本就没有返回值全是
void

  • 方法介绍

|------------------------------------|-------------------------------------|
| 方法名 | 说明 |
| V call() | 计算结果,如果无法计算结果,则抛出一个异常 |
| FutureTask(Callable<V> callable) | 创建一个 FutureTask,一旦运行就执行给定的 Callable |
| V get() | 如有必要,等待计算完成,然后获取其结果 |

  • 实现步骤
    • 定义一个类MyCallable实现Callable接口
    • 在MyCallable类中重写call()方法
    • 创建MyCallable类的对象
    • 创建Future的实现类FutureTask对象(因为future是一个接口,不能创建futre对象,要创建它的实现类futuretask对象),把MyCallable对象作为构造方法的参数(这个Futrue对象是用来管理多线程运行的结果的)
    • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
    • 启动线程
    • 再调用get方法,就可以获取线程结束之后的结果。
  • 代码演示

    public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    for (int i = 0; i < 100; i++) {
    System.out.println("跟女孩表白" + i);
    }
    //返回值就表示线程运行完毕之后的结果
    return "答应";
    }
    }
    public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    //线程开启之后需要执行里面的call方法
    MyCallable mc = new MyCallable();

    复制代码
          //Thread t1 = new Thread(mc);
    
          //可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
          FutureTask<String> ft = new FutureTask<>(mc);
    
          //创建线程对象
          Thread t1 = new Thread(ft);
    
          String s = ft.get();
          //开启线程
          t1.start();
    
          //String s = ft.get();
          System.out.println(s);
      }

    }

  • 三种实现方式的对比

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

讲完了这三种方式之后,我们来做一个对比如果是想要获取到返回的接口 只能用callable ,但是第一种是最简单的,但是缺点是扩展性差,java里面是单继承,也就是说如果你继承了,就无法再继承别的类了

线程里面常用的成员方法

这里面的出让线程/和插入线程 用到很少,这里就不用学了

这里看源码有一个小技巧

按下CTRL+f12 可以直接预览里面的所有方法

光标放在 nextThreadNum() 上并按下 Ctrl + B,它的作用是:跳转到声明(Go to Declaration)

不要搞的这么高深,其实

Ctrl + B****和 Ctrl + 鼠标左键点击****的功能是完全等价的

具体是在干什么?

  • 追根溯源: 你图里高亮的 nextThreadNum() 只是在这个构造方法里被"调用"(也就是被用了一下)。
  • 查看底层逻辑: 当你按下 Ctrl + B,编译器就会立刻跳到这个方法真正被定义(写出具体逻辑)的地方

这里有一个小细节 我们知道异常处理方式有两种,一种是抛出异常(这个是最懒人的,但是父类也必须抛,不然子类没法抛,比如thread类run方法是没有抛出异常,所以以后我们在继承thread类里面run方法也不能抛出异常) 另一种就是try catch了,这个就是自己处理了。

设置和获取线程名称【应用】

  • 方法介绍

|---------------------------|---------------------------------------------------------|
| 方法名 | 说明 |
| void setName(String name) | 将此线程的名称更改为等于参数name |
| String getName() | 返回此线程的名称 |
| Thread currentThread() | 返回对当前正在执行的线程对象的引用 (比如你如果在我们main方法里面写以这个获取到的就是mian线程的对象) |

  • 代码演示

    public class MyThread extends Thread {
    public MyThread() {}
    public MyThread(String name) {
    super(name);
    }

    复制代码
      @Override
      public void run() {
          for (int i = 0; i < 100; i++) {
              System.out.println(getName()+":"+i);
          }
      }

    }
    public class MyThreadDemo {
    public static void main(String[] args) {
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();

    复制代码
          //void setName(String name):将此线程的名称更改为等于参数 name
          my1.setName("高铁");
          my2.setName("飞机");
    
          //Thread(String name)
          MyThread my1 = new MyThread("高铁");
          MyThread my2 = new MyThread("飞机");
    
          my1.start();
          my2.start();
    
          //static Thread currentThread() 返回对当前正在执行的线程对象的引用
          System.out.println(Thread.currentThread().getName());
      }

    }

这里有一点我要说一下,构造方法是不能继承的,如果你子类想要使用父类的构造方法,需要自己去写一个构造方法,利用super关键词去调用

当jvm虚拟机在启动的时候,会自动去启动多条线程,其中有一条线程叫做main线程,main线程的作用其实就是去调用main方法,并执行里面所有的代码

线程休眠【应用】

  • 相关方法

|--------------------------------|--------------------------|
| 方法名 | 说明 |
| static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |

  • 代码演示

    public class MyRunnable implements Runnable {
    @Override
    public void run() {
    for (int i = 0; i < 100; i++) {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }

    复制代码
              System.out.println(Thread.currentThread().getName() + "---" + i);
          }
      }

    }
    public class Demo {
    public static void main(String[] args) throws InterruptedException {
    /System.out.println("睡觉前");
    Thread.sleep(3000);
    System.out.println("睡醒了");
    /

    复制代码
          MyRunnable mr = new MyRunnable();
    
          Thread t1 = new Thread(mr);
          Thread t2 = new Thread(mr);
    
          t1.start();
          t2.start();
      }

    }

线程优先级【应用】

  • 线程调度
    • 两种调度方式
      • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
      • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些 也就是多个cpu在抢夺cpu 的使用权
    • Java使用的是抢占式调度模型
    • 随机性

假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

  • 优先级相关方法

|-----------------------------------------|-----------------------------------|
| 方法名 | 说明 |
| final int getPriority() | 返回此线程的优先级 |
| final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10 |

优先级越高,抢到cpu的概率就越高,注意这里用词是概率,不是绝对的。低优先级也可能先执行完

  • 代码演示

    public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
    for (int i = 0; i < 100; i++) {
    System.out.println(Thread.currentThread().getName() + "---" + i);
    }
    return "线程执行完毕了";
    }
    }
    public class Demo {
    public static void main(String[] args) {
    //优先级: 1 - 10 默认值:5
    MyCallable mc = new MyCallable();

    复制代码
          FutureTask<String> ft = new FutureTask<>(mc);
    
          Thread t1 = new Thread(ft);
          t1.setName("飞机");
          t1.setPriority(10);
          //System.out.println(t1.getPriority());//5
          t1.start();
    
          MyCallable mc2 = new MyCallable();
    
          FutureTask<String> ft2 = new FutureTask<>(mc2);
    
          Thread t2 = new Thread(ft2);
          t2.setName("坦克");
          t2.setPriority(1);
          //System.out.println(t2.getPriority());//5
          t2.start();
      }

    }

守护线程【应用】

  • 相关方法

|----------------------------|--------------------------------------|
| 方法名 | 说明 |
| void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |

  • 代码演示

    public class MyThread1 extends Thread {
    @Override
    public void run() {
    for (int i = 0; i < 10; i++) {
    System.out.println(getName() + "---" + i);
    }
    }
    }
    public class MyThread2 extends Thread {
    @Override
    public void run() {
    for (int i = 0; i < 100; i++) {
    System.out.println(getName() + "---" + i);
    }
    }
    }
    public class Demo {
    public static void main(String[] args) {
    MyThread1 t1 = new MyThread1();
    MyThread2 t2 = new MyThread2();

    复制代码
          t1.setName("女神");
          t2.setName("备胎");
    
          //把第二个线程设置为守护线程
          //当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
          t2.setDaemon(true);
    
          t1.start();
          t2.start();
      }

    }

在这例子中备胎线程是女神线程的守护线程,当女神线程执行完毕之后,备胎线程也没有守护的必要了会陆续结束

但是这里有一个十分容易混淆的点

因为代码里把 t1 叫"女神",t2 叫"备胎",从人类的逻辑来看,我们自然会觉得"这个备胎是专门守护这个女神的"。但实际上,Java 底层的逻辑根本不是 1 对 1 绑定的。

之所以没有指定 t2****是谁的守护线程,是因为 守护线程(Daemon Thread)从来不是为某一个特定线程服务的,它是为"整个 JVM(Java虚拟机)进程"服务的。

要理解它是怎么知道何时该停止的,你只需要记住 JVM 的一条**"铁律"**:

只要整个系统中还有哪怕一个"普通线程(用户线程)"在运行,JVM 就不会关闭。 当系统中所有的"普通线程"都执行完毕后,不管"守护线程"有没有跑完,JVM 都会直接拉闸关机,所有守护线程瞬间死亡。

结合你的代码,我们来还原一下案发现场:

  1. 刚启动时: 程序里其实有 3 个 线程在跑。
    • main****主线程(普通线程)
    • t1****女神线程(普通线程)
    • t2****备胎线程(因为 t2.setDaemon(true)****,它变成了守护线程)
  1. 第一步: main****方法执行完 t1.start() **t2.start()后,它自己没代码了,于是 **main线程结束
  2. 第二步: 此时 JVM 环顾四周,发现虽然 main****死了,但是 t1**(普通线程)还在运行。因为还有普通线程活着,所以 JVM 继续保持运行,** t2****也就跟着正常跑。
  3. 第三步: 一段时间后, t1****的 run()方法执行完了, **t1**线程结束
  4. 第四步: 此时 JVM 再次环顾四周,发现 没有任何一个普通线程活着了 。虽然 t2**(守护线程)可能还在无限循环里等着,但 JVM 会冷酷地说:"真正的主人们都走了,没必要继续维持环境了,关机!"**
  5. 结局: JVM 退出, t2****被迫强行终止。

那这个守护线程有什么实际的应用场景吗?

比如说在这里 我们可以把聊天看作一个线程1,然后传输文件看作是线程2,当我们把qq的聊天窗口关闭时候,其实传输文件这个线程也应该关闭了

线程的生命周期

先来讲一下线程的生命周期

这个图讲的非常精辟,一个线程如果当我们把它new出来之后,它其实是就绪状态,直到我们使用start方法调用之后才会变成有执行资格 ,没有执行权的就绪态,也就是说这个时候我们有去枪cpu的权利,只是还没抢到
抢到cpu之后我们就会变成运行状态,我们用sleep睡眠之后就会变成阻塞态,这个时候就是没有执行资格,也没有执行权

所以sleep之后线程并不会马上就去执行,它此时还是就绪态,必须得抢cpu的执行权,抢到了才能接下来往下执行

线程安全

多线程看似可以提高效率,但是实际会出现安全问题

线程同步

卖票【应用】

  • 案例需求

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

  • 实现步骤
    • 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
    • 在SellTicket类中重写run()方法实现卖票,代码步骤如下
    • 判断票数大于0,就卖票,并告知是哪个窗口卖的
    • 卖了票之后,总票数要减1
    • 票卖没了,线程停止
    • 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
    • 创建SellTicket类的对象
    • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
    • 启动线程
  • 代码实现

    public class SellTicket implements Runnable {
    private int tickets = 100;
    //在SellTicket类中重写run()方法实现卖票,代码步骤如下
    @Override
    public void run() {
    while (true) {
    if(ticket <= 0){
    //卖完了
    break;
    }else{
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    ticket--;
    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
    }
    }
    }
    }
    public class SellTicketDemo {
    public static void main(String[] args) {
    //创建SellTicket类的对象
    SellTicket st = new SellTicket();

    复制代码
          //创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
          Thread t1 = new Thread(st,"窗口1");
          Thread t2 = new Thread(st,"窗口2");
          Thread t3 = new Thread(st,"窗口3");
    
          //启动线程
          t1.start();
          t2.start();
          t3.start();
      }

    }

卖票案例的问题【理解】

  • 卖票出现了问题
    • 相同的票出现了多次
    • 出现了负数的票
  • 问题产生原因

线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题

总结来说引发线程安全最重要的原因就是cpu的执行权随时都有可能被其他线程抢走

所以我们的解决的方法是把这个操作共享数据的代码给锁起来,让所有的代码在这段共享的代码里面能轮流执行

有个形象例子,厕所里面只有一个蹲坑呢,你和别的好哥们抢夺蹲坑的使用权,抢到之后把门关起来,别人用不了

同步代码块解决数据安全问题【应用】

  • 安全问题出现的条件
    • 是多线程环境
    • 有共享数据
    • 有多条语句操作共享数据
  • 如何解决多线程安全问题呢?
    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢?
    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
    • Java提供了同步代码块的方式来解决
  • 同步代码块格式:

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

synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

  • 同步的好处和弊端
    • 好处:解决了多线程的数据安全问题
    • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
  • 代码演示

    public class SellTicket implements Runnable {
    private int tickets = 100;
    private Object obj = new Object();

    复制代码
      @Override
      public void run() {
          while (true) {
              synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
                  //t1进来后,就会把这段代码给锁起来
                  if (tickets > 0) {
                      try {
                          Thread.sleep(100);
                          //t1休息100毫秒
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      //窗口1正在出售第100张票
                      System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                      tickets--; //tickets = 99;
                  }
              }
              //t1出来了,这段代码的锁就被释放了
          }
      }

    }

    public class SellTicketDemo {
    public static void main(String[] args) {
    SellTicket st = new SellTicket();

    复制代码
          Thread t1 = new Thread(st, "窗口1");
          Thread t2 = new Thread(st, "窗口2");
          Thread t3 = new Thread(st, "窗口3");
    
          t1.start();
          t2.start();
          t3.start();
      }

    }

这里有个object 对象,就是锁对象,这个对象可以是任意的,所以我们这里才随便给它创建了一个object对象,锁对象必须是唯一的,这里因为我们只用的是同一个runnable实现类,所以就是 唯一 的同一个

如果是继承therad类,那么三个线程的object对象就是三个不同的,此时我们就得必须加上static关键词让它变成全局变量

如果锁对象不是唯一的,那这个各自看各自的锁,那这个锁就没有意义了

所以最规范的写法是,我们会写上当前类的字节码对象.class对象,这个对象一定是唯一的,并且是全局唯一的

同步方法解决数据安全问题【应用】

如果我们想要把某个方法里面所有代码全部锁上,就没有没有必要使用这个了上面的同步代码块了 ,可以用一个叫同步方法的方式

但是同步方法里面的锁对象,和同步代码块里面的锁对象不同,同步方法里面的锁对象不能自己指定,都是java 里面已经规定好的,

但是很多人一上来并不会写同步方法里面的锁,那我们可以先写一个同步代码块里面的锁

比如这里我们就有一个小技巧ctrl+alt+m 就可以抽取出一个方法了

  • 同步方法的格式

同步方法:就是把synchronized关键字加到方法上

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

同步方法的锁对象是什么呢?

this

在这里this 就是mr(因为这个方法就是在myrunnbale实现类里面定义的所以当然是这个) ,mr是 唯一的所以我们的锁对象也是唯一的

  • 静态同步方法

同步静态方法:就是把synchronized关键字加到静态方法上

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

同步静态方法的锁对象是什么呢?

类名.class

  • 代码演示

    public class MyRunnable implements Runnable {
    private static int ticketCount = 100;

    复制代码
      @Override
      public void run() {
          while(true){
              if("窗口一".equals(Thread.currentThread().getName())){
                  //同步方法
                  boolean result = synchronizedMthod();
                  if(result){
                      break;
                  }
              }
    
              if("窗口二".equals(Thread.currentThread().getName())){
                  //同步代码块
                  synchronized (MyRunnable.class){
                      if(ticketCount == 0){
                          break;
                      }else{
                          try {
                              Thread.sleep(10);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          ticketCount--;
                          System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
                      }
                  }
              }
    
          }
      }
    
      private static synchronized boolean synchronizedMthod() {
          if(ticketCount == 0){
              return true;
          }else{
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              ticketCount--;
              System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
              return false;
          }
      }

    }

public class Demo { public static void main(String[] args) { MyRunnable mr = new MyRunnable();

复制代码
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);

t1.setName("窗口一");
t2.setName("窗口二");

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

}

复制代码
### 2.5Lock锁【应用】

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

- ReentrantLock构造方法

| 方法名             | 说明                   |
| --------------- | -------------------- |
| ReentrantLock() | 创建一个ReentrantLock的实例 |

- 加锁解锁方法

| 方法名           | 说明   |
| ------------- | ---- |
| void lock()   | 获得锁  |
| void unlock() | 释放锁  |

- 代码演示

```java
public class Ticket implements Runnable {
    //票的数量
    private int ticket = 100;
    private Object obj = new Object();
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //synchronized (obj){//多个线程必须使用同一把锁.
            try {
                lock.lock();
                if (ticket <= 0) {
                    //卖完了
                    break;
                } else {
                    Thread.sleep(100);
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            // }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

Lock锁

通过之前同步代码块我们知道,这里面的锁会自动加上,又是会自动关闭。所以对于锁的开关我们没有办法去手动干预

就是说必须得Lock lock = new ReentratLock才行(因为Lock 是接口不能直接实例化)

但是这样写是有问题的,这样写就创建出了三把不同Lock 锁,相当于根本没锁

要加上static关键词 例如: static Lock lock= new ReentrartLock

synchronized****同步代码块(自动挡)

synchronized 是 Java 中的关键字,它的锁机制是由 JVM(Java 虚拟机)在底层自动控制的。

  • 机制: 当线程进入 synchronized { ... } 块时,JVM 会自动获取锁(底层通过 monitorenter 指令);当线程离开 这个代码块的作用域时,JVM 会自动释放锁(底层通过 monitorexit 指令)。
  • 无论你怎么离开: 无论是代码正常执行完毕,还是遇到了 break 跳出循环、return 结束方法,甚至是在代码块里抛出了异常 ,JVM 都会保证在离开大括号的那一刻,自动把锁释放掉
  • 因此,如果你之前用的是 synchronizedbreak 跳出循环的同时退出了同步块,锁就自动解开了,其他线程就能进来了。

2. Lock****接口(例如 ReentrantLock**)(手动挡)**

你在截图里使用的 Lock 是 JDK 5 引入的 API 层面(Java 代码层面)的锁。它最大的特点就是:必须手动加锁,手动解锁。

  • 加锁: lock.lock()
  • 解锁: lock.unlock()

问题所在: 在你的代码第 18 行到 24 行:

  • Java

    if(ticket == 100){
    break;
    }
    // ... 省略卖票逻辑 ...
    Lock.unlock();

ticket 增加到 100 时,某个线程进来了,执行了 lock.lock() 拿到了锁。然后遇到 if(ticket == 100) 为真,直接执行了 break;break 会让程序直接跳出 while(true)**循环 。这导致它完美地跳过** 了最后那句 Lock.unlock();

结果: 这个线程带着锁"跑路"了(循环结束,线程死亡),但是锁从来没有被打开过。剩下的两个窗口(线程)还在 lock.lock() 那里死死地排队等待,导致整个程序死锁,永远不会停止。


如何解决 Lock****的这个问题?

使用 Lock 时,为了防止因为 breakreturn 或者抛出异常导致锁没有被释放,我们通常有两种改法:

方案一:在 break 之前手动解锁(简单直接)

既然 break 会跳过后面的代码,那就在 break 走人之前,先把锁放下:

Java

复制代码
if (ticket == 100) {
    Lock.unlock(); // 走之前先把锁解开
    break;
}

方案二:使用 try...finally****结构(业界绝对的规范和标准)

在实际开发中,使用 Lock 必须配合 try...finally,因为 finally 块里的代码无论发生什么情况(包括 break, return 或异常),都一定会执行。这也是 IDEA 等开发工具推荐的做法。

你应该把代码改成这样:

Java

复制代码
while (true) {
    Lock.lock(); // 1. 加锁
    try {
        if (ticket == 100) {
            break; // 即使这里 break 跳出循环
        } else {
            Thread.sleep(10);
            ticket++;
            System.out.println(getName() + "在卖第" + ticket + "张票");
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        Lock.unlock(); // 2. 无论上面是 break 还是抛异常,这里必定执行,绝对不会死锁
    }
}

快捷键是alt +t

死锁【理解】

那什么是死锁

死锁说白了就是在我们的程序中出现了锁的嵌套 ,我们要说明一下死锁他不是一个知识点,他是一个错误,我门学习死锁的目的是为了以后不要犯这个错误

在这段代码里面线程A和线程B 都在等对方

其实死锁我们不需要记得太多,只要记得以后我们写锁的时候,不要让两个锁 嵌套 起来就欧了

  • 概述

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

  • 什么情况下会产生死锁
    1. 资源有限
    2. 同步嵌套

生产者消费者

生产者和消费者也叫做等待唤醒机制

那什么叫做等待唤醒机制呢,我们都知道线程的执行他是有随机性的,但是我们学习的这个等待唤醒机制

就是要打破这个随机执行的规则,他会让两个顺序轮流执行,你一次,我一次

其实一共就只有两种情况

第一种就是就是消费者等待的情况 ,消费者这边如果没有他会进行wait,如果生产者这边做好了他会notify

在写这种多线程代码,阿伟告诉我们要分四部走

第一部要写循环,第二部要写同步代码块 如上图所示

生产者和消费者模式概述【应用】

  • 概述

生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

所谓生产者消费者问题,实际上主要是包含了两类线程:

一类是生产者线程用于生产数据

一类是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类的等待和唤醒方法

|------------------|--------------------------------------------------|
| 方法名 | 说明 |
| void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 |
| void notify() | 唤醒正在等待对象监视器的单个线程 |
| void notifyAll() | 唤醒正在等待对象监视器的所有线程 |

生产者和消费者案例【应用】

  • 案例需求
    • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量
    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

1.判断是否有包子,决定当前线程是否执行

2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子

3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

1.判断是否有包子,决定当前线程是否执行

2.如果没有包子,就进入等待状态,如果有包子,就消费包子

3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

创建生产者线程和消费者线程对象

分别开启两个线程

  • 代码实现

    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();
    
      }

    }

在这里不能直接调用wait方法,要用锁对象去调用wait方法,为什么要这样写呢?

因为这里唤醒肯定不是唤醒所有线程,肯定是唤醒这把锁绑定的所有对象,所以在这里其实就是让当前线程与锁之间进行绑定

然后我们还看到他这里用了一个final,加了final是为了安全性的考虑,这样就没法赋值了

加了 final****后,这个变量就死死地绑定在了最初分配的那个内存地址上。你 绝不能 使用 **=**号让它指向一个新的对象。

生产者和消费者案例优化【应用】

  • 需求
    • 将Desk类中的变量,采用面向对象的方式封装起来
    • 生产者和消费者类中构造方法接收Desk类对象,之后在run方法中进行使用
    • 创建生产者和消费者线程对象,构造方法中传入Desk类对象
    • 开启两个线程
  • 代码实现

    public class Desk {

    复制代码
      //定义一个标记
      //true 就表示桌子上有汉堡包的,此时允许吃货执行
      //false 就表示桌子上没有汉堡包的,此时允许厨师执行
      //public static boolean flag = false;
      private boolean flag;
    
      //汉堡包的总数量
      //public static int count = 10;
      //以后我们在使用这种必须有默认值的变量
      // private int count = 10;
      private int count;
    
      //锁对象
      //public static final Object lock = new Object();
      private final Object lock = new Object();
    
      public Desk() {
          this(false,10); // 在空参内部调用带参,对成员变量进行赋值,之后就可以直接使用成员变量了
      }
    
      public Desk(boolean flag, int count) {
          this.flag = flag;
          this.count = count;
      }
    
      public boolean isFlag() {
          return flag;
      }
    
      public void setFlag(boolean flag) {
          this.flag = flag;
      }
    
      public int getCount() {
          return count;
      }
    
      public void setCount(int count) {
          this.count = count;
      }
    
      public Object getLock() {
          return lock;
      }
    
      @Override
      public String toString() {
          return "Desk{" +
          "flag=" + flag +
          ", count=" + count +
          ", lock=" + lock +
          '}';
      }

    }

    public class Cooker extends Thread {

    复制代码
      private Desk desk;
    
      public Cooker(Desk desk) {
          this.desk = desk;
      }
      //    生产者步骤:
      //            1,判断桌子上是否有汉堡包
      //    如果有就等待,如果没有才生产。
      //            2,把汉堡包放在桌子上。
      //            3,叫醒等待的消费者开吃。
    
      @Override
      public void run() {
          while(true){
              synchronized (desk.getLock()){
                  if(desk.getCount() == 0){
                      break;
                  }else{
                      //System.out.println("验证一下是否执行了");
                      if(!desk.isFlag()){
                          //生产
                          System.out.println("厨师正在生产汉堡包");
                          desk.setFlag(true);
                          desk.getLock().notifyAll();
                      }else{
                          try {
                              desk.getLock().wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
              }
          }
      }

    }

    public class Foodie extends Thread {
    private Desk desk;

    复制代码
      public Foodie(Desk desk) {
          this.desk = desk;
      }
    
      @Override
      public void run() {
          //        1,判断桌子上是否有汉堡包。
          //        2,如果没有就等待。
          //        3,如果有就开吃
          //        4,吃完之后,桌子上的汉堡包就没有了
          //                叫醒等待的生产者继续生产
          //        汉堡包的总数量减一
    
          //套路:
          //1. while(true)死循环
          //2. synchronized 锁,锁对象要唯一
          //3. 判断,共享数据是否结束. 结束
          //4. 判断,共享数据是否结束. 没有结束
          while(true){
              synchronized (desk.getLock()){
                  if(desk.getCount() == 0){
                      break;
                  }else{
                      //System.out.println("验证一下是否执行了");
                      if(desk.isFlag()){
                          //有
                          System.out.println("吃货在吃汉堡包");
                          desk.setFlag(false);
                          desk.getLock().notifyAll();
                          desk.setCount(desk.getCount() - 1);
                      }else{
                          //没有就等待
                          //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
                          try {
                              desk.getLock().wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
              }
          }
    
      }

    }

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

    复制代码
          /*生产者步骤:
          1,判断桌子上是否有汉堡包
          如果有就等待,如果没有才生产。
          2,把汉堡包放在桌子上。
          3,叫醒等待的消费者开吃。*/
    
          Desk desk = new Desk();
    
          Foodie f = new Foodie(desk);
          Cooker c = new Cooker(desk);
    
          f.start();
          c.start();
    
      }

    }

在这里

当代码执行到 Desk.lock.wait(); 这一行时,会立刻发生一系列非常关键的底层动作。

当你执行 Desk.lock.wait() 时,当前线程会立即释放它持有的锁。

  • 动作: 消费者把"钥匙"丢回桌子上。
  • 结果: 此时,锁(钥匙)处于空闲状态,生产者或者其他消费者现在可以抢这把钥匙进入 synchronized 代码块了。

2. 线程去哪了?(进入"等待池")

执行了 wait() 的线程并不会在门口堵着,而是会进入一个叫 Wait Set(等待集) 的地方去睡觉。

  • 在这个状态下,它不参与 CPU 的调度,也不去抢锁。
  • 它在等一个"信号"。

3. 被 **notify()**叫醒后,发生了什么?

这是最容易产生误解的地方。 **notify()**叫醒不代表立刻继续执行代码。

  • 第一步:接到通知。 另一个线程执行 Desk.lock.notify()。此时,在等待池里睡觉的线程被拍醒了。
  • 第二步:重新排队。 被叫醒的线程会从"等待池"移步到"锁池(Entry Set)"。它现在有资格去抢钥匙了。
  • 第三步:抢锁。 它必须等当前拿着钥匙的那个线程(发通知的那个)把 synchronized 块跑完、把钥匙还回来。然后,它要和其他正在门口等着的线程一起竞争
  • 第四步:继续执行。只有当它再次抢到钥匙时 ,它才会回到当初 wait() 的那一行,继续往下走。

可以把wait看作让出锁的控制权

3.4阻塞队列基本使用【理解】

  • 阻塞队列继承结构

  • 常见BlockingQueue:

ArrayBlockingQueue: 底层是数组,有界

LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

  • BlockingQueue的核心方法:

put(anObject): 将参数放入队列,如果放不进去会阻塞

take(): 取出第一个数据,取不到会阻塞

  • 代码示例

    public class Demo02 {
    public static void main(String[] args) throws Exception {
    // 创建阻塞队列的对象,容量为 1
    ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);

    复制代码
          // 存储元素
          arrayBlockingQueue.put("汉堡包");
    
          // 取元素
          System.out.println(arrayBlockingQueue.take());
          System.out.println(arrayBlockingQueue.take()); // 取不到会阻塞
    
          System.out.println("程序结束了");
      }

    }

阻塞队列实现等待唤醒机制【理解】

  • 案例需求
    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

1.构造方法中接收一个阻塞队列对象

2.在run方法中循环向阻塞队列中添加包子

3.打印添加结果

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

1.构造方法中接收一个阻塞队列对象

2.在run方法中循环获取阻塞队列中的包子

3.打印获取结果

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

创建阻塞队列对象

创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象

分别开启两个线程

  • 代码实现

    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();
      }

    }

相关推荐
周末也要写八哥4 小时前
MATLAB R2025a超详细下载与安装教程(附安装包)
开发语言·matlab
希望永不加班4 小时前
SpringBoot 自动配置类加载顺序与优先级
java·spring boot·后端·spring·mybatis
纸鸢|5 小时前
从“一锤子买卖“到“持续价值共生“:物联网软件产品如何做到“叫好又叫座“
java·物联网·struts
blog_wanghao5 小时前
基于Qt的串口调试助手
开发语言·qt
qq_571099355 小时前
学习周报四十
学习
云霄IT6 小时前
安卓开发之java转dex再转smali
android·java·python
果汁华6 小时前
Typer:基于类型提示的现代Python CLI框架
开发语言·网络·python
用户6688599847666 小时前
BCrypt密码加密
java
赵药师6 小时前
多进程-生产者消费者C++实现
java·开发语言·jvm