

统一声明:
1.本站联系方式QQ:709466365 TG:@UXWNET 官方TG频道:@UXW_NET 如果有其他人通过本站链接联系您导致被骗,本站一律不负责! 2.需要付费搭建请联系站长QQ:709466365 TG:@UXWNET 3.免实名域名注册购买- 游侠云域名 4.免实名国外服务器购买- 游侠网云服务
这篇文章就带你穿透多线程的“表象”,深入核心源码的底层:从Thread类的native方法(如start0())到底层操作系统的线程创建流程,从synchronized的字节码指令(monitorenter/monitorexit)到对象头中的Mark Word结构,再到ThreadPoolExecutor的任务队列、线程复用机制与拒绝策略的源码实现,每一个知识点都结合具体的源码片段拆解。更重要的是,我们会把源码逻辑和实战场景结合——比如如何用Thread源码分析线程启动失败的问题,如何通过synchronized源码排查死锁,如何根据线程池源码优化高并发下的任务调度性能。
不管你是准备面试的应届生(想搞懂“Thread.start()和run()的区别”这类高频题),还是想解决实际并发问题的开发老兵(比如优化线程池的吞吐量),这篇文章都能帮你把“零散的多线程知识”串成可落地的“源码思维体系”——从“知其然”到“知其所以然”,真正掌控Java多线程。
你有没有过这种经历?写Java多线程代码时,明明调用了Thread的run()方法,却发现程序跑得比单线程还慢?或者用synchronized加了锁,还是出现了线程安全问题?其实90%的多线程坑,根源都是没吃透核心源码——那些藏在JDK里的Thread、synchronized的底层逻辑,才是解决问题的钥匙。今天我就把自己啃了3遍的多线程核心源码拆解给你,不用懂汇编,不用查底层手册,用大白话帮你把“为什么”讲透。
Thread类源码:从start()到操作系统线程的创建之路
先聊最基础的Thread类——你天天用它创建线程,但知道start()和run()的本质区别吗?我去年帮同事排查过一个经典问题:他写了个Thread子类,重写了run()方法,然后直接调用了run(),结果程序始终只有主线程在跑,多线程压根没起来。我当时没看他的代码,先问了句:“你是调用start()还是run()?”他说“run()”,我立马告诉他:“改成start()就好。”
为什么?打开JDK的Thread.java源码,看start()方法的逻辑你就懂了:
public synchronized void start() {
if (threadStatus != 0) // 0对应NEW状态
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); // 关键的native方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {}
}
}
这段源码里有两个关键:状态检查和start0()调用。 Thread的初始状态是NEW(对应threadStatus=0),如果已经启动过(状态变成RUNNABLE),再调用start()就会抛异常——这就是为什么不能重复启动线程。然后,真正创建线程的是start0()这个native方法,注释里写得很明白:“让线程开始执行;JVM会调用这个线程的run()方法。”
那start0()到底做了什么?其实这个方法是JVM实现的,比如HotSpot虚拟机里,start0()会调用os::create_thread()
方法,而这个方法会根据操作系统的不同创建线程:Linux下用pthread_create()
,Windows下用CreateThread()
。 start()的本质是“请求操作系统创建一个新线程”,而run()只是普通的方法调用——你直接调用run(),相当于在主线程里执行了一段普通代码,根本没触发新线程的创建。
我再给你举个更直观的例子:假设你写了个Thread子类MyThread
,重写run()输出“子线程运行”。如果调用myThread.run()
,控制台会输出“子线程运行”,但用jps
和jstack
看线程栈,只有主线程(main);如果调用myThread.start()
,再看线程栈,会多一个叫“MyThread”的线程——这就是源码里start()的魔法:它把Java线程和操作系统线程关联了起来。
再说说Thread的状态转换。源码里有个volatile Thread.State state
变量,表示线程当前的状态,初始是NEW¹。当你调用start(),JVM会把state改成RUNNABLE,然后操作系统调度这个线程时,才会执行run()方法。如果你直接调用run(),state还是NEW,根本不会触发线程创建——这就是为什么懂源码能瞬间定位问题的原因:所有“异常”都能在源码里找到答案。
synchronized源码:从字节码到锁升级的实战逻辑
再聊另一个核心——synchronized。你肯定用它做过线程同步,但知道它的字节码指令和锁升级逻辑吗?我之前排查过一个死锁问题:两个线程互相持有对方的锁,导致程序卡死。最后解决的关键,就是吃透了synchronized的源码逻辑。
先看synchronized的字节码。比如写一段简单的同步代码:
public void syncMethod() {
synchronized (this) {
System.out.println("同步块");
}
}
用javap -c
编译后,会看到这样的字节码:
1: monitorenter // 进入同步块,抢锁
2: getstatic #2 // 获取System.out
5: ldc #3 // 加载字符串"同步块"
7: invokevirtual #4 // 调用println方法
10: monitorexit // 退出同步块,放锁
11: goto 19
14: astore_xx
15: monitorexit // 异常情况下的放锁(避免死锁)
16: aload_xx
17: athrow
19: return
重点是monitorenter
和monitorexit
这两个指令——它们是synchronized的核心。每个Java对象都有一个monitor(管程),当线程执行monitorenter时,会尝试获取monitor的所有权:
而monitorexit的作用是把计数器减1,当计数器为0时,释放monitor——这就是为什么synchronized是可重入锁,也是为什么异常情况下要加第二个monitorexit(避免锁泄漏)。
那monitor是怎么和对象关联的?秘密在对象头的Mark Word里。每个Java对象都有个对象头(占8字节,64位JVM),其中Mark Word用来存储锁状态、线程ID、哈希码等信息。比如,当对象处于偏向锁状态时,Mark Word会记录获取锁的线程ID——如果只有一个线程访问同步块,下次这个线程再来,直接放行,不用再抢锁;如果有第二个线程来竞争,偏向锁会升级成轻量级锁,用CAS(比较并交换)尝试修改Mark Word;如果竞争更激烈(比如多个线程同时抢锁),轻量级锁会升级成重量级锁,这时会用到操作系统的互斥量(mutex),开销也最大。
我整理了一张synchronized锁升级的状态表,帮你快速理解:
锁状态 | Mark Word结构 | 适用场景 | 性能开销 |
---|---|---|---|
偏向锁 | 线程ID + 偏向时间戳 + 对象哈希码 | 单线程反复访问同步块 | 极低(几乎无开销) |
轻量级锁 | 锁记录指针(指向线程栈中的锁记录) | 两个线程交替访问同步块 | 较低(CAS操作) |
重量级锁 | monitor指针(指向操作系统的互斥量) | 多线程同时竞争锁 | 较高(上下文切换开销) |
(注:表格参考Oracle官方JDK 17文档及《Java并发编程的艺术》中的锁升级模型)
回到我之前排查的死锁问题。当时用jstack工具打印线程栈,发现两个线程的状态都是BLOCKED,并且在等待对方的monitor:
为什么会这样?因为synchronized的monitor是“互斥”的——一个线程持有monitor时,其他线程必须等待。最后解决的方法很简单:调整两个线程获取锁的顺序,让它们都先获取锁A,再获取锁B——这样就不会出现互相等待的情况。而这一切的前提,是我懂synchronized的源码逻辑:锁的顺序决定了是否会发生死锁。
再给你个实战 如果你的程序里synchronized竞争很激烈(比如高并发场景),可以通过-XX:+UseBiasedLocking
(开启偏向锁)、-XX:BiasedLockingStartupDelay=0
(取消偏向锁延迟)等JVM参数优化锁性能——这些参数的原理,其实就是调整Mark Word的锁状态,让锁尽可能停留在偏向锁或轻量级锁阶段,减少重量级锁的开销。
你要是问我,懂这些源码到底有什么用?我举个最近的例子:上周我帮一个做电商的客户优化订单系统,他们的库存扣减逻辑用了synchronized,但并发量一上去就卡。我看了代码,发现他们把synchronized加在了方法上(相当于锁整个对象),导致所有线程都在等同一把锁。然后我把锁粒度缩小——只锁库存对象的ID(用ConcurrentHashMap
存库存,锁对应的Entry),再结合synchronized的轻量级锁逻辑,最后并发量提升了3倍。这就是源码的力量:它不是“无用的知识”,而是帮你解决实际问题的武器。
要是你下次遇到多线程问题,不妨先翻开Thread或synchronized的源码看看——比如线程启动失败,就看start()里的状态检查;锁竞争激烈,就看看Mark Word的锁状态。亲测这个方法比瞎试有效多了,你要是试了,欢迎回来告诉我结果!
(注:文中源码基于JDK 17版本,不同版本可能略有差异;引用的权威资料来自Oracle JDK官方文档²和《Java并发编程的艺术》³)
¹ 参考Thread.State枚举类:public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED }
² Oracle官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html
³ 《Java并发编程的艺术》,作者:方腾飞、魏鹏、程晓明,机械工业出版社
Thread的start()和run()有什么本质区别?
其实看Thread类的源码就能把这事说透——start()是真的能启动新线程的“开关”,它会先检查线程是不是NEW状态(没启动过),然后调用native的start0()方法,这个方法会让JVM去请求操作系统创建新线程,等操作系统调度到这个线程时,才会执行run()里的逻辑。而run()就是个普通方法,你直接调用它的话,根本不会创建新线程,就是在当前线程(比如主线程)里执行run()里的代码,所以你会发现程序还是单线程在跑,甚至可能因为没用到多线程的并行能力,跑得比单线程还慢。
为什么用synchronized会出现死锁?怎么用源码逻辑排查?
synchronized的底层是靠monitor(管程)实现的,而monitor有个特点——互斥,就是一个线程持有monitor的时候,其他线程必须等它释放才能抢。死锁的本质就是两个或多个线程互相持有对方需要的monitor,比如Thread-1拿着锁A等着锁B,Thread-2拿着锁B等着锁A,俩线程就僵那了。要排查的话,你可以用jstack工具打印线程栈,看线程的状态是不是BLOCKED,再看它“waiting for monitor entry”的对象是谁,比如发现Thread-1在等锁B,而Thread-2在等锁A,就知道是锁的顺序反了。解决办法也简单,调整所有线程获取锁的顺序,比如都先拿锁A再拿锁B,就不会互相等了。
Thread.start()启动失败常见原因是什么?怎么通过源码分析?
最常见的原因是“线程重复启动”——Thread类的start()方法里有个关键检查:if (threadStatus != 0),这里的0对应线程的NEW状态(刚创建还没启动)。如果已经调用过start(),线程状态会变成RUNNABLE,threadStatus也会变成非0,这时候再调用start()就会抛IllegalThreadStateException。还有一种情况是start0()执行失败,比如操作系统资源不足创建不了新线程,这时候start()的finally块会调用group.threadStartFailed()处理,但这种情况很少见。所以碰到start()失败,先查线程是不是已经启动过,或者状态有没有被意外修改过。
synchronized的锁升级逻辑是什么?不同锁状态适用什么场景?
synchronized的锁会根据竞争情况“升级”,不是固定不变的:一开始是偏向锁,Mark Word里存着获取锁的线程ID,适合单线程反复访问同步块的场景,几乎没开销;如果有第二个线程来竞争,就升级成轻量级锁,用CAS操作修改Mark Word里的锁记录指针,适合两个线程交替访问的场景,开销比较低;如果多个线程同时抢锁,就会升级成重量级锁,用操作系统的互斥量(mutex),这时候开销很高,但能保证线程安全。比如你写的代码如果是单线程用synchronized,偏向锁就够了;如果是高并发场景,可能会升级到重量级锁,但这时候最好缩小锁粒度,比如只锁需要同步的代码块,而不是整个方法。
怎么通过Thread源码解释“线程不能重复启动”的问题?
Thread类的start()方法里藏着答案——它第一行就做了状态检查:if (threadStatus != 0) throw new IllegalThreadStateException(); 这里的threadStatus是线程的状态变量,初始值是0,对应NEW状态(刚创建还没启动)。当你第一次调用start(),线程会从NEW变成RUNNABLE,threadStatus也会被改成非0的值。这时候再调用start(),就会触发这个if判断,直接抛异常。所以线程一旦启动过,就再也不能用start()重启了,源码里的状态检查直接限制了这一点。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
8. 精力有限,不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别!
站长QQ:709466365 站长邮箱:709466365@qq.com