完整教程:Java多线程初阶

news/2025/9/22 17:41:33/文章来源:https://www.cnblogs.com/wzzkaifa/p/19105702

文章目录

  • 线程与进程
      • 背景
      • 进程的局限
      • 典型示例
      • 线程引入
    • 底层概念
      • 进程与线程的描述
      • 包含关系
      • 基本单位
      • 举例
    • 线程比进程更轻量的原因
    • 线程能提高效率的原因
    • 线程冲突问题
    • 进程与线程的概念和区别
  • 在 Java 中编写多线程程序
    • 使用 `Thread` 标准库
    • 创建线程的写法
    • 匿名内部类实现线程
    • 基于lambda表达式来创建线程
    • Thread类,属性和方法
  • 前台线程和后台线程
    • **1. 前台线程(User Thread)**
    • **2. 后台线程(Daemon Thread)**
    • 3. 操作系统中的前台进程和后台进程
  • 线程的核心操作
    • 创建线程start()
      • 一个经典的面试题:start和run之间的差别
    • 线程的终止
  • Java线程终止与中断机制
    • 协作式中断机制
    • 线程中断的实现
      • 代码示例
      • 关键点说明
    • interrupt()方法的双重作用
    • 为什么阻塞方法会清除中断标志位?
    • 正确终止线程
    • 线程的等待
      • 线程的调度
      • 线程等待与阻塞
        • Thread.join()方法
        • Thread.sleep()方法
  • 线程状态
      • 进程状态
      • Java线程的状态
  • 线程安全
      • 1. 线程安全问题的根源
      • 2. 线程安全问题的原因
      • 3. 解决方案——加锁
      • 代码示例
      • 加锁原理
      • 为什么锁对象可以随便设置?
      • 4. 加锁与 join 的区别
      • 5. Java中的加锁语法
        • 1. synchronized代码块
        • 2. synchronized方法
        • 3. synchronized静态方法
      • 6. synchronized机制优势
      • 7. 适用场景与注意事项
  • 死锁
    • 一、死锁的定义
    • 二、死锁的常见场景
      • 1. 单线程重复加锁(可重入锁)
      • 2. 多线程多锁互相等待(经典死锁)
      • 3. N线程M锁(哲学家就餐问题)
    • 三、死锁的四个必要条件(重点)
    • 四、死锁的解决方法
      • 避免循环等待:**统一加锁顺序**
        • 代码示例(修改后无死锁):
    • 五、可重入锁(Reentrant Lock)原理
  • Java 集合类的线程安全性
  • 内存可见性
    • 一、什么是内存可见性问题?
    • 二、代码示例
      • 问题代码
    • 三、问题原因分析
      • 1. JVM/编译器优化
      • 2. 单线程与多线程区别
    • 四、如何解决内存可见性问题?
      • 1. 加入`Thread.sleep`
      • 2. 使用`volatile`关键字(推荐)
        • 原理
      • 3. 其他方法
    • 五、编译器优化的目的与权衡
  • wait / notify
    • 一、为什么需要 wait/notify?
    • 二、wait/notify 的基本原理
    • 三、使用方法和注意事项
      • 1. 必须在同步(synchronized)代码块中使用
      • 2. wait 的三大作用
      • 3. notify/notifyAll
    • 四、常见错误及其原因
      • 1. 未加锁调用 wait/notify
      • 2. wait 必须释放锁
      • 3. notify 只唤醒一个线程
      • 4. notify 没有线程等待
    • 五、典型代码示例
      • 1. 基本用法
      • 2. 多线程 wait,notify 唤醒一个
      • 3. notifyAll 唤醒全部
    • 六、wait 的超时版本
    • 七、原子性问题
  • 单例模式
    • 一、什么是单例模式?
    • 二、设计模式与框架
    • 三、单例模式实现方式
      • 1. 饿汉式(类加载时就创建实例)
      • 2. 懒汉式(第一次使用时才创建实例)
    • 四、线程安全问题分析
    • 五、线程安全优化方案
      • 1. 加锁(同步方法/同步代码块)
      • 2. 指令重排序问题
      • 3. 使用 volatile 关键字
    • 六、面试答题套路
  • 阻塞队列
    • 一、阻塞队列是什么?
    • 二、应用场景:生产者-消费者模型
      • 1. 生产者-消费者模型
      • 2. 分布式系统中的应用
      • 3. 消息队列
    • 三、标准库实现:BlockingQueue
      • 1. BlockingQueue 的主要方法
      • 2. 代码示例
    • 四、自定义阻塞队列实现
      • 1. 基本原理
      • 2. 代码示例
    • 五、实现细节与面试要点
      • 1. 循环队列的写法
      • 2. wait/notify 的使用细节
      • 3. InterruptedException
      • 4. volatile 的作用
    • 六、阻塞队列的优缺点
      • 优点
      • 缺点
    • 七、面试答题套路
  • 线程池
    • 一、为什么要用线程池?
    • 二、线程池的底层原理
      • 内核态与用户态
      • 线程创建过程
    • 三、Java标准库中的线程池
      • 1. ThreadPoolExecutor(核心类)
        • 参数说明
        • 拒绝策略(RejectedExecutionHandler)
      • 2. Executors 工厂类(简化版)
      • 3. 线程池用法示例
    • 四、线程池参数如何设置?
    • 五、工厂设计模式(ThreadFactory)
    • 六、自定义线程池实现
    • 七、面试答题套路
  • 定时器(Timer)
    • 一、标准库定时器用法
    • 二、定时器的实现原理
      • 1. 定时任务的描述
      • 2. 定时任务的管理
    • 三、数据结构选择:为什么不用 List?
    • 四、自定义定时器实现(代码示例)
      • 1. 定时任务类
      • 2. 定时器类
      • 3. 使用示例
    • 五、线程安全与性能优化
    • 六、定时器的局限与扩展
    • 七、面试答题套路

线程与进程

背景

虽然多进程能够解决一些问题,但在高效率的要求下,我们希望有更好的编程方式。

进程的局限

多进程的主要缺点是进程相对较重。创建和销毁进程的开销(包括时间和空间)都比较大。当需求场景需要频繁创建进程时,这种开销就会变得非常明显。

典型示例

最典型的场景是服务器开发。在这种情况下,针对每个发送请求的客户端,通常会创建一个单独的进程,由该进程负责为客户端提供服务。

线程引入

为了解决这个问题,发明了线程——一种更轻量级的进程。线程同样能够处理并发编程的问题,但其创建和销毁的开销要低于进程。因此,多线程编程逐渐成为当下主流的并发编程方式。

底层概念

进程与线程的描述

  • 进程是通过 PCB(进程控制块) 结构体来描述的,并以链表形式组织。
  • Linux 系统中,线程同样是通过 PCB 来描述。
  • 一个进程实际上是一组 PCB,而一个线程对应一个 PCB

