当前位置: 首页 > news >正文

Java多线程:线程创建、安全、同步与线程池

目录

  • Java多线程:线程创建、安全、同步与线程池
    • 1. 什么是多线程?
    • 2. 线程创建和启动
      • 2.1 方式一:继承Thread类
      • 2.2 方式二:实现Runnable接口
      • 2.3 方式三:实现Callable接口(带返回值)
      • 2.4 三种创建方式的对比
    • 3. Thread的常用构造器和方法
      • 3.1 常用构造器
      • 3.2 常用方法及注意事项
    • 4. 线程安全
      • 4.1 什么是线程安全?
      • 4.2 线程安全问题出现的原因
      • 4.3 模拟线程安全问题(售票系统超卖)
    • 5. 线程同步(解决线程安全问题)
      • 5.1 认识线程同步
      • 5.2 同步代码块(显式指定锁对象)
      • 5.3 同步方法(隐式锁对象)
      • 5.4 Lock锁(JDK 5+,显式锁)
    • 6. 线程池
      • 6.1 认识线程池
      • 6.2 创建线程池:ThreadPoolExecutor
      • 6.3 常用方法
      • 6.4 线程池注意事项
        • 6.4.1 什么时候创建临时线程?
        • 6.4.2 什么时候拒绝新任务?
        • 6.4.3 任务拒绝策略
        • 6.4.4 参数选择配置公式
      • 6.5 Executors工具类(不推荐)
    • 7. 并发和并行的概念

Java多线程:线程创建、安全、同步与线程池

1. 什么是多线程?

多线程是指在一个程序中同时运行多个独立的执行流(线程),共享同一进程的资源(如内存空间),但各自拥有独立的执行栈和程序计数器。

  • 生活类比:一家餐厅(进程)有多个服务员(线程)同时为顾客服务,共享餐厅的资源(厨房、餐具),但各自处理不同的订单。
  • 核心优势:提高程序执行效率(如后台下载文件时不阻塞UI操作)、充分利用CPU资源。

2. 线程创建和启动

Java提供三种创建线程的方式,各有优缺点,适用于不同场景。

2.1 方式一:继承Thread类

步骤

  1. 定义类继承Thread,重写run()方法(线程执行体);
  2. 创建线程对象,调用start()方法启动线程(注意:直接调用run()不会启动新线程,只是普通方法调用)。

代码案例

// 1. 自定义线程类,继承Thread
class MyThread extends Thread {// 2. 重写run()方法:线程要执行的任务@Overridepublic void run() {for (int i = 0; i < 3; i++) {// Thread.currentThread().getName():获取当前线程名称System.out.println(Thread.currentThread().getName() + "执行:" + i);}}
}public class ThreadDemo {public static void main(String[] args) {// 3. 创建线程对象MyThread t1 = new MyThread();MyThread t2 = new MyThread();// 4. 设置线程名称(可选)t1.setName("线程A");t2.setName("线程B");// 5. 启动线程(底层调用start0()本地方法,由JVM创建新线程并执行run())t1.start(); t2.start();// 注意:主线程任务应放在启动子线程之后,避免主线程先执行完System.out.println("主线程执行完毕");}
}

执行结果(线程调度顺序不确定,每次运行可能不同):

主线程执行完毕
线程A执行:0
线程B执行:0
线程A执行:1
线程B执行:1
线程A执行:2
线程B执行:2

注意事项

  • 不能多次调用start():一个线程对象只能启动一次,重复调用会抛出IllegalThreadStateException
  • start() vs run()start()会启动新线程并异步执行run();直接调用run()会在当前线程同步执行,无多线程效果。

2.2 方式二:实现Runnable接口

步骤

  1. 定义类实现Runnable接口,重写run()方法;
  2. 创建Runnable实现类对象,作为参数传入Thread构造器;
  3. 调用Thread对象的start()方法启动线程。

优势:避免单继承限制(一个类可实现多个接口),适合多线程共享资源场景。

代码案例

// 1. 实现Runnable接口
class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "执行:" + i);}}
}public class RunnableDemo {public static void main(String[] args) {// 2. 创建Runnable对象(任务)MyRunnable task = new MyRunnable();// 3. 将任务交给Thread线程对象Thread t1 = new Thread(task, "线程C"); // 直接指定线程名称Thread t2 = new Thread(task); t2.setName("线程D");// 4. 启动线程t1.start();t2.start();}
}

简化写法

  • 匿名内部类:无需单独定义类,直接在Thread构造器中实现Runnable
  Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("匿名内部类线程执行");}}, "匿名线程");t.start();
  • Lambda表达式(Java 8+,进一步简化):
  Thread t = new Thread(() -> {System.out.println("Lambda线程执行");}, "Lambda线程");t.start();

