Java 并发编程系列(上篇):多线程深入解析

一、开篇:走进 Java 并发编程世界

        在现代软件开发中,充分利用多核 CPU 的计算能力至关重要,Java 并发编程为我们提供了实现这一目标的工具。从简单的多线程任务并行执行,到复杂的高并发系统设计,掌握并发编程是进阶 Java 工程师的关键一步。本篇作为上篇,聚焦多线程基础、线程状态、线程组与优先级、进程线程区别,以及synchronized锁的基础与状态体系 。

        先叠个甲,由于这一块内容是面试必问的部分,也是经常用的,内容太多,我分三篇逐步更新,从基础线程概念到线程池、锁等复杂场景。

二、Java 多线程入门:创建与核心逻辑

(一)创建线程的三种方式

1. 继承 Thread 类:线程逻辑内聚

        继承Thread,重写run方法定义线程执行体。调用start方法启动新线程(直接调用run是普通方法调用,不会新建线程 )。

class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "执行,i=" + i);}}
}public class ThreadInheritDemo {public static void main(String[] args) {MyThread thread1 = new MyThread();thread1.setName("自定义线程1");thread1.start(); MyThread thread2 = new MyThread();thread2.setName("自定义线程2");thread2.start(); }
}

运行后,两个线程交替输出,体现多线程并发执行特性,适合线程逻辑简单且无需复用的场景。

2. 实现 Runnable 接口:解耦任务与线程

        将线程执行逻辑封装到Runnable实现类,避免单继承限制(Java 类仅能单继承,但可实现多个接口 ),方便任务逻辑复用。        

class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "执行,i=" + i);}}
}public class RunnableImplDemo {public static void main(String[] args) {MyRunnable runnable = new MyRunnable();Thread threadA = new Thread(runnable, "线程A");Thread threadB = new Thread(runnable, "线程B");threadA.start();threadB.start();}
}

 Runnable作为任务载体,被不同线程实例执行,常用于线程池、任务分发等场景。

3. 实现 Callable 接口:支持返回结果

  CallableRunnable类似,但call方法有返回值,需结合FutureFutureTask获取结果,适用于需线程执行产出数据的场景。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 5; i++) {sum += i;}return sum;}
}public class CallableImplDemo {public static void main(String[] args) {MyCallable callable = new MyCallable();FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread thread = new Thread(futureTask, "计算线程");thread.start();try {Integer result = futureTask.get(); System.out.println("线程计算结果:" + result);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}
}

futureTask.get()会阻塞直到线程执行完毕返回结果,也可设置超时时间避免永久阻塞。

二)线程关键疑问:run 与 start、重写 run 的本质

1. 为何重写 run 方法?

  Thread类默认run方法体为空(public void run() {} ),重写run是为了定义线程实际执行的业务逻辑,让线程启动后执行我们期望的代码。

2. run 方法与 start 方法的区别
  • run 方法:普通实例方法,调用时由当前线程(调用run的线程)顺序执行run内代码,不会创建新线程。
  • start 方法Thread类特殊方法,调用后触发 JVM创建新线程,并由新线程执行run逻辑。即start是 “启动新线程 + 执行任务”,run只是 “执行任务(当前线程)” 。
class TestThread extends Thread {@Overridepublic void run() {System.out.println("当前线程名:" + Thread.currentThread().getName());}
}public class StartRunDistinguish {public static void main(String[] args) {TestThread thread = new TestThread();thread.setName("自定义线程");thread.run(); thread.start(); }
}

输出:
当前线程名:main
当前线程名:自定义线程
清晰展示两者差异,start才是真正启动新线程的方式。

三)控制线程的常用方法

1. sleep ():线程休眠

        使当前线程暂停指定时间(毫秒),让出 CPU 但不释放对象锁(若持有锁)。常用于模拟延迟、协调执行节奏。

public class SleepUsage {public static void main(String[] args) {new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "执行,i=" + i);try {Thread.sleep(1000); } catch (InterruptedException e) {e.printStackTrace();}}}, "睡眠线程").start();}
}

线程每隔 1 秒输出,期间 CPU 可被其他线程使用,但锁资源(若涉及同步代码)不会释放。

2. join ():线程等待

让当前线程等待目标线程执行完毕后再继续,用于协调线程执行顺序。

