JavaSE加强:线程Thread(线程安全重点)

一、什么是线程、进程

  1. 进程:正在运行的程序,操作系统资源分配的最小单位;进程之间相互独立。
  2. 线程:进程内部的执行路径(执行单元),CPU 调度的最小单位。一个进程至少有 1 个主线程。

多线程:同一个进程内,多条线程并发执行,抢占式调度(CPU 随机分配时间片)。

并发 vs 并行

  • 并发:同一时间段,多个任务交替执行(单核 CPU,看起来同时跑)
  • 并行:同一时刻,多个任务同时执行(多核 CPU)

二、创建线程的 3 种方式

方式 1:继承 Thread 类

  1. 定义类继承Thread
  2. 重写run():线程要执行的任务
  3. 创建子类对象,调用start()启动线程(不是直接调用 run ()!run 只是普通方法)
java 复制代码
public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("线程执行");
    }
}
//使用
MyThread t = new MyThread();
t.start();

缺点:Java 单继承,继承 Thread 后不能再继承其他类。

方式 2:实现 Runnable 接口(推荐)

  1. 类实现Runnable接口
  2. 重写run()
  3. 把 Runnable 实现类对象传入 Thread 构造器,调用start()
java 复制代码
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("runnable任务");
    }
}
//使用
Runnable run = new MyRunnable();
Thread t = new Thread(run);
t.start();

优点:避免单继承限制;任务和线程对象解耦,多个线程共用同一个任务对象。

方式 3:Callable + FutureTask(带返回值、可抛异常)

  • Runnable 的 run 无返回值,不能抛出受检异常;Callable 可以返回结果。
java 复制代码
//1.创建Callable实现类
class MyCall implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        return 100;
    }
}
//2.包装Callable到FutureTask
FutureTask<Integer> task = new FutureTask<>(new MyCall());
//3.传入Thread
Thread t = new Thread(task);
t.start();
//获取返回结果,会阻塞等待线程执行完成
Integer res = task.get();

三、Thread 常用 API

方法 作用
void start() 启动线程,自动调用 run ()
void run() 线程任务方法
static void sleep(long ms) 静态方法,让当前线程休眠指定毫秒,阻塞,抛出 InterruptedException
String getName() 获取线程名
void setName(String name) 设置线程名
static Thread currentThread() 获取当前正在执行的线程对象
void join() 等待该线程执行完毕,其他线程阻塞
void setDaemon(true) 设置为守护线程(后台线程,主线程结束守护线程自动结束,如垃圾回收 GC)
void interrupt() 中断线程(不是立刻停止,是标记中断状态)

重点:sleep()是静态方法,哪个线程执行 sleep,哪个线程休眠,不是写在哪个类就休眠哪个。

四、线程安全问题

1. 线程安全出现条件

  1. 多线程并发
  2. 多个线程共享同一个资源
  3. 共享资源存在写操作(修改)

经典案例:售票,多个线程同时卖票,出现重复票、负数票。

2. 线程同步(3 种解决方案,解决线程安全)

核心思想:加锁,同一时刻只允许一个线程进入临界区操作共享数据。

① 同步代码块 synchronized

java 复制代码
synchronized(锁对象){
    // 操作共享数据的代码(临界区)
}
  • 锁对象:任意同一个对象;多个线程必须共用同一把锁!
  • 执行逻辑:线程进入代码块,获取锁;执行完毕释放锁;其他线程阻塞等待锁。

② 同步方法 synchronized

把synchronized写在方法上

java 复制代码
//实例方法:锁对象默认 this
public synchronized void sellTicket(){
    
}
//静态方法:锁对象是 类名.class
public static synchronized void test(){

}

③ Lock 锁(JDK5 后,手动锁,更灵活)

ReentrantLock 可重入锁

java 复制代码
Lock lock = new ReentrantLock();
lock.lock(); //手动加锁
try{
    //共享资源操作
}finally{
    lock.unlock(); //手动释放锁,写finally防止异常不释放锁!
}

synchronized:自动加锁释放;Lock 手动控制,适合复杂场景。
面试区分:synchronized 是内置锁,非公平锁;ReentrantLock 支持公平 / 非公平锁、可中断。