2.3 方式三:实现Callable接口(带返回值)

步骤

  1. 实现Callable<T>接口,重写call()方法(有返回值,可抛出异常);
  2. 创建Callable对象,包装为FutureTask(实现RunnableFuture接口,兼具RunnableFuture特性);
  3. FutureTask传入Thread构造器,调用start()启动线程;
  4. 通过FutureTask.get()获取返回值(会阻塞当前线程,直到任务完成)。

优势:可获取线程执行结果,可处理异常。

代码案例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;// 1. 实现Callable接口,泛型指定返回值类型
class MyCallable implements Callable<Integer> {private int num;public MyCallable(int num) {this.num = num;}// 2. 重写call()方法,返回计算结果@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= num; i++) {sum += i;}return sum; // 返回1~num的和}
}public class CallableDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {// 3. 创建Callable任务MyCallable task = new MyCallable(100);// 4. 包装为FutureTask(用于获取结果)FutureTask<Integer> futureTask = new FutureTask<>(task);// 5. 启动线程new Thread(futureTask, "求和线程").start();// 6. 获取结果(会阻塞,直到call()执行完毕)Integer result = futureTask.get();System.out.println("1~100的和为:" + result); // 输出:5050}
}

2.4 三种创建方式的对比

方式优点缺点适用场景
继承Thread代码简单,直接使用this获取线程对象单继承限制,任务与线程耦合简单任务,无继承需求
实现Runnable无单继承限制,任务与线程解耦无法直接获取返回值,不能抛出受检异常多线程共享资源,复杂任务
实现Callable可获取返回值,可抛出异常代码较复杂,需配合FutureTask需要获取线程执行结果的场景

3. Thread的常用构造器和方法

3.1 常用构造器

构造器说明
Thread()创建线程对象,默认名称(Thread-0,1…)
Thread(String name)指定线程名称
Thread(Runnable target)将Runnable任务传入线程
Thread(Runnable target, String name)传入任务并指定线程名称

3.2 常用方法及注意事项

方法名作用注意事项
void start()启动线程(异步执行run())不可重复调用
void run()线程执行体直接调用无多线程效果
String getName()获取线程名称-
void setName(String name)设置线程名称建议在start()前设置
static Thread currentThread()获取当前执行的线程对象静态方法,可在任何地方调用
static void sleep(long millis)让当前线程休眠指定毫秒数会抛出InterruptedException,需处理
void join()等待该线程执行完毕后,当前线程再继续需处理InterruptedException

代码案例

public class ThreadMethodDemo {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {try {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "执行:" + i);Thread.sleep(500); // 休眠500ms,模拟任务耗时}} catch (InterruptedException e) {e.printStackTrace();}}, "演示线程");t.start();System.out.println("等待演示线程执行完毕...");t.join(); // 主线程等待t线程执行完System.out.println("演示线程已结束,主线程继续");}
}

执行结果

等待演示线程执行完毕...
演示线程执行:0
演示线程执行:1
演示线程执行:2
演示线程已结束,主线程继续

4. 线程安全

4.1 什么是线程安全?

当多个线程同时操作共享资源时,若无需额外同步操作就能保证结果正确,则称该资源是线程安全的。