public class JoinUsage {public static void main(String[] args) throws InterruptedException {Thread threadA = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("线程A执行,i=" + i);try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}});threadA.start();threadA.join(); System.out.println("线程A执行完毕,main线程继续");}
}

 “main 线程继续” 需在线程 A 执行完 3 次循环后才输出,确保执行顺序。

其实也可以这样理解,让A线程插队,当前线程main在A线程执行完毕后再执行

3. setDaemon ():守护线程

        守护线程为用户线程服务,所有用户线程结束后,守护线程自动终止(如 JVM 的 GC 线程 )。需在start前设置。

public class DaemonUsage {public static void main(String[] args) {Thread daemonThread = new Thread(() -> {while (true) {System.out.println("守护线程运行中...");try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}});daemonThread.setDaemon(true); daemonThread.start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main线程结束");}
}

main 线程(用户线程)结束后,守护线程随之停止,不会无限循环。

4. yield ():线程让步

        当前线程主动让出 CPU 使用权,回到就绪状态重新参与调度,仅为 “建议”,不保证生效,用于给同优先级线程更多执行机会。
 

public class YieldUsage {public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("线程1执行,i=" + i);Thread.yield(); }});Thread thread2 = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("线程2执行,i=" + i);}});thread1.start();thread2.start();}
}

线程 1 每次循环尝试让步,线程 2 可能更频繁执行,但因调度不确定性,结果不绝对一致。

三、Java 线程的 6 种状态:生命周期全解析

Java 线程状态定义在Thread.State枚举中,共 6 种,理解状态转换对排查线程问题、优化并发逻辑至关重要。

(一)状态枚举与含义

  1. NEW(新建):线程对象已创建(如new Thread() ),但未调用start,未启动。
  2. RUNNABLE(可运行):线程已启动(start调用后),可能正在 CPU 执行,或在就绪队列等待调度,也就是就绪状态。
  3. BLOCKED(阻塞):线程竞争synchronized锁失败,进入阻塞态,等待锁释放。
  4. WAITING(等待):线程调用Object.wait()(无超时)、Thread.join()(无超时)、LockSupport.park()等,无时限等待唤醒。
  5. TIMED_WAITING(计时等待):调用Thread.sleep(long)Object.wait(long)Thread.join(long) 、LockSupport.parkNanos/parkUntil等,限时等待,超时自动唤醒。
  6. TERMINATED(终止):线程执行完毕(run正常结束或抛未捕获异常),生命周期结束。

(二)状态转换示例

通过代码观察线程从新建到终止的状态变化:

public class ThreadStateAnalysis {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {try {Thread.sleep(1500); synchronized (ThreadStateAnalysis.class) {System.out.println("线程执行中,获取锁");}} catch (InterruptedException e) {e.printStackTrace();}});System.out.println("线程状态(NEW):" + thread.getState()); thread.start();Thread.sleep(500); System.out.println("线程状态(TIMED_WAITING):" + thread.getState()); Thread.sleep(1500); System.out.println("线程状态(RUNNABLE/执行中):" + thread.getState()); thread.join(); System.out.println("线程状态(TERMINATED):" + thread.getState()); }
}

         结合getState()与线程执行逻辑,可清晰看到状态NEWTIMED_WAITINGRUNNABLETERMINATED的流转。实际调试中,可借助 JConsole、VisualVM 等工具直观分析复杂状态切换。

四、线程组与线程优先级:管理与调度辅助

(一)线程组(ThreadGroup)

        线程组用于批量管理线程,可统一设置优先级、捕获未处理异常等。默认情况下,新建线程加入创建它的线程所在组(通常是main线程组 )。

public class ThreadGroupManagement {public static void main(String[] args) {ThreadGroup customGroup = new ThreadGroup("自定义线程组");Thread thread1 = new Thread(customGroup, () -> {System.out.println("线程1所属组:" + Thread.currentThread().getThreadGroup().getName());}, "线程1");Thread thread2 = new Thread(customGroup, () -> {System.out.println("线程2所属组:" + Thread.currentThread().getThreadGroup().getName());}, "线程2");thread1.start();thread2.start();Thread[] threads = new Thread[customGroup.activeCount()];customGroup.enumerate(threads);for (Thread t : threads) {System.out.println("线程组线程:" + t.getName());}}
}

线程组辅助批量操作,但现代并发更依赖线程池,线程组使用场景逐渐减少,了解即可。