包含关系

基本单位

  • 线程是系统“调度执行”的基本单位。
  • 进程是系统“资源分配”的基本单位。

举例

当一个可执行文件被点击时,操作系统会:

  1. 创建进程,并分配资源(如 CPU内存硬盘网络等)。
  2. 在该进程中创建一个或多个线程,这些线程随后会被调度到 CPU 上执行。
  3. 如果在一个进程中有多个线程,每个线程都会有自己的状态、优先级、上下文和记账信息,且每个线程都会各自独立地在 CPU 上调度执行。

线程比进程更轻量的原因

主要在于创建线程省去了“分配资源”过程,销毁线程也省去了“释放资源”过程。一旦创建进程,同时会创建第一个线程,该线程负责分配资源。后续创建第二个、第三个线程时,就不必重新分配资源了。

线程能提高效率的原因

开销较大的操作并不容易实现。能够提高效率的关键在于充分利用多核心进行“并行执行”。如果只是“微观并发”,速度没有提升;真正能提升速度的是“并行”。如果线程数目太多,超出了 CPU 核心数目,就无法在微观上完成所有线程的“并行”执行,势必会存在严重的“竞争”。

线程冲突问题

当线程数目多了之后,容易发生“冲突”。由于多个线程使用同一份资源(如内存资源),如果多个线程针对同一个变量进行读写操作(尤其是写操作),就容易发生冲突。

当一个进程中有多个线程时,一旦某个线程抛出异常,如果处理不当,可能导致整个进程崩溃,其他线程也会随之崩溃。

进程与线程的概念和区别

  1. 进程包含线程:一个进程里可以有一个或多个线程,不能没有线程。
  2. 资源分配单位:进程是系统资源分配的基本单位,线程是系统调度执行的基本单位。
  3. 资源共享:同一个进程里的线程之间共享同一份系统资源(内存、硬盘、网络带宽等),尤其是内存资源。
  4. 并发编程的主流方式:线程是实现并发编程的主流方式,通过多线程可以充分利用多核 CPU。
  5. 相互影响:多个线程之间可能会相互影响,线程安全问题,一个线程抛出异常,也可能会把其他线程一起带走。
  6. 进程的隔离性:多个进程之间一般不会相互影响,一个进程崩溃不会影响到其他进程。

在 Java 中编写多线程程序

线程是操作系统提供的概念,操作系统提供 API 供程序员调用。不同系统提供的 API 是不同的(Windows 和 Linux 的线程创建 API 差别很大)。Java(JVM)将这些系统 API 封装好了,我们只需关注 Java 提供的 API。

使用 Thread 标准库

class MyThread
extends Thread {
@Override
public void run() {
// 即将创建出的线程要执行的逻辑
System.out.println("hello thread");
}
}
public class Demo1
{
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}

注意:上述代码中,run 方法并没有手动调用,但最终也执行了。这种方法称为“回调函数”(callback)。

该代码运行时是一个进程,但这个进程包含了两个线程:调用 main 方法的线程(主线程)和新创建的线程。

创建线程的写法

  1. 继承 Thread,重写 run