  • 共享资源:多个线程都能访问的变量、对象、文件等(如多线程售票系统中的“剩余票数”)。

4.2 线程安全问题出现的原因

  1. 多线程并发访问:多个线程同时操作共享资源;
  2. 共享资源修改:对共享资源进行非原子性操作(如count++实际分为“读取-修改-写入”三步);
  3. 缺乏同步机制:未对共享资源的访问进行限制。

4.3 模拟线程安全问题(售票系统超卖)

场景:3个窗口同时售卖10张票,未加同步机制时出现超卖或重复售票。

class TicketSystem implements Runnable {private int ticketCount = 10; // 共享资源:总票数@Overridepublic void run() {while (true) {if (ticketCount > 0) {// 模拟售票耗时(放大线程安全问题)try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--; // 非原子操作:读取ticketCount -> 减1 -> 写回} else {break;}}}
}public class ThreadSafeDemo {public static void main(String[] args) {TicketSystem task = new TicketSystem();new Thread(task, "窗口A").start();new Thread(task, "窗口B").start();new Thread(task, "窗口C").start();}
}

问题结果(可能出现):

窗口A售出第10张票
窗口B售出第10张票  // 重复售票
窗口C售出第9张票
...
窗口A售出第1张票
窗口B售出第0张票  // 超卖(票数为负)

5. 线程同步(解决线程安全问题)

5.1 认识线程同步

线程同步:通过限制多个线程对共享资源的访问顺序,保证同一时刻只有一个线程操作资源,从而解决安全问题。核心是加锁:将共享资源的操作代码“锁住”,只有获得锁的线程才能执行。

5.2 同步代码块(显式指定锁对象)

语法

synchronized (锁对象) {// 共享资源操作代码(临界区)
}

作用:同一时刻只有一个线程能进入同步代码块(需获取锁对象的“对象锁”)。

原理:锁对象是同步的关键,多个线程必须使用同一个锁对象才能保证同步效果。

解决售票问题案例

class SafeTicketSystem implements Runnable {private int ticketCount = 10;private Object lock = new Object(); // 锁对象(必须是多个线程共享的对象)@Overridepublic void run() {while (true) {// 同步代码块:锁住共享资源操作synchronized (lock) { if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--;} else {break;}}}}
}

注意事项

  • 锁对象规范
    • 实例方法中:通常用this(当前对象)作为锁;
    • 静态方法中:必须用类对象(如TicketSystem.class)作为锁,因为静态方法属于类,不依赖实例。

5.3 同步方法(隐式锁对象)

语法

修饰符 synchronized 返回值类型 方法名(参数) {// 共享资源操作代码
}

原理

  • 实例同步方法:锁对象为this(当前实例);
  • 静态同步方法:锁对象为类对象(如Xxx.class)。

与同步代码块的区别

  • 同步方法:锁住整个方法体,粒度较粗;
  • 同步代码块:可只锁住关键代码,粒度更细,性能更好。

代码案例(同步方法解决售票问题):

class SafeTicketSystem2 implements Runnable {private int ticketCount = 10;// 同步实例方法:锁对象为this(当前Runnable实例)private synchronized void sellTicket() {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--;}}@Overridepublic void run() {while (ticketCount > 0) {sellTicket(); // 调用同步方法}}
}

5.4 Lock锁(JDK 5+,显式锁)

介绍java.util.concurrent.locks.Lock接口,提供比synchronized更灵活的锁定操作(如尝试获取锁、超时释放锁等)。常用实现类ReentrantLock(可重入锁)。

常用方法

