news 2026/10/27 9:03:05

【Java】线程初阶 下

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Java】线程初阶 下

单线程案例


单例模式

  • 单例模式:保证一个类在整个程序中只有唯一的一个实例,
    典型场景:JDBC 的 DataSource 连接池

  • 核心分类:饿汉模式、懒汉模式


饿汉模式
  • 特点:类加载的时候就直接创建实例,天然线程安全
    缺点:类加载就占用内存,不用也会创建

  • class Singleton { // 类加载直接初始化实例 private static Singleton instance = new Singleton(); // 私有构造方法:禁止外部new private Singleton() {} // 对外获取实例的方法 public static Singleton getInstance() { return instance; } }


懒汉模式1

单线程版

  • 特点:类加载不创建实例,第一次调用 getInstance () 才创建(懒加载)

  • 注意:线程不安全,多线程同时调用会创建多个对象

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

懒汉模式2

加 synchronized,多线程基础版

  • 解决线程不安全:给方法加锁

  • 缺点:每次调用 getInstance 都要竞争锁,并发性能差,哪怕实例已经创建好了

  • class Singleton { private static Singleton instance = null; private Singleton() {} // 方法加锁 public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

懒汉模式3

双重检查锁 DCL + volatile 版

  • 优化两点:
    (1)双重 if 判断:外层 if 判断实例是否已经创建,实例创建完成后,
    后续线程不用抢锁,大幅提升性能
    (2)volatile 关键字:禁止instance = new Singleton()的指令重排,
    避免拿到半初始化的对象

  • class Singleton { // volatile 禁止指令重排 + 保证内存可见性 private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { // 外层判断:实例已经存在,直接返回,不加锁 if (instance == null) { synchronized (Singleton.class) { // 内层判断:防止多个线程通过外层if之后重复创建 if (instance == null) { instance = new Singleton(); } } } return instance; } }

原理通俗解释
  • 多个线程同时进来,发现 instance 是 null,去抢锁

  • 抢到锁的线程,内层再判断一次 null,创建实例

  • 其他抢到锁的线程,内层 if 发现实例不为 null,直接跳过创建

  • 实例创建完毕之后,所有后续线程直接被外层 if 拦住,不再竞争锁


volatile 在这里两个作用
  • 保证内存可见性:一个线程修改 instance,其他线程立刻读到最新值
  • 禁止指令重排序:
    new Singleton()分为三步:分配内存 → 初始化对象 → 引用指向内存;
    volatile 防止 JVM 打乱顺序,避免别的线程拿到还没初始化完成的 instance

对比




代码

饿汉模式
  • public class SingletonHungry { // 类加载的时候,就直接创建实例 private static SingletonHungry instance =new SingletonHungry(); //私有构造方法:不让外面new 对象 private SingletonHungry() { } //对外唯一获取实例的方法 public static SingletonHungry getInstance() { return instance; } public static void main(String[] args) { SingletonHungry s1=SingletonHungry.getInstance(); SingletonHungry s2=SingletonHungry.getInstance(); System.out.println(s1==s2); // true 同一个对象 } }
  • 特点:类加载直接创建,天生线程安全
  • 缺点:类加载就占用内存,就算后面不用这个实例也创建了

懒汉模式1
  • 单线程版,线程不安全

  • public class SingletonLazy { // 先不创建实例,初始为null private static SingletonLazy instance= null; // 私有构造,禁止外部new private SingletonLazy() { } // 第一次调用的时候,才创建实例 public static SingletonLazy getInstance(){ if(instance==null) { instance =new SingletonLazy(); } return instance; } public static void main(String[] args) { SingletonLazy s1=SingletonLazy.getInstance(); SingletonLazy s2=SingletonLazy.getInstance(); System.out.println(s1==s2); // 单线程下输出 true } }
  • 特点:懒加载,什么时候用什么时候创建

  • 问题:多线程环境下不安全,多个线程同时走到 if 判断,会创建多个不同对象


懒汉模式2
  • synchronized 方法加锁版
  • public class SingletonLazySync { private static SingletonLazySync instance =null; //私有构造方法 private SingletonLazySync() { } // 给静态方法加上 synchronized , 保证同一时刻只能有一个线程进入 public static SingletonLazySync getInstance() { if(instance==null) { instance=new SingletonLazySync(); } return instance; } public static void main(String[] args) { SingletonLazySync s1=SingletonLazySync.getInstance(); SingletonLazySync s2=SingletonLazySync.getInstance(); System.out.println(s1==s2); // true } }
  • 优点:解决了线程不安全问题,支持懒加载
  • 缺点:每次调用 getInstance () 都要竞争锁,哪怕实例早就创建好了,并发效率很低。

懒汉模式3
  • DCL 双重检查锁 + volatile(面试最常问版本)
  • public class SingletonDCL { // volatile:静止指令重排序+保证内存可见性 private static volatile SingletonDCL instance =new SingletonDCL(); //私有构造 private SingletonDCL() { } public static SingletonDCL getInstance(){ // 外层if :实例已经创建,直接返回,不用抢锁 if(instance==null) { synchronized(SingletonDCL.class){ // 内层if :防止多个线程通过外层if重复创建对象 if(instance==null) { instance=new SingletonDCL(); } } } return instance; } public static void main(String[] args) { SingletonDCL s1=SingletonDCL.getInstance(); SingletonDCL s2=SingletonDCL.getInstance(); System.out.println(s1==s2); // true } }
  • 优点:懒加载、线程安全;实例创建完成后不再加锁,并发性能好
  • 重点:volatile 不能省略!
  • instance = new SingletonDCL() 分为三步:
    (1)分配对象内存空间
    (2)初始化对象
    (3)instance 引用指向这块内存
  • JVM 可能发生指令重排,顺序变成 1 → 3 → 2。
    别的线程拿到还没初始化完成的对象,出现 bug。
    volatile 阻止这个重排


阻塞队列


阻塞队列是什么
  • 阻塞队列(BlockingQueue)是线程安全、遵循先进先出 FIFO的队列,
    有两个核心阻塞特性:
    (1)队列满时:继续入队put(),线程阻塞,直到其他线程取走元素、队列有空位
    (2)队列空时:继续出队take(),线程阻塞,直到其他线程放入元素

  • 典型场景:生产者消费者模型


生产者消费者模型
  • 核心作用:通过阻塞队列作为缓冲区,解耦生产者和消费者、削峰填谷

  • 削峰填谷(流量缓冲)
    生产者生产速度 > 消费者处理速度时,数据先放到队列缓存,
    避免下游被瞬时流量打垮(秒杀、消息异步处理场景)
  • 解耦
    生产者和消费者不需要互相感知,生产者只管往队列放数据,
    消费者只管从队列拿数据,双方互不依赖
  • 例子:擀饺子皮(生产者)、包饺子(消费者),中间的饺子皮盘子就是阻塞队列

Java 标准库阻塞队列
  • BlockingQueue是接口,常用实现类:LinkedBlockingQueue



代码

例1:

  • // 创建一个无界的 LinkedBlockingQueue BlockingQueue<String> queue=new LinkedBlockingQueue<>(); // put:阻塞式入队,如果队列满了,当前线程阻塞等待 queue.put("abc"); System.out.println("元素 abc 已经放入队列"); // take :阻塞式出队,如果队列为空,当前线程阻塞等待 String elem=queue.take(); System.out.println("取出的元素:"+elem);


    补充说明
    LinkedBlockingQueue<>() 无参构造默认是无界队列(容量 Integer.MAX_VALUE),
    所以这个例子里 put 不会阻塞

例2:put 阻塞例子

  • public static void main(String[] args) { // 容量为1,最多只能存1个元素 BlockingQueue<String> queue=new LinkedBlockingQueue<>(1); //消费者线程,3秒后取出元素,释放位置 new Thread(()->{ try{ Thread.sleep(3000); System.out.println("【消费者】准备 take 取出元素"); String val=queue.take(); System.out.println("【消费者】取出:"+val); }catch(InterruptedException e){ e.printStackTrace(); } }).start(); try { queue.put("abc"); System.out.println("放入第一个"); //此时队列已满,这一行会阻塞,直到消费者take之后才有空位 System.out.println("准备放入第二个元素123,队列已满,开始阻塞...."); queue.put("123"); System.out.println("放入第二个 123"); }catch(InterruptedException e){ e.printStackTrace(); } }

  • 逻辑
    (1)new LinkedBlockingQueue<>(1):有界阻塞队列,容量 = 1
    (2)第一个put("abc")成功,队列满了
    (3)执行put("123"):队列没有空余位置,主线程阻塞在这一行,不会往下走
    (4)3 秒后消费者线程执行take(),拿走abc,队列空出位置
    (5)阻塞的put("123")被唤醒,成功放入,打印最后一行

例3:队列为空时 take () 会阻塞等待

  • take 阻塞效果:单独执行 queue.take(),队列是空的,
    线程会卡在这里等待其他线程 put 数据】
  • 感受:队列为空时 take () 会阻塞等待
  • public static void main(String[] args) { // 创建阻塞队列 BlockingQueue<String> queue =new LinkedBlockingQueue<>(); // 开启消费者线程 new Thread(()->{ try{ System.out.println("消费者准备取元素"); // take() : 阻塞式出队 // 如果当前队列,里面没有元素,当前线程就阻塞在这里,直到别的线程put放入数据 String s=queue.take(); System.out.println("消费者拿到:"+s); }catch(InterruptedException e){ e.printStackTrace(); } }).start(); try{ // 主线程休眠2秒,延迟放入数据 // 这2秒内,消费者线程卡在take() 处阻塞等待 Thread.sleep(2000); System.out.println("主线程准备put数据"); // put(): 阻塞式入队,队列满才阻塞,当前是无界队列,put 不会阻塞 queue.put("abc"); }catch(InterruptedException e){ e.printStackTrace(); } }

  • 流程
    (1)程序启动,开启消费者线程,执行queue.take(),
    此时队列是空的,消费者线程阻塞在这里卡住
    (2)主线程睡眠 2s,这 2 秒内消费者一直在等待
    (3)2 秒后主线程执行put("abc"),往队列放入元素
    (4)take()被唤醒,取出元素,打印拿到的数据

例4:生产者消费者

  • public static void main(String[] args) throws InterruptedException { // 创建阻塞队列,无界队列 BlockingQueue<Integer> blockingQueue= new LinkedBlockingQueue<>(); // 消费者线程不断队列take 取元素 Thread customer=new Thread(()->{ while(true) { try { // take(): 队列为空就阻塞等待 int value=blockingQueue.take(); System.out.println("消费者元素:"+value); }catch(InterruptedException e) { e.printStackTrace(); } } },"消费者"); customer.start(); // 生产者线程:每隔一秒生产一个随机数放入队列 Thread producer=new Thread(()-> { Random random=new Random(); while(true) { try{ int num=random.nextInt(1000); System.out.println("生产元素:"+num); //put() : 队列满才阻塞,这里是无界队列,不会阻塞 blockingQueue.put(num); //生产完 休眠1秒,控制生产速度 Thread.sleep(1000); }catch(InterruptedException e) { e.printStackTrace(); } } },"消费者"); producer.start(); // main 线程等待消费者, 生产者线程执行完 customer.join(); producer.join(); }

  • 说明
    (1)生产者每 1s 生成一个 0~999 的随机数字,put 放进阻塞队列
    (2)消费者循环调用 take 获取元素;队列空的时候,消费者阻塞等待
    (3)join():让主线程阻塞,等待生产者、消费者线程结束
    (这个程序是死循环,所以主线程会一直卡在 join 这里)
    (4)无界 LinkedBlockingQueue,所以这里 put 不会阻塞;
    (5)如果改成 newLinkedBlockingQueue<>(2) 设置容量,
    生产速度大于消费速度时 put 就会阻塞

高频面试
  • wait 为什么要写在 while 里,if 不行吗?
    不行。notifyAll()唤醒全部等待线程,A 线程唤醒后还没拿到锁,B 线程抢先修改了队列状态;等 A 拿到锁的时候条件已经不成立,if 不会二次判断,直接往下执行,引发 bug。while 会循环校验条件。

  • 为什么用 notifyAll 而不是 notify?
    notify()只随机唤醒一个线程,可能唤醒的是同类线程(比如队列满了,生产者 wait,notify 唤醒另一个生产者,消费者没被唤醒,程序卡死);notifyAll()唤醒全部,能保证另一方线程被唤醒。
  • 阻塞队列的应用:消息队列、线程池、异步削峰、生产者消费者。

阻塞队列实现
  • 实现要点:
    (1)底层用循环数组存储元素,head队首、tail队尾、size记录元素个数
    (2)synchronized保证线程互斥
    (3)必须用 while 循环判断等待条件,不能用 if
    原因:notifyAll()会唤醒所有等待线程,线程被唤醒后,
    条件可能又不满足(虚假唤醒),while 会再次检查条件,不满足继续 wait

    (4)put:队列满 → wait;放入元素后notifyAll()唤醒等待线程
    (5)take:队列空 → wait;取出元素后notifyAll()唤醒等待线程

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 9:03:01

事件相机与E-VLA技术:突破机器人视觉极限

1. 项目概述&#xff1a;事件相机如何突破机器人视觉极限在机器人感知领域&#xff0c;传统视觉系统面临着一个根本性挑战&#xff1a;当环境光照条件恶化时&#xff0c;基于帧的摄像头会立即失效。这个问题在具身智能&#xff08;Embodied AI&#xff09;应用中尤为致命——想…

作者头像 李华
网站建设 2026/10/27 9:02:50

XCZU2EG双核AMP实战:从VITIS工程搭建到Cache一致性与IPI避坑

简介&#xff1a;本资源面向FPGA嵌入式开发工程师与高校研究者&#xff0c;聚焦XCZU系列MPSoC&#xff08;含XCZU2CG、XCZU2EG、XCZU4EV&#xff09;在VITIS平台下的双核AMP驱动实现&#xff0c;解决异构多核任务划分、核间通信与协同调度等工程问题&#xff0c;适合具备Zynq U…

作者头像 李华
网站建设 2026/10/27 9:02:00

YOLOv12在工业火焰烟雾检测中的优化实践

1. 项目概述&#xff1a;当YOLOv12遇上火焰烟雾检测 去年参与某工业园区安防系统升级时&#xff0c;我第一次将YOLOv12应用于火焰烟雾检测场景。传统监控系统平均需要17秒才能触发火警警报&#xff0c;而我们的原型系统将这个时间缩短到惊人的83毫秒。这个基于YOLOv12的检测系统…

作者头像 李华
网站建设 2026/10/27 9:01:18

企业算力规划新思路:从智能算力到推理负载与ROI测算

1. 云栖大会算力展区里&#xff0c;我看到的三个风向变化 今年云栖大会我的整体感受是&#xff1a; 算力的讨论重心&#xff0c;正从“资源规模”切换到“业务结果” 。会场里主论坛也好、分论坛也好&#xff0c;头部云厂商上台几乎不再像前两年那样喊“我们有多少万张GPU卡”…

作者头像 李华
网站建设 2026/10/27 9:00:53

2026年大模型技术学习路线与实战指南

1. 为什么你需要体系化的大模型学习路线&#xff1f;2023年被称为大模型元年&#xff0c;但到了2026年&#xff0c;这个领域已经发生了翻天覆地的变化。三年前那些零散的教程和碎片化知识&#xff0c;现在看起来就像是用DOS系统的操作指南来教人使用Windows 11。我见过太多学习…

作者头像 李华
网站建设 2026/10/27 9:00:43

SNPE 1.5安装配置与模型转换避坑指南

简介&#xff1a;这份高通SNPE安装包面向移动端与嵌入式AI推理部署开发者&#xff0c;提供snpe-1.5版本的完整工具链&#xff0c;可直接下载安装&#xff0c;用于在高通骁龙平台上完成神经网络模型的转换、量化、性能验证与集成调试&#xff0c;适合具备一定深度学习与C/Python…

作者头像 李华