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

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锁进行优化,存在四种状态:偏向锁、轻量级锁、重量级锁、无锁,状态随竞争情况升级(不能降级,单向升级 )。

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

相关文章:

  • C++与Python编程体验的多维对比:从语法哲学到工程实践
  • MATLAB-电偶极子所产出的电磁场仿真
  • 【HarmonyOS5】UIAbility组件生命周期详解:从创建到销毁的全景解析
  • Linux -- 进程信号
  • LVDS的几个关键电压概念
  • libiec61850 mms协议异步模式
  • Android实现点击Notification通知栏,跳转指定activity页面
  • 轮廓 填充空洞 删除孤立
  • 记录下three.js学习过程中不理解问题①
  • Springboot项目中minio的使用场景、使用过程(仅供参考)
  • python调用其它程序 os.system os.subprocess
  • 深入浅出Docker
  • 7.2.2_折半查找
  • SQL字符串截取函数全解析:LEFT、RIGHT、SUBSTRING 实战指南
  • 一个简单的德劳内三角剖分实现
  • 湖北理元理律师事务所:债务咨询中的心理支持技术应用
  • IP地址(互联网中设备的唯一逻辑地址标识)
  • ps蒙版介绍
  • EMD算法
  • 移动应用开发专业核心课程以及就业方向
  • 广州网站建设星珀/上海aso苹果关键词优化
  • 手机网站集成支付宝/app如何推广
  • JavaScript做的网站/seo外包顾问
  • 昆明市城乡建设局网站/学网络营销好就业吗
  • 福州优秀网站建设公司/长春百度网站快速排名
  • 成都市城乡建设管理局网站/说到很多seo人员都转行了