(二)线程优先级:调度 “建议”

        线程优先级是调度器优先调度的 “建议”,范围 1(最低)~10(最高),默认 5。优先级高的线程理论上获取 CPU 时间片机会更多,但不保证执行顺序(受操作系统调度策略影响 )。

public class ThreadPriorityControl {public static void main(String[] args) {Thread highPriority = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("高优先级线程执行,i=" + i);}});highPriority.setPriority(Thread.MAX_PRIORITY); Thread lowPriority = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("低优先级线程执行,i=" + i);}});lowPriority.setPriority(Thread.MIN_PRIORITY); highPriority.start();lowPriority.start();}
}

        多次运行可能观察到高优先级线程更 “活跃”,但因系统调度不确定性,不能完全依赖优先级控制执行顺序,实际开发需谨慎使用。

五、进程与线程的区别:资源与执行单元

(一)核心差异对比

对比维度
进程
线程
资源分配单位操作系统分配资源(内存、文件句柄等)的基本单位进程内的执行单元,CPU 调度的基本单位
资源占用独立地址空间,资源消耗大共享进程资源(内存、文件描述符等),消耗小
上下文切换成本高(需切换地址空间、寄存器等)
低(主要切换寄存器、程序计数器)
通信复杂度进程间通信复杂(IPC:管道、socket 等)线程间通信简单(共享内存)
独立性进程间相互独立,一个崩溃不影响其他进程线程同属进程,一个崩溃可能致进程崩溃

(二)通俗类比

以 “在线文档编辑应用” 为例:

  • 进程:整个应用是进程,操作系统为其分配独立内存,存储代码、用户数据等,是资源隔离的单位。
  • 线程:拼写检查、自动保存、实时协作同步等功能,作为线程共享进程内存,协作完成任务。若拼写检查线程崩溃,可能导致整个应用(进程)异常,体现线程对进程的依赖。

 

六、synchronized 关键字:锁的基础与状态体系

(一)锁的基本认知:基于对象的锁

        Java 中每个对象均可作为锁,常说的 “类锁” 本质是Class对象的锁(Class对象在 JVM 加载类时创建,唯一对应类元数据 )。通过synchronized实现同步,保障多线程下共享资源的原子性、可见性。

(二)synchronized 的三种使用形式

1. 同步实例方法(锁当前对象)
        银行账户(Account)有一个withdraw方法,一个人在不同设备上同时取钱,多线程可能同时取款,需保证余额正确。

代码示例

public class Account {private double balance;public Account(double balance) {this.balance = balance;}// 同步实例方法:锁当前对象(this)public synchronized void withdraw(double amount) {if (balance >= amount) {try {Thread.sleep(100); // 模拟业务耗时} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + balance);} else {System.out.println(Thread.currentThread().getName() + "取款失败,余额不足");}}
}
  • 锁的是当前对象(this),若多个线程操作同一对象,会互斥。
  • 若多个线程操作不同对象,则互不影响(每个对象有独立的锁)。
2. 同步静态方法(锁类对象)
统计网站访问量(静态变量visitCount),多线程并发访问需保证计数正确。

代码示例

public class WebSite {private static int visitCount = 0;// 同步静态方法:锁类对象(WebSite.class)public static synchronized void incrementVisit() {visitCount++;System.out.println(Thread.currentThread().getName() + "访问,总访问量:" + visitCount);}
}
  • 锁的是类的Class对象(全局唯一),无论创建多少实例,所有线程都会互斥。
  • 适合保护静态共享资源(如全局计数器、配置信息)。
3. 同步代码块

电商系统中,商品库存(stock)和订单号生成器(orderIdGenerator)需分别加锁。

代码示例:

public class ShoppingSystem {private int stock = 10;private static final Object STOCK_LOCK = new Object(); // 库存锁private static final Object ORDER_LOCK = new Object(); // 订单号锁private static int orderId = 0;// 扣减库存public void reduceStock() {synchronized (STOCK_LOCK) { // 锁库存专用对象if (stock > 0) {stock--;System.out.println(Thread.currentThread().getName() + "扣减库存成功,剩余:" + stock);} else {System.out.println(Thread.currentThread().getName() + "库存不足");}}}// 生成订单号public static void generateOrderId() {synchronized (ORDER_LOCK) { // 锁订单号专用对象orderId++;System.out.println(Thread.currentThread().getName() + "生成订单号:" + orderId);}}
}
  • 锁对象可以是任意Object,推荐使用private static final修饰,避免外部访问。
  • 缩小锁的范围,提高并发性能(如只锁需要保护的代码,而非整个方法)。