  • void lock():获取锁(若未获取到则阻塞);
  • void unlock():释放锁(必须在finally中调用,确保锁释放);
  • boolean tryLock():尝试获取锁(成功返回true,失败返回false,不阻塞)。

代码案例(Lock解决售票问题):

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;class SafeTicketSystem3 implements Runnable {private int ticketCount = 10;// 锁对象用final修饰,防止被篡改(重要!)private final Lock lock = new ReentrantLock(); @Overridepublic void run() {while (true) {lock.lock(); // 获取锁try {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "售出第" + ticketCount + "张票");ticketCount--;} else {break;}} finally {lock.unlock(); // 释放锁(必须在finally中,防止异常时锁未释放)}}}
}

注意事项

  • 必须将unlock()放在finally中,否则若代码抛出异常,锁可能永远无法释放,导致死锁;
  • 锁对象建议用final修饰,防止在运行中被修改为其他对象,导致锁失效。

6. 线程池

6.1 认识线程池

线程池:管理一组预先创建的线程,用于重复执行多个任务,避免频繁创建/销毁线程的开销(线程创建和销毁需要消耗CPU和内存资源)。

不使用线程池的影响

  • 频繁创建线程:导致CPU资源浪费、内存占用过高;
  • 无限制创建线程:可能引发OutOfMemoryError(OOM)。

线程池工作原理

