对于线程的思路
2026/4/27 19:47:13 网站建设 项目流程

这里我写这个文章,对学习线程相关知识的一个思路

这篇文章是一个目录类的文章。
沉淀的是,对于线程,要产生什么样的认识。
要学习哪些东西

以之前文章的思路为例子
psvm开启类里面的方法,是一个线程。
这样就是一个线程开启了。

我们在springboot中,是使用tomcat,接收http请求。
变成一个线程,执行完controller的方法以后
把数据响应给前端,结束这个线程。

这就是,我们写代码,要理解的第一点。
但是除此以为,我们要意识到threallocal的存在
就是我们可以利用threallocal提供的机制,
往线程里面放数据
这个有个demo案例就是
登录注册的时候,可以往线程中加入token

然后我们要了解的是线程相关的知识。
我们在一个线程执行的时候,在去开启另一个线程,执行异步操作步骤
或者使用线程池,执行,大批量的操作。

可以做一个比喻,cpu就是一个大石头,开启线程,就是招一个人,使用绳子把石头拉起来。
多开启几个线程,就多招几个人。
只要绳子没断,就可以招人

正常我们是用不到线程的,除非遇到异步,或者一个接口要执行几分钟的情况。

在然后。
如果数据,是在java中存在的。
由于java在底层操作系统代码执行的机制上,有问题。
数据并不安全,会出现超卖,并发的问题。

于是引入了锁和原子对象的概念。
我们还需要去了解锁的机制,和原子类的机制
以及超卖的机制这些。

才能写代码,如果代码是使用java层面的数据
不会导致数据不会出现问题。

先说个大概

关于线程,你需要建立这5层认知

不是教你背API,而是帮你建立一张完整的线程知识地图

写在前面

很多初学者学线程,上来就背synchronizedvolatileThreadPoolExecutor的7个参数,结果写代码时还是一脸懵。

其实,学习线程最关键的是建立正确的认知层次

这篇文章不打算面面俱到,而是用一条主线,帮你把线程相关的知识点串起来。每一层认知,都配一个最小可运行的代码示例,让你真正理解,而不是死记硬背。


第一层认知:线程就是一条执行路径

你其实每天都在用线程

先看最基础的代码:

publicclassMainThreadDemo{publicstaticvoidmain(String[]args){System.out.println("当前线程:"+Thread.currentThread().getName());// 输出:main}}

psvm(public static void main)本身就是主线程。你的代码从一开始就跑在一条线程上。

再看 Spring Boot 开发中的场景:

@RestControllerpublicclassUserController{@GetMapping("/user")publicUsergetUser(){// 这个方法的代码,跑在 Tomcat 工作线程里// 线程名类似:http-nio-8080-exec-1System.out.println(Thread.currentThread().getName());returnnewUser("张三",18);}}

Tomcat 接收到 HTTP 请求后,会从线程池里取一个空闲线程来执行 Controller 方法。方法执行完、响应返回给前端,这条线程并不会销毁,而是回到线程池等待下一个请求。

核心结论:你每天都在用线程,只是没意识到。Web 开发中,每个请求天然就是一个独立的线程。

这一层你需要记住

  • 代码永远跑在某个线程里
  • 学会用Thread.currentThread().getName()观察当前线程
  • 理解 Tomcat 线程模型:一个请求 → 一个线程

第二层认知:线程与数据绑定 —— ThreadLocal

为什么需要 ThreadLocal?

同一个请求处理过程中,可能要经过拦截器、Controller、Service、DAO 多个层级。如果有些数据(比如当前登录用户信息)需要在整个链路中共享,总不能每个方法都传参吧?

ThreadLocal的作用就是:给当前线程绑定一个专属变量,同一线程的任何地方都能拿到

一看就懂的示例

publicclassThreadLocalDemo{// 创建一个 ThreadLocal,泛型指定存储的数据类型privatestaticThreadLocal<String>currentUser=newThreadLocal<>();publicstaticvoidmain(String[]args){// 主线程设置数据currentUser.set("张三");// 同一个线程里,任何地方都能拿到System.out.println("主线程获取:"+currentUser.get());// 张三// 新线程拿不到主线程的数据(线程隔离)newThread(()->{System.out.println("子线程获取:"+currentUser.get());// null}).start();}}

真实项目用法:登录拦截器

// 1. 定义一个用户上下文工具类publicclassUserContext{privatestaticThreadLocal<String>tokenHolder=newThreadLocal<>();publicstaticvoidsetToken(Stringtoken){tokenHolder.set(token);}publicstaticStringgetToken(){returntokenHolder.get();}// 关键:用完要清理,防止内存泄漏publicstaticvoidremove(){tokenHolder.remove();}}// 2. 拦截器中存入用户信息publicclassLoginInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){Stringtoken=request.getHeader("Authorization");UserContext.setToken(token);returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){UserContext.remove();// 请求结束,一定要清理}}// 3. Controller 中直接获取@RestControllerpublicclassUserController{@GetMapping("/info")publicStringgetInfo(){Stringtoken=UserContext.getToken();return"当前用户token:"+token;}}

这一层你需要记住

  • ThreadLocal 实现线程级别的数据隔离
  • 每个线程都有自己的副本,互不影响
  • 用完必须 remove(),否则在线程池场景下会导致内存泄漏

第三层认知:主动创建线程 —— 异步 & 线程池

什么时候需要自己开线程?

正常情况下,一个请求对应一个 Tomcat 工作线程,你不需要手动创建。但遇到以下场景,就需要主动创建线程:

  • 异步操作:发短信、写日志、推送消息,不想阻塞主流程
  • 耗时接口:接口要执行几秒钟,不能等,先返回「处理中」
  • 批量处理:大量数据要计算,多线程并行加速

比喻理解

想象一块大石头(CPU),你要把它拉走:

  • 单线程:1 个人拉
  • 多线程:多找几个人一起拉
  • 线程池:提前雇好一群人,随时安排任务,省去招人、辞退的成本

只要绳子(程序)不断,你可以随时加人(开线程)。

方式一:直接创建线程(不推荐)

publicclassSimpleThreadDemo{publicstaticvoidmain(String[]args){System.out.println("主线程开始:"+Thread.currentThread().getName());// 创建并启动一个新线程newThread(()->{System.out.println("子线程执行:"+Thread.currentThread().getName());// 模拟耗时操作try{Thread.sleep(1000);}catch(InterruptedExceptione){}System.out.println("子线程结束");}).start();System.out.println("主线程结束(不等待子线程)");}}

输出顺序:

主线程开始:main 主线程结束(不等待子线程) 子线程执行:Thread-0 子线程结束

方式二:线程池(强烈推荐)

importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassThreadPoolDemo{publicstaticvoidmain(String[]args){// 创建一个固定大小的线程池,里面预置了 3 个线程ExecutorServicepool=Executors.newFixedThreadPool(3);// 提交 10 个任务,这 3 个线程会轮流执行for(inti=1;i<=10;i++){finalinttaskId=i;pool.submit(()->{StringthreadName=Thread.currentThread().getName();System.out.println(threadName+" 正在执行任务 "+taskId);try{Thread.sleep(500);}catch(InterruptedExceptione){}});}pool.shutdown();// 关闭线程池(不再接收新任务)}}

为什么要用线程池?

  • 复用线程,避免频繁创建销毁的开销
  • 控制并发数量,防止资源耗尽

这一层你需要记住

  • 异步 ≠ 多线程,但多线程是实现异步的主要方式
  • 永远不要用new Thread(),一律使用线程池
  • 线程池用完要shutdown()

第四层认知:线程带来新问题 —— 并发安全

什么是并发安全问题?

当多个线程同时访问同一份共享数据时,由于 Java 底层的内存模型和 CPU 指令重排序,会出现预料之外的结果。

最经典的例子:超卖

publicclassUnsafeSellDemo{// 共享数据:库存privatestaticintstock=10;publicstaticvoidmain(String[]args)throwsInterruptedException{// 模拟 100 个用户同时下单for(inti=0;i<100;i++){newThread(()->{if(stock>0){// 模拟数据库查询、库存扣减等耗时try{Thread.sleep(10);}catch(InterruptedExceptione){}stock--;System.out.println(Thread.currentThread().getName()+" 购买成功,剩余:"+stock);}else{System.out.println(Thread.currentThread().getName()+" 购买失败,库存不足");}}).start();}Thread.sleep(3000);System.out.println("最终库存:"+stock);// 可能是负数!!!}}

运行结果可能是:

Thread-1 购买成功,剩余:9 Thread-2 购买成功,剩余:8 ... Thread-99 购买成功,剩余:-5 ← 超卖了!

为什么会发生?

多个线程同时读取到stock > 0,都认为有库存,然后各自扣减。最终导致扣减次数超过初始库存。

这不是你代码写得不好,而是多线程环境下 Java 底层的行为不同

  • 可见性问题:一个线程修改了变量,另一个线程看不到
  • 原子性问题stock--不是一步操作,而是“读-改-写”三步
  • 有序性问题:CPU 可能重排序指令

这一层你需要记住

  • 共享 + 可变 + 多线程 = 并发安全问题
  • 三个特征缺一不可,缺一个就是安全的
  • 出现问题时的现象:数据不一致、超卖、死循环、程序卡死

第五层认知:解决并发问题的手段

手段一:synchronized(内置锁)

publicclassSafeSellDemo1{privatestaticintstock=10;// 加锁方法:同一时刻只有一个线程能进入publicstaticsynchronizedvoidsell(){if(stock>0){try{Thread.sleep(10);}catch(InterruptedExceptione){}stock--;System.out.println(Thread.currentThread().getName()+" 购买成功,剩余:"+stock);}}publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<100;i++){newThread(SafeSellDemo1::sell).start();}Thread.sleep(3000);System.out.println("最终库存:"+stock);// 正确:0}}

synchronized保证:同一时刻最多一个线程执行该方法,其他线程必须等待。

手段二:原子类(Atomic)

importjava.util.concurrent.atomic.AtomicInteger;publicclassSafeSellDemo2{privatestaticAtomicIntegerstock=newAtomicInteger(10);publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<100;i++){newThread(()->{// CAS 操作:原子的减1intleft=stock.decrementAndGet();if(left>=0){System.out.println(Thread.currentThread().getName()+" 购买成功,剩余:"+left);}else{System.out.println(Thread.currentThread().getName()+" 购买失败");}}).start();}Thread.sleep(3000);System.out.println("最终库存:"+stock.get());// 正确:0}}

原子类底层使用 CAS(Compare-And-Swap)机制,比锁的性能更好。

手段三:并发容器

importjava.util.concurrent.ConcurrentHashMap;publicclassConcurrentMapDemo{privatestaticConcurrentHashMap<String,Integer>map=newConcurrentHashMap<>();publicstaticvoidmain(String[]args)throwsInterruptedException{// 多线程同时 put,不会出问题for(inti=0;i<100;i++){finalintid=i;newThread(()->{map.put("key"+id,id);}).start();}Thread.sleep(1000);System.out.println("size = "+map.size());// 正确:100}}
  • ConcurrentHashMap:线程安全的 HashMap
  • BlockingQueue:线程安全的队列,常用于生产者-消费者模式

这一层你需要记住

场景解决方案
需要互斥执行代码块synchronized/ReentrantLock
一个变量的原子操作AtomicInteger/LongAdder
线程安全的 MapConcurrentHashMap
线程安全的 ListCopyOnWriteArrayList
线程安全的队列BlockingQueue

核心原则:共享的可变数据,必须用并发手段保护。


进阶认知(可选):线程的生命周期与状态

了解线程的六种状态,能帮你快速定位问题(比如为什么程序卡住了?哪个线程在等待?)。

publicclassThreadStateDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Objectlock=newObject();Threadt=newThread(()->{synchronized(lock){try{System.out.println("子线程:拿到锁,开始睡觉");Thread.sleep(5000);System.out.println("子线程:睡醒了");}catch(InterruptedExceptione){}}});System.out.println("状态1(刚创建): "+t.getState());// NEWt.start();System.out.println("状态2(启动后): "+t.getState());// RUNNABLEThread.sleep(100);System.out.println("状态3(sleep中): "+t.getState());// TIMED_WAITING// 主线程尝试获取同一把锁,会被阻塞synchronized(lock){System.out.println("主线程:拿到锁了");}t.join();// 等待子线程结束System.out.println("状态4(结束): "+t.getState());// TERMINATED}}

线程状态转换图(记住这个就够了):

NEW → RUNNABLE → TERMINATED ↓ WAITING / TIMED_WAITING / BLOCKED

两种常用排查手段

  1. jstack 命令:打印线程堆栈,看哪些线程在等待锁
  2. Arthas 工具thread命令实时查看线程状态

学习检查清单

学完以上内容,你可以自测一下:

  • 我能说出当前代码运行在哪个线程吗?
  • 我知道什么时候该用线程池,而不是new Thread
  • 面试官问 ThreadLocal,我能说出原理+内存泄漏问题吗?
  • 我在写共享变量时,会本能想到加锁或原子类吗?
  • 我能区分synchronizedAtomicInteger的使用场景吗?
  • 我看得懂一次简单的线程 dump 吗?

最后一句总结

学习线程,不是学习API,而是建立一种「并发脑」:

看到共享数据 → 本能想到安全问题
看到耗时操作 → 本能想到异步
看到线程 → 本能想到生命周期和资源释放

把这5层认知理清楚,你就已经超越了80%只会背八股文的开发者。剩下的,就是在项目中遇到并发问题时,回来查缺补漏。


这篇文章是一个认知框架,不是完整的手册。建议收藏,每遇到一次线程相关问题,就回来看一眼对应的层级。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询