结合场景更容易理解,注意理解,而非死记硬背 

(三)synchronized 的四种锁状态

        JVM 对synchronized锁进行优化,存在四种状态:偏向锁、轻量级锁、重量级锁、无锁,状态随竞争情况升级(不能降级,单向升级 )。

后面的内容我们在下一篇中讲....

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

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

相关文章

[逆向工程] C实现过程调试与钩子安装(二十七)

[逆向工程] C实现过程调试与钩子安装&#xff08;二十七&#xff09; 引言 在现代逆向工程和调试领域&#xff0c;能够动态监控和操控进程执行非常关键。本篇文章将全面讲解如何使用 C 编写一个进程调试器——hookdbg64.exe&#xff0c;实现对目标进程的附加、监控 WriteFile…

分页查询的实现

第一步&#xff1a;导入pom依赖 <!--配置PageHelper分页插件--><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.4.6</version><exclusions>…

JDK17 Http Request 异步处理 源码刨析

为什么可以异步&#xff1f; #调用起始源码 // 3. 发送异步请求并处理响应 CompletableFuture future client.sendAsync( request, HttpResponse.BodyHandlers.ofString() // 响应体转为字符串 ).thenApply(response -> { // 状态码检查&#xff08;非200系列抛出异常&…

会计 - 合并4 - 或有对价的会计处理

一、多次交易(构成一揽子交易)形成非同一控制下企业合并 构成一揽子交易的,在取得控制权时确认长期股权投资;取得控制权之前已支付的款项应作为预付投资款项(通常以”预付账款“科目核算)处理。 满足以下一种或多种情况的,通常应将多次交易事项作为“一揽子交易”进行会…

【HTTP三个基础问题】

面试官您好&#xff01;HTTP是超文本传输协议&#xff0c;是互联网上客户端和服务器之间传输超文本数据&#xff08;比如文字、图片、音频、视频等&#xff09;的核心协议&#xff0c;当前互联网应用最广泛的版本是HTTP1.1&#xff0c;它基于经典的C/S模型&#xff0c;也就是客…

NLP中的input_ids是什么?

在自然语言处理(NLP)中,input_ids 是什么 在自然语言处理(NLP)中,input_ids 是将文本转换为模型可处理的数字表示后的结果,是模型输入的核心参数之一。 一、基本概念 文本数字化 原始文本(如 “Hello world!”)无法直接被模型处理,需要通过分词器(Tokenizer) 将其…

⚡️ Linux Docker 基本命令参数详解

&#x1f433; Linux Docker 基本命令参数详解 &#x1f4d8; 1. Docker 简介 Docker 是一个开源的容器化平台&#xff0c;它通过将应用及其依赖打包到一个轻量级、可移植的容器中&#xff0c;从而实现跨平台运行。Docker 采用 C/S 架构&#xff0c;服务端称为 Docker Daemon&a…

Spring IoC 模块设计文档

注&#xff1a;码友们&#xff0c;我们是从设计的角度一步步学习和分解Spring&#xff1b;所以不要一上来就想看源码&#xff0c;也不需要关心Spring具体加载进去的&#xff1b;我们只封装工具&#xff08;如IoC&#xff09;&#xff0c;至于调用&#xff0c;暂时不用考虑&…

Linux(生产消费者模型/线程池)

目录 一 生产消费者模型 1. 概念&#xff1a; 2. 基于阻塞队列的生产消费者模型&#xff1a; 1. 对锁封装 2. 对条件变量封装 二 信号量(posix) 1. 概念 2. API 3. 基于环形队列的生产消费者模型 三 线程池 1. 概念 2. 示例 四 补充字段 1. 可重入函数 VS 线程安…

无线网络扫描与分析工具 LizardSystems Wi-Fi Scanner 25.05

—————【下 载 地 址】——————— 【​本章下载一】&#xff1a;https://pan.xunlei.com/s/VOS4QQ9APt3FgFQcxyArBiZlA1?pwdi4du# 【​本章下载二】&#xff1a;https://pan.xunlei.com/s/VOS4QQ9APt3FgFQcxyArBiZlA1?pwdi4du# 【百款黑科技】&#xff1a;https://uc…