  1. 线程池初始化时创建核心线程
  2. 任务提交时,优先使用核心线程执行;
  3. 核心线程满时,任务进入阻塞队列等待;
  4. 队列满时,创建临时线程(不超过最大线程数);
  5. 所有线程和队列都满时,触发任务拒绝策略
  6. 临时线程空闲超过存活时间,自动销毁。

6.2 创建线程池:ThreadPoolExecutor

构造器及7个参数

public ThreadPoolExecutor(int corePoolSize, // 核心线程数(常驻线程,即使空闲也不销毁)int maximumPoolSize, // 最大线程数(核心+临时线程的上限)long keepAliveTime, // 临时线程空闲存活时间TimeUnit unit, // 存活时间单位(如TimeUnit.SECONDS)BlockingQueue<Runnable> workQueue, // 任务阻塞队列(核心线程满时存放任务)ThreadFactory threadFactory, // 线程工厂(用于创建线程,可自定义线程名称)RejectedExecutionHandler handler // 任务拒绝策略(队列和线程都满时如何处理新任务)
)

参数解析案例(创建“工厂生产线程池”):

import java.util.concurrent.*;public class ThreadPoolDemo {public static void main(String[] args) {// 1. 创建线程池ThreadPoolExecutor pool = new ThreadPoolExecutor(2, // 核心线程数:2个(生产线固定工人)5, // 最大线程数:5个(最多临时加3个工人)3, // 临时线程存活时间:3秒TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), // 队列容量:3个任务(超出核心线程的任务排队)Executors.defaultThreadFactory(), // 默认线程工厂new ThreadPoolExecutor.AbortPolicy() // 拒绝策略:直接抛出异常);// 2. 提交10个任务(模拟10个产品需要加工)for (int i = 1; i <= 10; i++) {int taskId = i;pool.submit(() -> {System.out.println(Thread.currentThread().getName() + "加工产品" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {} // 模拟加工耗时});}// 3. 关闭线程池(不再接受新任务,等待现有任务执行完)pool.shutdown();}
}

6.3 常用方法

方法名作用
submit(Runnable task)提交Runnable任务(无返回值)
submit(Callable<T> task)提交Callable任务(有返回值,返回Future)
shutdown()平缓关闭线程池:不再接受新任务,等待现有任务完成
shutdownNow()立即关闭线程池:尝试中断所有任务,返回未执行的任务

6.4 线程池注意事项

6.4.1 什么时候创建临时线程?

核心线程全部繁忙阻塞队列已满时,才会创建临时线程(最多到maximumPoolSize)。

6.4.2 什么时候拒绝新任务?

核心线程满队列满临时线程满(达到maximumPoolSize)时,新任务触发拒绝策略。

6.4.3 任务拒绝策略
拒绝策略作用
AbortPolicy(默认)直接抛出RejectedExecutionException
CallerRunsPolicy由提交任务的线程(如主线程)自己执行
DiscardPolicy默默丢弃新任务,无任何提示
DiscardOldestPolicy丢弃队列中最旧的任务,尝试提交新任务
6.4.4 参数选择配置公式
  • 核心线程数
    • CPU密集型任务(如计算):核心线程数 = CPU核心数 + 1(减少线程切换开销);
    • IO密集型任务(如文件读写、网络请求):核心线程数 = CPU核心数 * 2(IO操作时线程会阻塞,可多开线程提高利用率)。
  • 队列容量:根据任务提交频率和处理耗时调整,避免过大(导致OOM)或过小(频繁创建临时线程)。

6.5 Executors工具类(不推荐)

作用:提供快速创建线程池的静态方法,但存在严重弊端。

常用方法及弊端

方法名说明弊端
newFixedThreadPool(n)固定核心线程数(n),队列容量无上限队列无限大,任务过多时导致OOM
newCachedThreadPool()无核心线程,最大线程数无限,临时线程存活60秒无限创建线程,导致OOM
newSingleThreadExecutor()单线程池,队列容量无上限队列无限大,导致OOM

推荐做法手动使用ThreadPoolExecutor创建线程池,明确指定核心参数,避免OOM风险。

7. 并发和并行的概念

  • 并发(Concurrency):多个任务在同一时间段内交替执行(宏观上同时,微观上交替)。
    • 例:一个CPU核心“快速切换”处理多个任务(如边听歌边聊天)。
  • 并行(Parallelism):多个任务在同一时刻同时执行(需多个CPU核心支持)。
    • 例:两个CPU核心分别处理“听歌”和“聊天”任务,真正同时进行。

生活类比

  • 并发:一个厨师同时处理多个订单(切菜→炒菜→装盘,交替进行);
  • 并行:多个厨师同时处理不同订单(各自独立工作,互不干扰)。
http://www.xdnf.cn/news/1325863.html

相关文章:

  • 常见的 Bash 命令及简单脚本
  • C语言实战:从零开始编写一个通用配置文件解析器
  • SpringAI——向量存储(vector store)
  • 电子电气架构 --- 软件项目成本估算
  • UE5 PCG 笔记(一)
  • 零基础数据结构与算法——第八章 算法面试准备-数组/字符串/链表/树/动态规划/回溯
  • JVM之Java内存区域与内存溢出异常
  • Python + 淘宝 API 开发:自动化采集商品数据的完整流程​
  • 8.19作业
  • 星图云开发者平台新功能速递 | 微服务管理器:无缝整合异构服务,释放云原生开发潜能
  • 部署tomcat应用时注意事项
  • 数据迁移:如何从MySQL数据库高效迁移到Neo4j图形数据库
  • 高性能AI推理与工作站GPU:DigitalOcean L40s、RTX 6000 Ada与A6000全解析
  • UniApp 微信小程序之间跳转指南
  • Leetcode 343. 整数拆分 动态规划
  • 【最新版】CRMEB Pro版v3.4系统源码全开源+PC端+uniapp前端+搭建教程
  • LLM 中 token 简介与 bert 实操解读
  • 大语言模型中的归一化实现解析
  • Vim笔记:缩进
  • AiPPT怎么样?好用吗?
  • Qt密码生成器项目开发教程 - 安全可靠的随机密码生成工具
  • Orbbec---setBoolProperty 快捷配置设备行为
  • Go高效复用对象:sync.Pool详解
  • JavaScript 性能优化:new Map vs Array.find() 查找速度深度对比
  • openldap安装 -添加条目
  • 【什么是非晶合金?非晶电机有什么优点?】
  • RecSys:粗排模型和精排特征体系
  • 图解快速排序C语言实现
  • “道法术器” 思维:解析华为数字化转型
  • Lua学习记录 - 自定义模块管理器