class MyThread
extends Thread {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1
{
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

多个线程之间的调度执行顺序是不确定的,取决于操作系统的调度器实现。我们只能将这个过程近似视为“随机”的“抢占式执行”。

  1. 实现 Runnable 接口,重写 run
class MyRunnable
implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello thread!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo2
{
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

通过 Runnable 描述任务,而不是通过 Thread 自己来描述,有助于解耦合。后续执行这个任务的载体,可以是线程,也可以是其他(如线程池、虚拟线程)。

匿名内部类实现线程

public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(){
@Override
public void run(){
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
  1. 定义匿名内部类,该类是 Thread 的子类。
  2. 在类内部重写父类 run 方法。
  3. 创建子类实例并将其引用赋值给 t
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
  1. 定义匿名内部类,该类实现了 Runnable 接口。
  2. 在类内部重写 Runnable 接口的 run 方法。
  3. 创建匿名内部类的实例,并将其引用传递给 Thread

基于lambda表达式来创建线程

public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->
{
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
  • 没有显式写 Runnable 是因为编译器通过 Thread(Runnable) 的参数类型自动推断。
  • 没有写 run() 是因为 Lambda 只针对函数式接口的唯一方法,方法名可省略。
  • Lambda 不是内部类,但效果等价,且更高效(不生成额外类文件)。

简单来说:Lambda 是 Runnable.run() 的“语法糖”,编译器帮你补全了细节

Thread类,属性和方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group, Runnable target)线程可以被用来分组管理,分好的组即为线程组(目前了解即可)
属性获取方法说明
IDgetId()线程的唯一标识,不同线程不会重复。
名称getName()用于调试工具,可自定义线程名(如 Thread-0)。
状态getState()表示线程当前状态(如 RUNNABLEWAITING),后续详细说明。
优先级getPriority()优先级高的线程理论上更容易被调度(范围:1~10,默认5)。
是否后台线程isDaemon()JVM 在所有非后台线程(用户线程)结束后才会退出。默认是 false(非后台)。
是否存活isAlive()run() 方法是否正在执行(未结束返回 true)。
是否被中断isInterrupted()线程的中断状态,需手动处理中断逻辑,后续详细说明。

ID是jvm自动分配的

不能手动设置通常情况下,一个 Thread 对象对应系统内部的一个线程(PCB),但也可能存在 Thread 对象存在而系统内部线程已经销毁或尚未创建的情况。

isAlive()

代码中,创建的new Thread 对象,生命周期,和内核中实际的线程,是不一定一样的.

可能会出现,Thread对象仍然存在,但内核中的线程不存在了这样的情况.

  1. 调用start之前,内核中,还没创建线程
  2. 线程的run执行完毕了,内核的线程就无了,但是Thread对象,仍然存在

前台线程和后台线程

1. 前台线程(User Thread)

  • 特点

    • JVM会等待所有前台线程执行完毕才会退出。
    • 默认创建的线程都是前台线程(包括main主线程)。
  • 示例

    Thread userThread = new Thread(() ->
    {
    System.out.println("前台线程运行中");
    });
    userThread.start();
    // 默认是前台线程

2. 后台线程(Daemon Thread)

  • 特点

    • JVM不会等待后台线程结束,只要所有前台线程终止,JVM会立即退出,后台线程会被强制终止。
    • 需要通过setDaemon(true)显式设置为后台线程(必须在start()前调用)。
    • 常用于辅助任务(如垃圾回收、心跳检测等)。
    • 前台进程要结束,无法阻止
    • 后台进程结束,不影响前台进程
  • 示例

    Thread daemonThread = new Thread(() ->
    {
    while (true) {
    System.out.println("后台线程持续运行");
    }
    });
    daemonThread.setDaemon(true);
    // 设置为后台线程
    daemonThread.start();

3. 操作系统中的前台进程和后台进程

  • 前台进程 (Foreground Process)
    • 直接与用户交互(占用终端输入/输出),会阻塞Shell直到进程结束。
    • 例如:在终端直接运行 vim file.txt,此时Shell被阻塞,无法输入其他命令。
  • 后台进程 (Background Process)
    • 不占用终端,在后台运行(通过 &bg 命令启动)。
    • 例如:python script.py &,进程运行时用户仍可操作Shell。

线程的核心操作

创建线程start()

  1. 初始状态(NEW)
    • 线程对象创建后(未调用 start())处于 NEW 状态,此时允许调用 start()
  2. 禁止重复启动
    • 一旦调用 start(),线程状态从 NEW 转为其他状态(如 RUNNABLE),再次调用 start() 将抛出 IllegalThreadStateException

一个经典的面试题:start和run之间的差别

start调用系统函数,真正再系统内核中,创建线程.

此处start,会根据不同的系统,分别调用不同的api(windows,linux,mac…)

创建好新的线程,再来单独执行run

run描述线程要执行的任务,也可以称为线程的入口

一个Thread对象,只能调用一次start.

如果多次调用start就会出现问题.

(一个Thread对象,只能对应系统中的一个线程)

线程的终止

当然可以!下面是对你关于Java线程终止与中断机制的整理与归纳,结构清晰,便于理解和复习:


Java线程终止与中断机制

协作式中断机制

  • Java采用协作式中断(Cooperative Interruption),即线程终止由线程自身决定,而不是被外部强制终止。
  • 其他线程只能通过调用interrupt()方法请求目标线程“自愿”终止。

线程中断的实现

代码示例

public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->
{
Thread currentThread=Thread.currentThread();
while (!currentThread.isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
  1. 由于判定isInterrupted()和执行打印这两个操作执行速度很快,整个循环的主要时间都消耗在sleep(1000)上。
  2. 当main线程调用interrupt()时,目标线程t大概率正处于sleep状态。
  3. interrupt()操作不仅能设置中断标志位,还能唤醒正在sleep的线程。例如:
    • 如果线程刚sleep了100ms,还剩900ms
    • 此时调用interrupt()
    • sleep会立即被唤醒,并抛出InterruptedException异常
  4. 由于catch块中的默认代码会再次抛出异常,而这次抛出的异常没有被捕获,最终会传递到JVM层,导致进程异常终止。

所以需要把抛出异常改掉

public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->
{
Thread currentThread = Thread.currentThread();
while (!currentThread.isInterrupted()) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("执行到catch操作");
break;
// 或 return,确保线程能终止
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}

关键点说明

  • 循环条件为!currentThread.isInterrupted(),即只要未被中断就继续运行
  • Thread.sleep(1000)是阻塞方法,主线程3秒后调用t.interrupt()
    • 立即唤醒t线程,并抛出InterruptedException异常。
    • sleep等阻塞方法被中断唤醒时,会清除中断标志位
  • 如果在catch块里不做break/return处理,循环会继续,线程不会退出。

interrupt()方法的双重作用

作用说明
设置中断标志位通过isInterrupted()可检测
唤醒阻塞线程sleep()wait()join()等,直接抛出InterruptedException

为什么阻塞方法会清除中断标志位?

正确终止线程

  • 不要在catch块直接抛出异常,否则会导致线程异常终止。
  • 推荐在catch块中使用breakreturn,主动退出循环,使线程正常结束。

线程的等待

线程的调度

  • 操作系统对多个线程采用

    随机调度

    抢占式执行

    • 随机调度:线程的执行顺序由操作系统决定,具有不确定性。
    • 抢占式执行:操作系统可随时暂停某个线程,把CPU分配给其他线程,实现多线程“并发”效果。

线程等待与阻塞

Thread.join()方法
方法说明
public void join()等待目标线程结束,阻塞当前线程
public void join(long millis)最多等待 millis 毫秒,超时解除
public void join(long millis, int nanos)最多等待指定毫秒+纳秒,超时解除
  • join()无参数时,当前线程会一直等待目标线程结束,如果目标线程迟迟不结束,当前线程会一直阻塞。
Thread.sleep()方法

线程状态

进程状态

就绪:在cpu上执行,或者随时可以去cpu上执行

阻塞:暂时不能参与cpu执行

Java线程的状态

状态触发条件典型场景
NEW线程被创建但未启动 (start() 未调用)Thread t = new Thread();
RUNNABLE线程可运行(包括正在运行或就绪等待CPU调度)执行中、或等待系统资源(如CPU时间片)
BLOCKED线程等待获取监视器锁(synchronized 阻塞)竞争同步锁时被阻塞
WAITING无限期等待其他线程唤醒(不设超时)object.wait()thread.join()
TIMED_WAITING有限时间等待(设定了超时)Thread.sleep(ms)object.wait(timeout)thread.join(timeout)
TERMINATED线程执行完毕 (run() 方法结束)线程任务完成或异常终止

线程安全

1. 线程安全问题的根源

private static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->
{
for (int i=0;i<
50000;i++){
count++;
}
});
Thread t2=new Thread(()->
{
for (int i=0;i<
50000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
  • 多线程并发执行时,若同时修改同一个变量,且修改操作非原子性,就容易产生线程安全问题。

  • count++为例,实际由三步组成:

    1. load:从内存读到CPU寄存器
    2. add:寄存器值+1
    3. save:写回内存

    cpu调度执行线程的时候,不知道什么时候就会把线程调度走(抢占执行,随机调度)

    指令是cpu执行的最基本单位,至少要把当前执行完,不会执行一半调度走

    但是count++是三个指令,会出现执行几个后调度走的情况

    基于上面的情况,两个线程同时对线程进行++,就容易出现bug

  • 操作系统采用随机调度、抢占式执行,导致多个线程的指令可能交错执行,出现数据错乱。

2. 线程安全问题的原因

  1. 随机调度,抢占式执行(根本原因)
  2. 多线程同时修改同一个变量
  3. 修改操作不是原子的
  4. 内存可见性问题
  5. 指令重排序问题

3. 解决方案——加锁

代码示例

private static int count = 0;
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->
{
for (int i = 0; i <
50000; i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(() ->
{
for (int i = 0; i <
50000; i++) {
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count=" + count);
}

加锁原理

  • 锁对象 locker:多个线程针对同一个对象加锁时,才会产生互斥效果,保证同步。
  • 若锁对象不同,则各自加锁互不干扰,仍是并发执行。
  • 锁的本质是将并发执行变成串行执行,从而避免数据竞争。

为什么锁对象可以随便设置?

4. 加锁与 join 的区别

操作作用
加锁只让一小段代码串行执行,其他部分仍可并发
join让整个线程等待另一个线程结束,阻塞当前线程

5. Java中的加锁语法

1. synchronized代码块
synchronized (locker) {
// 受保护的代码
}
2. synchronized方法
synchronized public void add() {
count++;
}
  • 等价于:
public void add() {
synchronized (this) {
count++;
}
}
  • 锁对象是当前实例对象this
3. synchronized静态方法
synchronized public static void func() {
// 静态方法
}
  • 锁对象是类对象Counter.class,与实例无关。

等价于:

public static void func() {
synchronized (Counter.class)
{
// 代码
}
}

6. synchronized机制优势

  • 操作系统原生API / C++ / Python:加锁和解锁是分开的函数调用(如 lock()unlock())。
    • 原生做法(分开加锁/解锁)的最大问题是 unlock() 可能执行不到(比如因异常或代码逻辑遗漏导致未解锁),从而引发死锁等问题。
  • Java:通过 synchronized 关键字同时完成加锁和解锁,这种方式相对少见。
    • 进入代码块时自动加锁退出代码块时自动解锁
    • 无论是通过 return 正常返回,还是因抛出异常退出,都能确保锁被释放。
    • 有效避免了手动调用 unlock() 可能遗漏的问题,防止死锁风险。

7. 适用场景与注意事项

  • 不是所有场景都需要加锁,无脑加锁会影响性能。
  • 只有在多线程共享数据且存在竞争时才需要加锁。
  • StringBuffer、Vector、Hashtable等类因“无脑加锁”不推荐使用,JDK后续可能移除。

死锁

一、死锁的定义

死锁指的是两个或多个线程在执行过程中,因争夺资源而造成一种互相等待的现象,导致所有线程都无法继续运行。


二、死锁的常见场景

1. 单线程重复加锁(可重入锁)

void func(){
synchronized(this){
synchronized(this){
// ...
}
}
}
  • 在Java中,上述代码不会死锁,因为synchronized实现了可重入锁(Reentrant Lock)。
  • 原理:同一线程多次获得同一把锁时,只要是自己持有的锁,可以直接进入临界区,不会阻塞。内部通过“计数器”实现,只有最外层释放时才真正释放锁。
  • 对比:C++/Python的原生锁不是可重入锁,上述代码会死锁。

2. 多线程多锁互相等待(经典死锁)

private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() ->
{
synchronized (locker1) {
System.out.println("t1 加锁 locker1 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (locker2) {
System.out.println("t1 加锁 locker2 完成");
}
}
});
Thread t2 = new Thread(() ->
{
synchronized (locker2) {
System.out.println("t2 加锁 locker2 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (locker1) {
System.out.println("t2 加锁 locker1 完成");
}
}
});
t1.start();
t2.start();
}
  • t1先锁locker1,t2先锁locker2,然后分别请求对方持有的锁,相互等待,形成死锁

3. N线程M锁(哲学家就餐问题)

  • 多个线程(哲学家)同时竞争多个锁(筷子),如果每个人都先拿左边再拿右边,极易形成循环等待,导致死锁。

三、死锁的四个必要条件(重点)

  1. 互斥条件:资源(锁)是互斥的,每次只能被一个线程占用。
  2. 不可抢占条件:资源一旦被线程占有,其他线程不能强行夺取,只能等待持有线程释放。
  3. 请求与保持条件:线程已经持有至少一个资源,在等待获取其他资源的同时不释放已持有的资源。
  4. 循环等待条件:存在一个线程—资源的循环等待链。

四个条件同时满足时,才会出现死锁。只要破坏其中任意一个条件,就可以避免死锁。


四、死锁的解决方法

避免循环等待:统一加锁顺序

一个简单有效的方法:给锁编号,1, 2, 3…N。
约定所有的线程在加锁的时候,都必须按照一定的顺序来加锁。
(比如,必须先针对编号小的锁加锁,后针对编号大的锁加锁)

代码示例(修改后无死锁):
public static void main(String[] args) {
Thread t1 = new Thread(() ->
{
synchronized (locker1) {
System.out.println("t1 加锁 locker1 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (locker2) {
System.out.println("t1 加锁 locker2 完成");
}
}
});
Thread t2 = new Thread(() ->
{
// t2也先锁locker1,再锁locker2
synchronized (locker1) {
System.out.println("t2 加锁 locker1 完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (locker2) {
System.out.println("t2 加锁 locker2 完成");
}
}
});
t1.start();
t2.start();
}
  • 两个线程加锁顺序一致,不会形成循环等待,不会死锁

  • 破坏请求与保持条件:每次只能申请一把锁,用完立即释放。

  • 破坏不可抢占条件:如果获取不到新锁,主动释放已持有的锁,过一段时间重试(如tryLock机制)。

  • 死锁检测与恢复:如银行家算法(实际开发中较少用,主要用于操作系统等底层场景)。


五、可重入锁(Reentrant Lock)原理

Java 为了减少程序员写出死锁的概率,引入了特殊机制,解决上述的死锁问题。“可重入锁”

对于可重入锁来说,发现加锁的线程就是当前锁的持有线程,并不会真正进行任何加锁操作,也不会进行任何的“阻塞操作”,而是直接放行,往下执行代码。

假设这里的加锁有嵌套7、8层,如何知道当前释放锁的操作是最外层需要真正释放的呢?
(类比OJ题中判定括号是否匹配→使用栈)
字符串包含多种括号。

针对该问题,可引入计数器:

  • 初始计数器为0
  • 每遇到{(加锁),计数器+1
  • 每遇到}(释放锁),计数器-1
  • 若某次-1后计数器为0,则此次释放为最外层,需真正释放锁。

引用计数

加锁时,需要判断当前锁是否已被占用。
可重入锁的实现方式是在锁中额外记录当前是哪个线程对其进行了加锁。

Java 集合类的线程安全性

线程安全的集合类 (内置了 synchronized 同步机制):

线程不安全的集合类

注意

内存可见性

一、什么是内存可见性问题?

内存可见性问题指的是在多线程环境下,一个线程对共享变量的修改,另一个线程无法及时“看到”最新的值,导致程序出现逻辑错误。


二、代码示例

问题代码

private static int n = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() ->
{
while (n == 0) {
// 循环等待
}
System.out.println("t1 线程结束循环");
});
Thread t2 = new Thread(() ->
{
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数: ");
n = scanner.nextInt();
});
t1.start();
t2.start();
}
  • 用户输入1后,t1线程可能依然在循环,无法跳出。

三、问题原因分析

1. JVM/编译器优化

2. 单线程与多线程区别

  • 单线程下,这种优化不会影响程序逻辑。
  • 多线程下,编译器/JVM的优化可能导致数据不同步,出现bug。

四、如何解决内存可见性问题?

1. 加入Thread.sleep

while (n == 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
  • 加入sleep后,循环开销变大,JVM不再做缓存优化,每次都会重新读取内存,问题得到缓解。

2. 使用volatile关键字(推荐)

private static volatile int n = 0;
  • volatile告诉编译器和JVM:这个变量是易变的,禁止优化缓存,必须每次都从内存读取最新值
  • 保证了多线程之间的内存可见性
原理
  • 编译器会在读/写volatile变量时,插入内存屏障指令(Memory Barrier),确保数据同步。
  • 保证所有线程都能看到最新的值。

3. 其他方法


五、编译器优化的目的与权衡

  • 编译器/JVM优化是为了提升代码运行效率,但在多线程场景下可能带来数据不同步的问题。
  • 通过volatile等关键字,程序员可以主动干预优化行为,确保多线程正确性

wait / notify


一、为什么需要 wait/notify?

  • 多线程环境下,线程的调度是随机的,有时需要控制线程之间执行某些逻辑的顺序
  • 通过 waitnotify,可以让线程协调配合,实现“条件满足才执行”、“先后顺序控制”等功能。
  • 还可以解决线程饿死等问题。

二、wait/notify 的基本原理


三、使用方法和注意事项

1. 必须在同步(synchronized)代码块中使用

synchronized (obj) {
obj.wait();
// 释放锁并等待
// ...被唤醒后继续执行
}
synchronized (obj) {
obj.notify();
// 唤醒等待在 obj 上的一个线程
}

2. wait 的三大作用

  1. 释放锁
  2. 进入阻塞等待,准备接受通知
  3. 收到通知后,唤醒并重新竞争锁

3. notify/notifyAll


四、常见错误及其原因

1. 未加锁调用 wait/notify

Object obj = new Object();
obj.wait();
// 抛出 IllegalMonitorStateException

原因:没有获得锁,不能 wait/notify。

2. wait 必须释放锁

  • wait 的本质是“释放锁并等待”。如果只是 sleep,则不会释放锁,其他线程无法获得锁。

3. notify 只唤醒一个线程

4. notify 没有线程等待

  • 如果没有线程在 wait,notify 调用不会有任何副作用。

五、典型代码示例

1. 基本用法

public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(() ->
{
synchronized (locker) {
System.out.println("t1 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() ->
{
Scanner scanner = new Scanner(System.in);
scanner.next();
synchronized (locker) {
locker.notify();
}
System.out.println("t2 notify 之后");
});
t1.start();
t2.start();
}

2. 多线程 wait,notify 唤醒一个

Thread t1 = ... // wait
Thread t2 = ... // wait
Thread t3 = ... // wait
Thread t4 = new Thread(() ->
{
Scanner scanner = new Scanner(System.in);
scanner.next();
synchronized (locker) {
locker.notify();
// 随机唤醒一个
}
});

3. notifyAll 唤醒全部

synchronized (locker) {
locker.notifyAll();
// 唤醒所有等待线程
}

六、wait 的超时版本

  • wait(long timeout):等待指定时间后自动唤醒,超时未被 notify 也会继续执行。

七、原子性问题

以下是关于多线程环境下单例模式实现的系统整理,涵盖设计思路、线程安全问题、优化方案及面试答题套路,条理清晰,便于学习和复习。

单例模式

一、什么是单例模式?


二、设计模式与框架

  • 设计模式:软性约束,是代码编写的“套路”,让代码更稳健、可维护,减少 bug。
  • 框架:硬性约束,大部分逻辑已实现,开发者只需填充自定义部分。

三、单例模式实现方式

1. 饿汉式(类加载时就创建实例)

class Singleton
{
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
}
  • 优点:简单,线程安全。
  • 缺点:类加载时就创建实例,浪费资源(如果实例很大,但实际用不到)。

2. 懒汉式(第一次使用时才创建实例)

class SingletonLazy
{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
  • 优点:只有用到时才创建实例,节省资源。
  • 缺点线程不安全,多线程下可能创建多个实例。

四、线程安全问题分析

  • 多线程环境下,多个线程同时执行 getInstance(),可能导致多次创建实例,违背单例原则。

五、线程安全优化方案

1. 加锁(同步方法/同步代码块)

class SingletonLazy
{
private static SingletonLazy instance = null;
private static final Object locker = new Object();
public static SingletonLazy getInstance() {
if (instance == null) {
// 外层判断:是否需要加锁
synchronized (locker) {
if (instance == null) {
// 内层判断:是否需要创建实例
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
  • 双重检查锁定(Double-Checked Locking):
    • 外层 if:避免每次都加锁,提高性能。
    • 内层 if:防止多个线程同时进入创建实例。

2. 指令重排序问题

  • Java 对对象实例化的底层步骤:

    1. 分配内存空间
    2. 执行构造方法
    3. 将对象引用赋值给变量
  • 编译器可能重排序为 1-3-2,导致另一个线程拿到未初始化完成的对象。


3. 使用 volatile 关键字

class SingletonLazy
{
private static volatile SingletonLazy instance = null;
private static final Object locker = new Object();
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
  • volatile 的作用:
    1. 保证内存可见性
    2. 禁止指令重排序(确保对象初始化顺序正确)

六、面试答题套路

  1. 先写最简单的懒汉式单例(不考虑线程安全)。
  2. 面试官追问:线程安全吗?
    • 回答:不安全,多线程下可能创建多个实例。
  3. 加锁(同步方法或同步代码块)。
  4. 面试官追问:性能如何?
    • 回答:加锁后性能下降(每次都加锁)。
  5. 优化为双重检查锁定(减少不必要的加锁)。
  6. 面试官追问:还有问题吗?
    • 回答:可能存在指令重排序问题。
  7. 加上 volatile,彻底解决多线程下的单例模式。

阻塞队列

一、阻塞队列是什么?


二、应用场景:生产者-消费者模型

1. 生产者-消费者模型

2. 分布式系统中的应用

  • 多台服务器之间解耦合:A服务器只和队列通信,B服务器也只和队列通信,彼此不知道对方存在。
  • 削峰填谷:应对流量激增,保护下游服务器不被冲垮。

3. 消息队列


三、标准库实现:BlockingQueue

1. BlockingQueue 的主要方法

2. 代码示例

public static void main(String[] args) {
BlockingQueue<
Integer> queue = new ArrayBlockingQueue<
>(1000);
// 生产者线程
Thread producer = new Thread(() ->
{
int i = 1;
while (true) {
try {
queue.put(i);
System.out.println("生产元素 " + i);
i++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 消费者线程
Thread consumer = new Thread(() ->
{
while (true) {
try {
Integer item = queue.take();
System.out.println("消费元素 " + item);
Thread.sleep(1000);
// 模拟消费速度
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
consumer.start();
}

四、自定义阻塞队列实现

1. 基本原理

2. 代码示例

class MyBlockingQueue
{
private String[] data;
private volatile int head = 0;
private volatile int tail = 0;
private volatile int size = 0;
public MyBlockingQueue(int capacity) {
data = new String[capacity];
}
public void put(String s) throws InterruptedException {
synchronized (this) {
while (size == data.length) {
// 队列满,阻塞
this.wait();
}
data[tail] = s;
tail++;
if (tail >= data.length) {
tail = 0;
}
size++;
this.notify();
}
}
public String take() throws InterruptedException {
String ret;
synchronized (this) {
while (size == 0) {
// 队列空,阻塞
this.wait();
}
ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
this.notify();
}
return ret;
}
}

五、实现细节与面试要点

1. 循环队列的写法

2. wait/notify 的使用细节

  • 建议用 while 而不是 if,防止虚假唤醒(Spurious Wakeup)。
  • wait 可能因 notify、notifyAll、interrupt 或虚假唤醒被唤醒。

3. InterruptedException

4. volatile 的作用

  • 队列指针和计数器加 volatile,保证多线程下的可见性。

六、阻塞队列的优缺点

优点

  1. 解耦合:生产者和消费者逻辑分离,代码更易维护和扩展。
  2. 削峰填谷:应对流量突发,保护下游服务。

缺点

  1. 增加服务器部署和硬件成本
  2. 增加通信延迟,不适合对响应时间要求极高的场景。

七、面试答题套路

  1. 先说 BlockingQueue 的标准用法(put/take)。
  2. 能说出生产者消费者模型的应用场景(解耦合、削峰填谷)。
  3. 能自己手写一个阻塞队列(synchronized + wait/notify)。
  4. 能说出 while 防虚假唤醒,volatile 保证可见性,异常处理细节。
  5. 能分析优缺点,适用场景。

以下是关于线程池的系统整理,涵盖原理、标准库用法、参数详解、工厂设计模式、拒绝策略、优化建议、自定义实现、面试答题套路等,条理清晰,便于学习和复习。

线程池

一、为什么要用线程池?

  • 线程池本质上是提前创建好一批线程,后续需要执行任务时直接复用这些线程,而不是频繁创建/销毁线程。
  • 优势
    • 避免频繁创建/销毁线程的系统开销。
    • 统一管理线程资源,提升系统性能和稳定性。
    • 控制最大并发线程数,防止系统资源耗尽。
    • 支持任务排队,便于实现生产者-消费者模型。

二、线程池的底层原理

  • 线程池维护一组工作线程和一个任务队列。
  • 用户提交任务(Runnable/Callable),线程池从队列取任务分配给空闲线程执行。
  • 用完的线程不会销毁,而是继续等待下一个任务。

效率对比:

  • 从线程池取线程是用户态操作,速度快、可控。
  • 直接创建线程需要内核参与,开销大,效率低。

为什么从线程池里取线程,比从系统申请,来的更高效?

内核态与用户态

操作系统由内核和配套的应用程序组成,内核负责管理和服务所有应用程序的请求。

线程创建过程

从系统申请线程

  • 通过系统API创建新线程时,涉及到内核态的操作。
  • 系统内核需要执行一系列复杂的逻辑来分配资源、设置线程状态等。
  • 这个过程可能会引起上下文切换,增加了系统负担和延迟。

从线程池获取线程

  • 从线程池中获取线程的过程完全在用户态中完成。
  • 线程池预先创建了一定数量的线程,避免了频繁的线程创建和销毁。
  • 整个过程是可控的,不涉及内核的复杂逻辑,减少了上下文切换。

效率对比

  • 控制性:使用线程池,开发者可以更好地控制线程的生命周期和资源分配。
  • 性能:纯用户态操作通常比内核态操作效率更高,因为用户态不需要频繁切换到内核态,减少了系统调用的开销。
  • 资源管理:线程池管理线程的创建和复用,降低了系统资源的消耗,提升了应用程序的响应速度。

三、Java标准库中的线程池

1. ThreadPoolExecutor(核心类)

构造方法参数详解(经典面试题):

ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程最大空闲时间
TimeUnit unit, // 时间单位
BlockingQueue<
Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数说明
  • corePoolSize:核心线程数,线程池始终保有的最少线程数。
  • maximumPoolSize:最大线程数,线程池允许的最大线程数量(包括核心+非核心)。
  • keepAliveTime & unit:非核心线程空闲多久后被销毁。
  • workQueue:任务队列,通常是阻塞队列(如 ArrayBlockingQueue),决定任务排队方式和容量。
  • threadFactory:线程工厂,用于定制线程属性(如名字、优先级等),一般用默认即可。
  • handler:拒绝策略,任务无法处理时的应对措施(标准库提供4种)。
拒绝策略(RejectedExecutionHandler)
  1. AbortPolicy:直接抛出异常(默认)。
  2. CallerRunsPolicy:由提交任务的线程自己执行任务。
  3. DiscardPolicy:直接丢弃任务,不抛异常。
  4. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试提交新任务。

2. Executors 工厂类(简化版)

  • newFixedThreadPool(n):固定大小线程池。
  • newCachedThreadPool():可扩容线程池,线程数无限制。
  • newSingleThreadExecutor():单线程池,任务串行执行。
  • newScheduledThreadPool(n):定时/周期任务线程池。
  • newWorkStealingPool():工作窃取线程池。

3. 线程池用法示例

public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
for (int i = 0; i <
100; i++) {
int id = i;
service.submit(() ->
{
System.out.println("hello thread " + id + ", " + Thread.currentThread().getName());
});
}
Thread.sleep(2000);
// 等待任务执行完
service.shutdown();
// 关闭线程池
System.out.println("程序退出");
}
  • 线程池默认创建的是前台线程,主线程结束后线程池仍在运行。
  • 使用 shutdown() 正确关闭线程池,防止资源泄漏。

四、线程池参数如何设置?

  • 线程数多少合适?
    • 受限于主机CPU核心数、内存、其他资源。
    • 任务类型不同,最佳线程数不同:
      • 纯计算型任务:线程数 ≈ CPU核心数。
      • IO密集型任务:线程数可略多于CPU核心数(因为线程会主动让出CPU)。
    • 推荐通过实际测试、性能监控,选取最优线程数。

五、工厂设计模式(ThreadFactory)

  • 工厂设计模式用于解决构造方法不灵活的问题。

    构造方法,特殊方法. 必须和类名一样

    多个版本的构造方法,必须是通过“重载”(overload)

    class Point
    {
    public Point(double x, double y) {
    ....
    }
    public Point(double r, double a) {
    ....
    }
    }

    这两方法没办法构成重载

    使用构造方法创建实例,就会存在上述局限性

    为了解决上述问题,引入了"工厂设计模式"

    通过"普通方法"(通常是静态方法)完成对象构造和初始化的操作

    class Point
    {
    public static Point makePointByXY(double x, double y) {
    Point p;
    p.setX(x);
    p.setY(y);
    return p;
    }
    public static Point makePointByRA(double r, double a) {
    Point p;
    p.setR(r);
    p.setA(a);
    return p;
    }
    }
  • 通过静态方法/工厂类创建实例,便于定制初始化逻辑。

  • 线程池中的 ThreadFactory 用于批量定制线程属性。


六、自定义线程池实现

class MyThreadPool
{
private BlockingQueue<
Runnable> queue = new ArrayBlockingQueue<
>(1000);
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() ->
{
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public class Demo31
{
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(4);
for (int i = 0; i <
1000; i++) {
int id = i;
pool.submit(() ->
{
System.out.println("执行任务 " + id + ", " + Thread.currentThread().getName());
});
}
}
}
  • 仅实现了任务提交和执行,未实现线程池销毁(可通过 interrupt 等方式扩展)。

七、面试答题套路

  1. 为什么要用线程池?(性能、资源控制、任务排队)
  2. ThreadPoolExecutor参数含义及作用。
  3. 拒绝策略有哪些?各自适用场景。
  4. 如何设置线程池线程数?(计算型 vs IO型任务)
  5. Executors与ThreadPoolExecutor的区别,为什么推荐后者?
  6. 能否手写一个简单线程池?
  7. shutdown/资源释放细节。
  8. 工厂设计模式在线程池中的作用。

以下是关于定时器(Timer)实现原理与优化的系统整理,涵盖标准库用法、定时器任务管理的数据结构选择、线程安全与性能优化、业界经典实现、面试答题套路等,便于学习和复习。


定时器(Timer)

一、标准库定时器用法

public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
// 延迟3秒执行
System.out.println("程序开始运行");
}
  • 可以安排多个任务,任务类继承自 TimerTask
  • schedule 方法的参数是延迟时间(delay)。

二、定时器的实现原理

1. 定时任务的描述

2. 定时任务的管理

  • 需要一个数据结构存储所有待执行任务。
  • 任务调度线程负责不断检查任务列表,将到时的任务执行掉。

三、数据结构选择:为什么不用 List?

问题:

解决方案:


四、自定义定时器实现(代码示例)

1. 定时任务类

class MyTimerTask
implements Comparable<
MyTimerTask> {
private Runnable runnable;
private long time;
// 绝对时间戳
public MyTimerTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public void run() { runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTimerTask o) {
return Long.compare(this.time, o.time);
// 小堆
}
}

2. 定时器类

class MyTimerTask
implements Comparable<
MyTimerTask> {
private Runnable runnable;
// 这里的 time, 通过毫秒时间戳,表示这个任务具体啥时候执行
private long time;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTimerTask o) {
// 此处这里的 - 的顺序,就决定了这是大堆还是小堆
return (int) (this.time-o.time);
}
}
class MyTimer
{
private PriorityQueue<
MyTimerTask> queue= new PriorityQueue<
>();
public MyTimer(){
// 创建线程,负责执行上述队列中的内容
Thread t = new Thread(()->
{
while (true){
if (queue.isEmpty()){
continue;
}
MyTimerTask current = queue.peek();
if (System.currentTimeMillis()>=current.getTime()){
current.run();
queue.poll();
}else{
continue;
}
}
});
t.start();
}
public void schedule(Runnable runnable,long delay){
MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
queue.offer(myTimerTask);
}
}

这里的代码有两个问题

while (true){
if (queue.isEmpty()){
continue;
}
  1. 初始情况下,如果队列中,没有任何元素

    此处的逻辑,就会在短时间内进行大量的循环

    这些循环的,都是没什么意义的

    一直在争抢锁,类似于线程的饿死

if (System.currentTimeMillis()>=current.getTime()){
current.run();
queue.poll();
}else{
continue;
}
  1. 假设队列中,已经包含元素了

    12:00 执行,现在是10:45

    就会一直反复循环检查是否是12:00,没有什么意义

改进后

class MyTimerTask
implements Comparable<
MyTimerTask> {
private Runnable runnable;
// 这里的 time, 通过毫秒时间戳,表示这个任务具体啥时候执行
private long time;
public MyTimerTask(Runnable runnable,long delay){
this.runnable=runnable;
this.time=System.currentTimeMillis()+delay;
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTimerTask o) {
// 此处这里的 - 的顺序,就决定了这是大堆还是小堆
return (int) (this.time-o.time);
}
}
class MyTimer
{
private PriorityQueue<
MyTimerTask> queue= new PriorityQueue<
>();
private Object locker = new Object();
public MyTimer(){
// 创建线程,负责执行上述队列中的内容
Thread t = new Thread(()->
{
});
t.start();
}
public void schedule(Runnable runnable,long delay){
synchronized (locker){
MyTimerTask myTimerTask=new MyTimerTask(runnable,delay);
queue.offer(myTimerTask);
locker.notify();
}
}
}

每次来新的任务,都会把wait唤醒,重新设定等待时间

3. 使用示例

public class Demo33
{
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(() ->
System.out.println("hello 3000"), 3000);
myTimer.schedule(() ->
System.out.println("hello 2000"), 2000);
myTimer.schedule(() ->
System.out.println("hello 1000"), 1000);
System.out.println("程序开始执行");
}
}

五、线程安全与性能优化

  • 优先队列本身不线程安全,需用锁(synchronized)保护。
  • 调度线程和添加任务线程要协作,使用 wait/notify 实现阻塞和唤醒。
  • 性能优化点:
    • 只需唤醒到最近任务的时间点,无需频繁循环检查。
    • 队列空时调度线程阻塞,省CPU。
    • 每次新任务加入,唤醒调度线程,重新计算等待时间。

六、定时器的局限与扩展

七、面试答题套路

  1. 定时器的核心原理是什么?(任务+调度线程+任务队列)
  2. 为什么不用 List?为什么选 PriorityQueue?
  3. 如何实现线程安全和高效唤醒?
  4. 如果任务很多,如何优化调度效率?
  5. 优先队列和时间轮的区别和适用场景?
  6. 代码细节:wait/notify、锁的使用、任务时间戳的设计。
  7. 多线程调度的潜在问题及解决方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/909848.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网站查询服务器成都网站建设公

战神4 幕后花絮 概念艺术找出Java幕后发生的事情&#xff0c;以及新功能如何实现 在上一篇文章中&#xff0c;我们介绍了即将发布的Java 9版本的新功能和尚待解决的功能&#xff0c;并简要提到了将新功能添加到下一个版本之前要经历的过程。 由于此过程几乎影响了所有Java开发人…

redis 6.0 多线程

Redis 6.0 的多线程,并非指命令处理逻辑的多线程(命令执行仍然是单线程的),而是特指网络 I/O 的多线程,其核心目标是优化大量网络 I/O 带来的性能瓶颈,提升吞吐量,尤其是在高并发场景下。Redis 6.0 之前 - 单线…

docker 常用命令与端口映射

搜索镜像:从 Docker Hub 查找镜像docker search <镜像名称> # 例如:docker search nginx拉取镜像:从仓库下载镜像到本地docker pull <镜像名称:标签> # 例如:docker pull nginx:latest # 如果不写标签…

衡阳市住房建设局网站软装设计ppt

好的思维导图软件能帮助用户更好的发挥创作能力&#xff0c;XMind是一款流行的思维导图软件&#xff0c;可以帮助用户创建各种类型的思维导图和概念图。 多样化的导图类型&#xff1a;XMind提供了多种类型的导图&#xff0c;如鱼骨图、树形图、机构图等&#xff0c;可以满足不同…

网站建设优惠券企业形象设计论文

在你储存项目的文件夹里面应该是这样的 里面.vcxproj后缀名的就是原来创建的项目&#xff0c;直接打开这个头文件源文件就会一起出来了&#xff01; 真的管用&#xff0c;亲测有效。

家居网站建设如何更新不了wordpress

第一部分:选择题 1、Python L6 (15分) 运行下面的程序,哪个值不可能出现?( ) import random print(random.randint(0, 3) * 2) 0236正确答案:C 2、Python L6 (15分) 运行下面的程序,输入哪

做糕点哪个网站影视网站建设方案

os.environ 是 Python 中 os 模块提供的一个字典&#xff0c;它表示当前系统的环境变量。环境变量是在操作系统级别设置的键值对&#xff0c;用于配置系统行为和提供信息给正在运行的进程。 os.environ 的基本用法包括获取和设置环境变量的值。下面是一些基本的使用示例&#…

网站押金收回怎么做分录做网站将文字放在图片上

任务&#xff08;task&#xff09; 任务是最抽象的,是一个一般性的术语,指由软件完成的一个活动。一个任务既可以是一个进程,也可以是一个线程。简而言之,它指的是一系列共同达到某一目的的操作。例如,读取数据并将数据放入内存中。这个任务可以作为一个进程来实现,也可以作为一…

广州自助网站设计平台app免费下载入口

6.5 代理proxy java.lang.reflect.proxy&#xff0c;用于创建动态代理类和实例&#xff0c; java.lang.reflect.InvocationHandler&#xff0c;使用动态代理创建实例时&#xff0c;需提供实现接口的类的实例。提供invoke方法&#xff0c;接收三个参数&#xff1a;代理对象、…

在线做效果图有哪些网站足球比赛直播间在线观看

目录 一、安装Anaconda3二、安装open3d三、测试代码四、结果展示五、测试数据 Windows系统下python版本Open3D-0.18.0 的快速安装与使用由CSDN点云侠原创&#xff0c;爬虫自重。如果你不是在点云侠的博客中看到该文章&#xff0c;那么此处便是不要脸的爬虫。 一、安装Anaconda…

linux重启mysql服务,几种常见的方法

在 Linux 系统中重启 MySQL 服务的方法因操作系统的初始化系统不同而略有差异,以下是几种常见的方法: 方法一:使用 systemctl 命令(适用于 systemd 系统,如 CentOS 7+/Ubuntu 16.04+) 这是现代 Linux 发行版最常…

详细介绍:Day20 K8S学习

详细介绍:Day20 K8S学习2025-09-22 17:32 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; fo…

opencv学习记录3

梯度边缘计算sobel算子 用来计算图像灰度的近似梯度,梯度越大越有可能是边缘。 Soble算子的功能集合了高斯平滑和微分求导,又被称为一阶微分算子,求导算子,在水平和垂直两个方向上求导,得到的是图像在X方法与Y方向…

统计分析神器 NCSS 2025 功能亮点+图文安装教程

软件介绍 NCSS 2025作为最新一代的统计分析与绘图软件,携众多新功能与改进强势登场。该软件对167个程序的输入和/或输出进行了更新,涵盖从基础统计分析到复杂模型构建的各个方面,如回归分析、方差分析、生存分析、质…

mysql常用语句,常用的语句整理

MySQL 常用语句可分为数据库操作、表操作、数据查询、数据增删改等几大类,以下是最常用的语句整理: 一、数据库操作 创建数据库 sql CREATE DATABASE 数据库名 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unico…

五星花园网站建设兼职腾讯企业qq

经常会帮别人恢复系统&#xff0c;很多都能把系统恢复的&#xff0c;但是也有时只有重装&#xff0c;重装系统拿个GHOST版很容易的&#xff0c;关键是需要把里面的数据给取出来&#xff0c;一些C盘上的文档&#xff0c;最老土的办法就是拆开机箱&#xff0c;把硬盘挂到另一台系…

上海网站备案审核怎么建设一个人自己网站

在 Vue 中使用 structuredClone 进行深拷贝来初始化对象内的数组 一、引言1.什么是深拷贝&#xff1f;2.为什么使用 structuredClone&#xff1f;3.示例代码4.详细解释5.兼容性注意事项 二、总结 一、引言 在前端开发中&#xff0c;处理复杂对象和数组时&#xff0c;深拷贝是一…

郑州二七区网站建设赔率网站怎么做

题解&#xff1a;CF1929C&#xff08;Sasha and the Drawing &#xff09; 一、 理解题意 CF链接 洛谷链接 大佬syz带着 a a a 元来到赌场&#xff0c;赌场的规则如下&#xff1a; 对于每一轮&#xff0c;假设选手下注 y y y 元钱&#xff08; y y y 应正整数&#xff0c;并…

临沂罗庄建设局网站网站建设要在哪学

摘要&#xff1a;形式化验证是证明软件、硬件或系统正确性的一种方法&#xff0c;近年来受到了越来越多的关注。 本文对形式化验证的研究进行了综述。首先介绍了形式化验证的基本概念&#xff0c;然后重点介绍了形式化验证的三种技术&#xff0c;包括模型检测、定理证明和等价性…

网站反链接是什么意思网站开始怎么做的

目录 归并排序详解 递归实现 迭代实现 面试题 77 : 链表排序 面试题 78 : 合并排序链表 法一、利用最小堆选取值最小的节点 法二、按照归并排序的思路合并链表 归并排序详解 归并排序就是将两个或两个以上的有序表合并成一个有序表的过程。将两个有序表合并成一个有序表…