Java Map完全指南:从基础到高级应用

文章目录 1. Map接口概述Map的基本特性 2. Map接口的核心方法基本操作方法批量操作方法 3. 主要实现类详解3.1 HashMap3.2 LinkedHashMap3.3 TreeMap3.4 ConcurrentHashMap 4. 高级特性和方法4.1 JDK 1.8新增方法4.2 Stream API结合使用 5. 性能比较和选择建议性能对比表选择建…

[最全总结]城市灾害应急管理系统

城市灾害应急管理集成系统 | 国家重点研发政府间合作项目 Vue+ElementUI+Bpmn+Cesium+Java SpringBoot 项目描述 在智慧城市战略背景下,项目面向内涝、团雾和火灾等灾害,开发了集灾害模型集成模拟、场景可视化与应急预案管理于一体的系统,系统各子模块进行软件功能测试,测…

QtWidgets模块功能及架构解析

QtWidgets 是 Qt 框架中用于创建传统桌面应用程序图形用户界面(GUI)的核心模块。在 Qt 6.0 中&#xff0c;QtWidgets 模块继续提供丰富的 UI 组件和功能&#xff0c;尽管 Qt 正在向 QML 方向演进&#xff0c;但 QtWidgets 仍然是许多桌面应用程序的基础。 一、主要功能 基础窗…

grep、wc 与管道符快速上手指南

&#x1f3af; Linux grep、wc 与管道符快速上手指南&#xff1a;从入门到实用 &#x1f4c5; 更新时间&#xff1a;2025年6月7日 &#x1f3f7;️ 标签&#xff1a;Linux | grep | wc | 管道符 | 命令行 文章目录 前言&#x1f31f; 一、grep、wc 和管道符简介1.核心功能2.核心…

C++11 右值引用:从入门到精通

文章目录 一、引言二、左值和右值&#xff08;一&#xff09;概念&#xff08;二&#xff09;区别和判断方法 三、左值引用和右值引用&#xff08;一&#xff09;左值引用&#xff08;二&#xff09;右值引用 四、移动语义&#xff08;一&#xff09;概念和必要性&#xff08;二…

java复习 04

心情复杂呢&#xff0c;现在是6.7高考第一天&#xff0c;那年今日此时此刻我还在考场挣扎数学&#xff0c;虽然结果的确很糟糕&#xff0c;&#xff0c;现在我有点对自己生气明明很多事情待办确无所事事没有目标&#xff0c;不要忘记曾经的自己是什么样子的&#xff0c;去年今日…

从零开始搭建 Pytest 测试框架(Python 3.8 + PyCharm 版)

概述 在软件开发中&#xff0c;自动化测试是确保代码质量的重要方式。而 Pytest 是一个功能强大且易于上手的 Python 测试框架&#xff0c;非常适合初学者入门。 本文将带你一步步完成&#xff1a; 安装和配置 Pytest在 PyCharm 中搭建一个清晰的测试项目结构 准备工作 在…

用电脑通过网口控制keysight示波器

KEYSIGHT示波器HD304MSO性能 亮点: 体验 200 MHz 至 1 GHz 的带宽和 4 个模拟通道。与 12 位 ADC 相比,使用 14 位模数转换器 (ADC) 将垂直分辨率提高四倍。使用 10.1 英寸电容式触摸屏轻松查看和分析您的信号。捕获 50 μVRMS 本底噪声的较小信号。使用独有区域触摸在几秒…

Java Smart 系统题库试卷管理模块设计:从需求到开发的实战指南

在教育信息化不断推进的背景下&#xff0c;高效的题库及试卷管理系统至关重要。Java Smart 系统中的题库及试卷管理模块&#xff0c;旨在为教师提供便捷的试题录入、试卷生成与管理功能&#xff0c;同时方便学生在线练习与考试。本文将详细介绍该模块的设计思路与核心代码实现。…

PDF图片和表格等信息提取开源项目

文章目录 综合性工具专门的表格提取工具经典工具 综合性工具 PDF-Extract-Kit - opendatalab开发的综合工具包&#xff0c;包含布局检测、公式检测、公式识别和OCR功能 仓库&#xff1a;opendatalab/PDF-Extract-Kit特点&#xff1a;功能全面&#xff0c;包含表格内容提取的S…