Java多线程从入门到线程安全

博客简介:本文从零梳理Java线程基础、线程核心API、线程生命周期,再深入讲解并发安全、synchronized锁原理、死锁问题。

一、基础概念:进程 vs 线程

  1. 进程:操作系统资源分配的最小单位。一个运行起来的程序就是进程,拥有独立内存空间;进程之间互相隔离,进程切换开销很大。
  2. 线程 :进程内部的执行单元,是CPU调度的最小单位。一个进程可以包含多个线程,同进程内的线程共享进程资源(堆、方法区),线程切换开销远小于进程。

一句话理解:进程是工厂,线程就是工厂里的工人;工厂资源所有工人共用,工人之间独立干活。

二、线程创建方式

Java里创建线程常见4种写法:

  1. 继承Thread类,重写run()
  2. 实现Runnable接口,传给Thread
  3. 实现Callable + FutureTask(可以获取返回值、抛异常)
  4. 线程池(生产环境推荐,后面拓展学习)

注意:调用thread.start()才会真正创建操作系统线程;直接调用run()只是普通方法调用,不会开启新线程。

守护线程(Daemon)

  • 用户线程:普通业务线程,JVM必须等所有用户线程执行完毕才会退出。
  • 守护线程:后台服务线程(GC垃圾回收就是守护线程)。当所有用户线程结束,不管守护线程有没有跑完,JVM直接退出。
java 复制代码
thread.setDaemon(true); // 设置守护线程,必须在start()之前调用

三、核心API:sleep / join / interrupt

1. sleep()

Thread.sleep(long ms):让当前线程休眠指定时间,不会释放锁

  • 休眠期间线程进入TIMED_WAITING,时间到后重新参与CPU调度,不一定立刻执行。
  • 会抛出InterruptedException中断异常;线程在sleep时收到中断标记,直接抛出异常、清除中断标志。

重点:sleep只是让出CPU,锁依旧持有!和wait()有本质区别,wait会释放锁。

2. join()

等待另一个线程执行完毕。主线程调用t.join(),主线程阻塞,直到t线程执行结束才继续运行。

java 复制代码
Thread t = new Thread(() -> { ... });
t.start();
t.join(); // 主线程在这里等待t执行完

支持带超时时间join(long timeout):最多等待指定毫秒,超时不等了。

3. interrupt 线程中断

不是强制终止线程,只是给线程打一个中断标记

  1. interrupt():给目标线程设置中断标志位
  2. isInterrupted():判断是否被中断,不清除标记
  3. Thread.interrupted():判断中断,会清除中断标记

当线程处于sleep/wait/join阻塞状态,调用interrupt会直接抛InterruptedException,并且清空中断标记。

最佳实践:捕获异常后,业务自行决定是否终止线程,而不是直接结束。

四、Java线程6大状态 & 状态流转

Java线程一共6种状态,定义在Thread.State枚举:

  1. NEW:新建,new Thread之后,还没start()
  2. RUNNABLE:可运行,包含【正在CPU执行】和【等待CPU分配时间片】两种情况
  3. BLOCKED:阻塞,等待synchronized锁,拿不到锁就进入这个状态
  4. WAITING :无限等待,调用wait()/join()无参版本,需要别的线程唤醒
  5. TIMED_WAITING:限时等待,sleep、带超时wait/join,时间到自动唤醒
  6. TERMINATED:线程执行完毕,终止

状态流转简要: NEW → start() → RUNNABLE RUNNABLE遇到sleep/wait(time)TIMED_WAITING RUNNABLE遇到无参wait/join → WAITING 抢不到synchronized锁 → BLOCKED 线程代码执行完 / 异常退出 → TERMINATED

重点区分: BLOCKED:仅仅抢锁失败;WAITING/TIMED_WAITING是主动放弃CPU等待唤醒。

五、并发带来的问题:线程安全

多个线程同时操作共享可变变量 ,就会出现线程安全问题。 经典例子:两个线程同时对count做count++ count++本质3条CPU指令:load读取内存 → add自增 → save写回内存。 线程切换会打断这个过程,出现指令交错,最终结果小于预期。

产生线程安全3个必要条件(全部满足才会出问题):

  1. 多线程并发执行
  2. 共享可变资源(多个线程访问同一个变量)
  3. 对资源存在非原子操作(操作不是一步完成,可以被打断)