五、线程池

为什么要用线程池

频繁创建销毁线程开销大;线程池复用线程,控制线程数量,防止 CPU 过载。 核心思想:提前创建一批线程,任务提交给池子,线程做完任务归还池子复用。

创建线程池方式

  1. Executors 工具类(简单,不推荐生产)
    • Executors.newFixedThreadPool(固定线程数)
  2. ThreadPoolExecutor 原生构造器(生产推荐,7 大参数)
    • 核心参数:核心线程数、最大线程数、空闲存活时间、阻塞队列、线程工厂、拒绝策略

5.1 ThreadPoolExecutor 七大参数详解(PPT 员工比喻:正式工 + 临时工)

java 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  1. corePoolSize:核心线程数量(正式员工)。线程池创建后,默认不会销毁,长期保留。
  2. maximumPoolSize:最大线程数量。核心线程 + 临时线程的总数上限。
  3. keepAliveTime:临时线程空闲存活时间。临时线程空闲超过这个时间,就会被销毁。
  4. unit :时间单位,配合keepAliveTime(TimeUnit.SECONDS 等)。
  5. workQueue:任务阻塞队列。任务提交,如果核心线程都忙,任务进入队列排队。
  6. threadFactory:线程工厂,用来创建线程对象,可以自定义线程名称。
  7. handler:拒绝策略。当核心线程、临时线程全部繁忙,队列也满了,新任务触发拒绝策略。

员工比喻: corePoolSize=3(3 个正式工),maximumPoolSize=5(最多一共 5 个人),临时线程 = 5-3=2 个临时工。临时工空闲太久就开除;正式工一直保留。

5.2 线程池任务执行流程(重点!面试必考)

  1. 提交任务,判断核心线程数是否未满 → 有空闲核心线程,直接创建核心线程执行任务。
  2. 核心线程全部繁忙,判断阻塞队列是否已满 → 队列没满,任务放进队列排队。
  3. 队列也满了,判断当前总线程数是否小于最大线程数 → 还没到上限,创建临时线程执行任务。

✅触发临时线程条件:核心线程全部忙 + 队列满 + 总线程数 < maximumPoolSize

  1. 总线程数已经达到最大值,此时新任务进来 → 触发拒绝策略。

一句话流程:核心线程 → 队列排队 → 创建临时线程 → 拒绝任务

5.3 四大拒绝策略(RejectedExecutionHandler)

表格

拒绝策略 行为说明
AbortPolicy(默认) 丢弃任务,并抛出RejectedExecutionException异常
DiscardPolicy 默默丢弃任务,不抛异常(生产不推荐,任务丢失无感知)
DiscardOldestPolicy 丢掉队列里等待最久的任务,尝试把当前新任务入队
CallerRunsPolicy 不丢弃,由提交任务的主线程来执行这个任务

5.4 Executors 工具类创建线程池(了解,生产禁止使用)

Executors 底层本质还是调用ThreadPoolExecutor,只是封装了参数,开发简单,但存在隐患:

  1. Executors.newFixedThreadPool(n):固定线程池,核心线程 = n,无临时线程,队列长度为Integer.MAX_VALUE。任务堆积会造成 OOM 内存溢出。
  2. Executors.newSingleThreadExecutor():单线程池,只有 1 个核心线程。队列容量极大,容易 OOM。
  3. Executors.newCachedThreadPool():可缓存线程池,无核心线程,临时线程上限 MAX_VALUE,大量任务会疯狂创建线程,CPU 爆满。

开发规范:阿里巴巴开发手册禁止在生产使用 Executors 创建线程池 ,强制手写ThreadPoolExecutor,自己指定队列长度、最大线程数,规避 OOM 风险。

5.5 线程池常用方法

方法 作用
execute(Runnable task) 提交无返回值任务
submit(Callable task) 提交带返回值任务,返回 Future 对象,可以获取结果
shutdown() 平缓关闭线程池:不再接收新任务,等待已提交任务全部执行完毕再关闭
shutdownNow() 立刻关闭,尝试中断正在执行任务,队列未执行任务直接返回

5.6 代码示例(手写 ThreadPoolExecutor)