解决思路:破坏任意一个条件即可。最常用:加锁,保证操作原子性。

六、synchronized 内置锁

基础用法

  1. 修饰实例方法:锁对象是this
  2. 修饰静态方法:锁对象是类的Class对象
  3. 同步代码块:手动指定锁对象 synchronized(锁对象){}

核心特性:

  1. 互斥:同一时刻,锁对象只能被一个线程持有;其他线程抢锁失败进入BLOCKED阻塞。
  2. 内存可见性:保证锁内修改,对后续拿到同一锁的线程立即可见(类似volatile)
  3. 可重入:同一个线程,已经拿到锁,再次获取同一个锁不会死锁(锁计数+1,退出计数-1)

锁升级(偏向锁 → 轻量级锁 → 重量级锁)

JDK优化,synchronized不是一开始就是重量级OS锁,会动态升级:

  1. 偏向锁:单线程无竞争,偏向第一个拿到锁的线程,几乎无开销。
  2. 轻量级锁:少量线程交替竞争,用自旋(循环CAS)尝试拿锁,不挂起线程。
  3. 重量级锁 :多线程激烈竞争,向操作系统申请互斥锁,抢不到的线程阻塞。 锁只能升级,不能降级

七、死锁

死锁四个必要条件(必须同时满足)

  1. 互斥:资源是独占锁,一个线程持有,别人拿不到
  2. 占有且等待:线程拿着自己的锁,同时请求别人手里的锁
  3. 不可抢占:锁不能被强行抢走,只能持有者主动释放
  4. 循环等待:线程形成环形等待,A拿锁1等锁2,B拿锁2等锁1

只要破坏任意一条,就能避免死锁。最简单方案:统一锁获取顺序,所有线程申请锁顺序保持一致。

死锁示例:线程A持有lock1,等待lock2;线程B持有lock2,等待lock1,互相卡死。

八、volatile关键字

volatile不能保证原子性(不能解决count++这种复合操作),只保证两点:

  1. 内存可见性:一个线程修改volatile变量,其他线程立刻感知,不会停留在CPU缓存。
  2. 禁止指令重排:防止JVM、CPU为优化打乱代码执行顺序。

适用场景:状态标记、开关变量(布尔标记),适合读多写少场景。

易错点:volatile无法替代synchronized,不能保护复合操作。

九、补充:wait() 和 sleep() 对比

sleep wait
所属类 Thread Object
不释放锁 释放锁
唤醒 时间到自动唤醒 需要notify/notifyAll唤醒
状态 TIMED_WAITING WAITING / TIMED_WAITING
使用位置 任意地方 必须写在synchronized同步代码块内

十、总结

多线程核心难点:不是开启线程,而是控制并发安全

  1. 先分清线程状态、基础API
  2. 理解线程安全产生的底层原理(指令交错、缓存)
  3. 掌握synchronized锁原理、锁升级、死锁
  4. 拓展学习:线程池、Lock锁(AQS)、CAS、ThreadLocal
相关推荐
gis开发之家1 小时前
Spring Boot 4 定时任务与异步线程池:从 @Scheduled 到高并发任务编排(生产级实战)
java·spring boot·后端·wpf·spring boot4
郑州光合科技余经理2 小时前
本地生活系统:多业务订单字段怎么分账本导出
java·开发语言·前端·数据库·uni-app·php·ai编程
weixin199701080163 小时前
《跨境二手ERP的3种对接模式:自研API / SaaS中间件 / 平台认证服务商,怎么选?》(附Python源码)
开发语言·python·中间件
YangYang9YangYan3 小时前
2026 校招审计风控岗位 JD 拆解,工具、专业能力与面试考点
java·大数据·人工智能·数据分析
传奇开心果编程5 小时前
【Rust入门知识点学与练】第24课:Trait 基础
开发语言·学习·rust
wuminyu8 小时前
Kafka中sendfile与mmap实现机制解析
java·linux·c语言·jvm·c++
考虑考虑10 小时前
Java实现hmacsha256加密算法
java·后端·java ee
captain37610 小时前
▲网络原理(2)-TCP
java·服务器·网络·tcp/ip·java-ee
MetaLite10 小时前
SpringBoot接口分层规范-外网网关内部服务与参数边界
java·数据库·spring boot