java 复制代码
import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 7个参数手动创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,      //核心线程
                5,      //最大线程
                2,      //临时线程空闲存活时间
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10), //阻塞队列容量10
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy() //默认拒绝策略
        );

        //提交任务
        for (int i = 1; i <= 20; i++) {
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "执行任务");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        pool.shutdown();
    }
}

5.7 面试简答背诵

  1. 什么时候创建临时线程?

核心线程全部繁忙,任务队列已经满,并且当前线程总数还没达到 maximumPoolSize,才创建临时线程。

  1. 什么时候触发拒绝策略?

核心线程、临时线程全部繁忙,任务队列已满,新来任务触发拒绝策略。

  1. 为什么不推荐 Executors?

底层队列或者最大线程数是极大值,高并发场景下容易出现 OOM 内存溢出,生产推荐手动创建 ThreadPoolExecutor。

5.8 小结

  • 线程池核心目的:复用线程对象,减少频繁创建销毁线程带来的性能损耗,可控并发数量。
  • 任务执行顺序:核心线程优先 → 队列排队 → 临时线程 → 拒绝策略。
  • 四大拒绝策略:默认抛异常、丢弃无异常、丢弃最老任务、调用者执行。

线程池执行流程

  1. 任务进来,核心线程未满 → 创建核心线程执行
  2. 核心线程满了 → 任务放入阻塞队列排队
  3. 队列满 → 创建非核心线程,直到达到最大线程数
  4. 最大线程也满 → 触发拒绝策略(丢弃任务、抛异常等)

六、线程状态(6 种)

  1. NEW:新建,new Thread (),还没 start
  2. RUNNABLE:就绪 + 运行(CPU 调度)
  3. BLOCKED:阻塞,等待 synchronized 锁
  4. WAITING:无限等待 wait ()
  5. TIMED_WAITING:限时等待 sleep ()/wait (时间)
  6. TERMINATED:线程执行完毕终止

七、面试高频考点总结

  1. start () 和 run () 区别:start () 向操作系统申请创建线程;run 只是普通任务方法,直接调用不会开启新线程。
  2. synchronized 锁对象:实例方法锁 this;静态方法锁类字节码对象;同步代码块锁自定义对象。
  3. 守护线程:setDaemon (true) 必须写在 start 之前;主线程退出,守护线程自动终止。
  4. wait () 和 sleep ():wait 必须在同步代码块中执行,会释放锁;sleep 不释放锁;wait 是 Object 方法,sleep 是 Thread 静态方法。

八、售票案例完整代码(同步代码块演示线程安全)

java 复制代码
public class TicketTask implements Runnable{
    private int ticket = 100;
    private Object lock = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (lock) {
                if(ticket <=0){
                    break;
                }
                System.out.println(Thread.currentThread().getName()+"卖票:"+ticket);
                ticket--;
            }
        }
    }
}
//测试
public class Test{
    public static void main(String[] args) {
        Runnable task = new TicketTask();
        Thread t1 = new Thread(task,"窗口1");
        Thread t2 = new Thread(task,"窗口2");
        Thread t3 = new Thread(task,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
相关推荐
一只废狗狗狗狗狗狗狗狗狗1 小时前
Network架构1——卷积神经网络CNN
人工智能·算法·cnn
令狐少侠20111 小时前
centos7 下,使用docker 部署IOT 调试平台,ThingsBoard 并完美启动
java·spring boot·iot
小灰灰搞电子1 小时前
Rust+Slint 实现一个终端软件源码分享(半成品)
开发语言·rust
大鹏说大话1 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 对比
开发语言·c++·list
JAVA面经实录9171 小时前
Java高级后端 · 全套面试通关手册(线上故障排查)
java·jvm·面试
IT_Octopus1 小时前
【零基础入门 LLM 开发 · Day 11】:ChatOpenAI vs init_chat_model——一行换供应商
java·前端·javascript
宇卿.1 小时前
C#语句:
开发语言·c#
ComPDFKit1 小时前
用 ComPDF DocSlight 提取 PDF 表格:三种样本实测与 Python 接入
开发语言·python·pdf·pdf表格提取
alphaTao1 小时前
LeetCode 每日一题 2026/10/5-2026/10/11
算法·leetcode