尧图精选

Java线程从基础到实践:状态、锁、线程池与虚拟线程选型详解

🕒 发布时间:2026/9/26 21:23:34 📁 来源:尧图网络
做Java开发这些年凡是和并发沾边的问题最后几乎都绕回到线程这个根上。很多同学把“多线程”想得很简单不就是new个Thread再start一下吗可真写起来才发现线程状态怎么切、锁该用哪种、线程池参数怎么定、死锁怎么排查每一步都有坑等着。这篇就把Java线程基础从头到尾捋一遍重点讲清楚线程的实现方式、控制手段、同步互斥以及现代JDK里线程池和虚拟线程的实践选型。适合两类人准备面试的中级开发以及平时写并发代码总出隐蔽问题的实战派。1. 线程基础先把进程、线程和内存模型理清1.1 进程和线程的边界用厨房打个比方早期操作系统只有进程概念一个进程就是一个正在运行的程序实例有自己的内存空间、文件句柄、环境变量。后来发现进程太“重”了创建、切换、销毁成本都高而且进程间通信麻烦。于是线程出现了一个进程内部可以拆出多个执行流共享进程的内存和资源但每条执行流有自己独立的栈和寄存器状态。我用厨房来类比进程就是整个餐厅有自己的菜单、食材仓库、灶台设备线程就是在厨房里干活的厨师。多个厨师共享同一个食材仓库这就是共享内存每个厨师手里有自己的铲子和围裙这就是线程私有的栈和局部变量。餐厅需要接待不同客人不可能每次来客人都重新装修厨房所以进程内部多开几个“厨师”执行流成本远低于再开一家新餐厅。这个类比在面试里特别好用因为很多后续问题都从这里延伸为什么线程能共享数据因为同一个进程里的线程看到的是同一份堆内存而每个线程又有一份独立的调用栈。为什么说线程更轻量因为线程切换只需要保存和恢复寄存器、程序计数器、栈指针不涉及虚拟内存空间的切换。1.2 线程的组成不只是“一段代码”热词里有“线程控制块和私有存储区的关系 线程的组成”这其实是一个非常经典的底层考点。Java线程跑起来后在操作系统层面会有一个线程控制块TCB它记录了这个线程的运行时信息线程ID、状态、调度优先级、寄存器内容、程序计数器、栈指针等。可以将TCB理解为厨师的工牌工牌上写着这个厨师现在在哪个灶台、做到哪道菜了。Java线程本身的组成至少要清楚三层第一层是Thread对象本身它包含线程名、优先级、是否为守护线程、ThreadLocal值等Java层面的属性以及一个native的线程句柄。第二层是JVM层面的线程数据包括Java虚拟机栈里面存放栈帧每个栈帧对应一个未执行完的方法包含局部变量表、操作数栈、动态链接、返回地址。还有程序计数器记录当前执行到哪条字节码指令本地方法执行时计数器是undefined。第三层是操作系统层面的线程数据结构就是前面说的TCB。JVM的线程本质是“1:1映射到内核线程”也就是说每个Java线程背后都有一个操作系统线程由内核负责调度。理解了这一层你就明白为什么大量创建线程会导致系统资源耗尽因为每一个都对应着内核资源。1.3 五态模型与生命周期别和教科书搞混Java线程的官方状态有六种NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。但操作系统教科书里讲的是“新建、就绪、运行、阻塞、终止”五态很多人会把它和Java线程状态一一对应其实对不上。我把Java的六态和实际操作对应起来你感受一下NEWThread对象new出来了但还没调用start()。此时线程对象存在底层线程还没创建。RUNNABLE调用start()之后线程进入可运行状态。注意Java的RUNNABLE把操作系统里的“就绪”和“运行”合并了因为JVM不关心是正在跑还是等CPU只要没有阻塞等待都是RUNNABLE。BLOCKED线程卡在synchronized上没拿到monitor锁等待别人释放锁。WAITING线程调用了wait()、join()无参版本、LockSupport.park()等待被notify或unpark唤醒没有超时概念。TIMED_WAITING带超时的等待sleep(1000)、wait(1000)、join(1000)、parkNanos都在这一状态。TERMINATED线程run()方法执行完或者抛异常退出线程生命周期结束此时线程对象虽在但底层线程已销毁。实操中容易踩的坑就是通过getState()调试时觉得状态“不对”。比如线程调用sleep()在Java里是TIMED_WAITING不是BLOCKED又比如线程正在执行大量计算getState()返回RUNNABLE但你一看CPU占用率很低这其实是它在等待I/OJVM眼里它仍然是可运行的因为阻塞在系统调用层面这个细节做性能分析时要特别注意。2. 线程的创建与实现方式四种写法一套思路这是整个主题的核心章节。Java官方给我们的线程实现方式最初是两种继承Thread、实现Runnable。后来JDK 5加了Callable和FutureTaskJDK 8之后可以用CompletableFuture做异步编排再往后还有虚拟线程。但面试和实战里基础始终是前四种。2.1 继承Thread类简单直接但别在业务里用继承Thread创建线程的核心逻辑是写一个子类重写run()方法然后new这个子类再调start()。注意你必须调start()而不是直接调run()。这是一个高频面试题直接跑run()只是普通方法调用还是在当前线程里顺序执行根本没有新线程。public class MyThread extends Thread { Override public void run() { System.out.println(当前线程 Thread.currentThread().getName()); } } // 使用 MyThread t new MyThread(); t.start();为什么我不推荐业务代码里用继承最关键的原因是Java是单继承你一旦extends Thread就不能再继承其他业务基类扩展性极差。而且继承方式把“任务代码”和“线程载体”耦合在了一起后续想放进线程池复用都不方便。它只适合教学演示或者极简单的隔离场景。还有些细节比如线程名字设置默认线程名是Thread-0、Thread-1这样的序列排查日志时非常痛苦。建议在构造器里显式setName或者用ThreadFactory统一命名。命名规则我习惯是“业务模块-线程类型-序号”比如order-pool-1一眼能看出是订单模块的线程池第一个线程。2.2 实现Runnable接口最经典的任务与执行分离Runnable是真正推荐的基础写法。它是只有一个抽象方法的函数式接口run()方法定义了“任务是什么”而线程的创建、调度、生命周期管理由Thread或线程池负责。这样做的好处是把任务和执行解耦你可以把同一个Runnable实例传给多个线程也可以扔给线程池。public class Task implements Runnable { Override public void run() { System.out.println(执行任务 Thread.currentThread().getName()); } } // 使用 Thread t new Thread(new Task(), task-thread-1); t.start(); // 更常见的是配合线程池 ExecutorService pool Executors.newFixedThreadPool(4); pool.submit(new Task());有一点要注意Runnable的run()不允许返回值也不能抛出受检异常方法内部如果出错了只能自己try-catch否则异常会直接传送到线程的未捕获异常处理器。所以如果任务需要返回结果就要用Callable。很多人刚接触时容易在这个选择上纠结其实原则就一句话要结果用Callable不要结果用Runnable匿名内部类或Lambda能简化就简化。2.3 Callable与FutureTask让线程跑完能交出结果JDK 5引入了Callable接口call()方法有返回值且可以抛出受检异常。它不能直接用来new Thread因为Thread构造器只接受Runnable所以需要一个FutureTask做适配FutureTask实现了RunnableFuture而RunnableFuture又继承了Runnable和Future。这就是为什么FutureTask既能传给Thread又能拿Future相关方法。CallableInteger callable () - { Thread.sleep(1000); return 42; }; FutureTaskInteger futureTask new FutureTask(callable); Thread t new Thread(futureTask, compute-thread); t.start(); // 在另一个线程里取结果 Integer result futureTask.get(); // 会阻塞等待关于futureTask.get()有两件事必须提醒第一get()是阻塞的。任务没做完调用get()的线程会进入WAITING状态挂起直到任务完成或者超时。线上环境强烈建议用带超时的重载get(long timeout, TimeUnit unit)不然任务一旦卡死调用方线程也跟着永久卡死。第二get()会抛出InterruptedException和ExecutionException。ExecutionException包装的是任务内部的异常需要getCause()才能看到真正的异常栈。我见过很多新人在日志里只打e.toString()最后发现是“ExecutionException: null”真正原因全被吞了排查时特别坑。2.4 三种实现方式的对比与选型依据我自己给团队做技术分享时会把三种经典方式放在同一张表里面试和评审时都很好用维度继承Thread实现RunnableCallable FutureTask是否解耦任务与执行否耦合严重是任务和线程分离是且支持返回结果返回值无无有通过Future获取异常处理只能自己处理只能自己处理可抛出受检异常复用性差一个线程只能跑一次好同一任务可多线程执行好配合线程池不推荐常用常用适用场景教学、极简演示无返回值的异步任务需要计算结果的异步任务选型逻辑其实很简单能用Runnable就不写Thread子类需要返回值就上Callable。复杂异步编排的用CompletableFuture替代裸FutureTask链式调用更舒服。如果能用线程池就不要手动new Thread这是我一直强调的底线。3. 线程控制与同步互斥从会创建到会玩明白线程创建只是起点。现实中更多的时间耗在“怎么让多个线程有序配合”上这就涉及到守护线程、等待唤醒机制、锁和可见性。这一节是面试八股和线上问题排查的重灾区我会把这些机制串成一个整体来讲。3.1 守护线程后台默默干活的角色Java线程分两类用户线程和守护线程。守护线程是给用户线程“打下手”的典型如JVM的垃圾回收线程。当进程中只剩下守护线程时JVM会直接退出不会等守护线程执行完。setDaemon(true)必须在start()之前调用否则会抛IllegalThreadStateException。这是网上偏门但偶尔会考的细节。业务里什么时候用守护线程比如后台周期性的心跳上报、临时缓存清理任务都可以设为守护线程。但有一个风险要讲清楚如果你在守护线程里做了重要的资源清理或数据落盘进程退出时它可能随时被杀死数据就丢了。所以涉及关键任务的线程宁可设置为非守护线程也别赌JVM退出前的“最后运行时间”。3.2 sleep、yield、join、wait/notify控制手段的边界这几个方法可以说是线程控制的基础套餐但它们的语义差异很多人并没有真正分清sleep让出CPU指定毫秒但不释放锁。sleep期间线程进入TIMED_WAITING到点后自动回到RUNNABLE。yield提示调度器“我可以让出CPU”但只是建议是否让出由系统决定。yield后线程仍是RUNNABLE。join当前线程等待目标线程终止。内部其实是用wait/notify实现的本质是让“当前线程”进入WAITING等目标线程结束后由JVM唤醒。wait/notify这两个方法来自Object类核心前提是线程必须持有该对象的monitor锁也就是要在synchronized代码块或方法里调用否则会抛IllegalMonitorStateException。我经常用一个场景把join和wait区分开线程A想等线程B执行完再继续A调B.join()如果A是被某个条件阻塞希望B干完某件事之后唤醒它则A调obj.wait()B在适当时机调obj.notify()。前者是“等整个线程生命周期结束”后者是“等某个条件满足”。wait/notify还有一个经典陷阱过早的notify。如果notify发生在wait之前这个通知就丢失了wait线程可能永远等不到唤醒。所以规范写法是用while循环包裹wait判断条件而不是if防止虚假唤醒和通知丢失synchronized (lock) { while (!ready) { lock.wait(); } // 条件满足后继续 }3.3 synchronized与Lock线程互斥的两条路线线程安全的大部分问题都出在“多个线程同时修改共享变量”。保证互斥最原始的手段是synchronized从JDK 1.0就有了后来JVM做了大量优化比如偏向锁、轻量级锁、重量级锁的升级过程。synchronized是“代码块级别”的互斥锁的是某个对象monitor可以是当前实例、指定对象或者Class对象。Lock接口从JDK 5引入提供了更灵活的锁控制lock()和unlock()要手动配对通常放在try-finally里保证一定释放。用得最多的是ReentrantLock它支持公平锁、可中断获取锁、超时获取锁、多个条件队列Condition等待唤醒。两者的选型我的实用判断标准是能用synchronized就尽量用synchronized。代码简洁不需要手动释放JVM自动优化死锁和漏释放的坑更少。需要尝试获取锁、带超时、可中断、公平性控制再考虑Lock。比如tryLock(3, TimeUnit.SECONDS)拿不到锁就降级处理这在synchronized里做不到。一个同步方法里的逻辑非常复杂、需要多个等待条件时用Condition会比用wait/notify清晰得多。锁的本质是“用阻塞换安全”但锁竞争本身有成本所以锁粒度要尽量小不要在大方法上加锁尽量锁代码块而不是锁整个方法。3.4 volatile与可见性锁解决不了的另一个问题有些同学以为只要不加锁就不安全加了锁就万事大吉但并发问题其实可以拆成三类原子性、可见性、有序性。synchronized和Lock主要解决原子性和互斥保证同一时刻只有一个线程操作volatile解决的是可见性和一定程度的顺序性它不保证原子性。我举个线上真实遇到过的例子// 线程A while (!stop) { doWork(); } // 线程B晚一点执行 stop true;这段代码在多线程下可能永远跑不完。因为线程A在工作循环里频繁读stopJIT可能把它优化成从寄存器直接取线程B修改了主存里的stopA却感知不到。用volatile修饰stop之后每次读取都强制从主存拿最新值写的时候也立即刷新到主存问题就解决了。这其实是JMM里happens-before规则的一个体现volatile写先行发生于volatile读。但volatile不能替代锁典型场景是“多个线程同时执行count”volatile修饰count只能保证读的是最新值但自增操作本身不是原子的会丢更新。这种必须用AtomicInteger或者加锁。理解了原子性和可见性的区别很多并发Bug的排查思路一下子就清晰了。3.5 StringBuidler这类“非线程安全”到底意味着什么热词里有“java stringbuilder”这里正好展开说。StringBuilder不是线程安全的StringBuffer是线程安全的原因是StringBuffer的关键方法加了synchronized。但实际项目里单线程拼接字符串用StringBuilder就够了StringBuffer反而因为锁开销更慢。多线程环境如果需要共享可变字符串通常也不该用StringBuffer而应该让每个线程持有自己的StringBuilder或者干脆用不可变字符串。“线程安全”是个容易被误解的词它不是说这个类内部一定加了锁而是说“在多线程并发访问下这个类对外暴露的状态始终保持一致”。比如HashMap线程不安全多线程并发put可能导致链表成环、丢数据而ConcurrentHashMap通过分段锁或CAS加锁设计保证并发下的安全。所以在面试里被问到“HashMap线程安全吗”正确的回答分三层不安全具体表现是数据覆盖和扩容死循环替代方案是ConcurrentHashMap或Collections.synchronizedMap。理解到这个层次才算真正答到位。4. 线程池、AQS与虚拟线程迈向工业化并发线程是好东西但不代表“越多越好”。每个线程都要占据内存、内核数据结构频繁创建销毁线程的代价非常大。线程池就是把线程复用的思路落到工程里的基础设施同时它是Java并发框架里最容易考、最容易配置错的点。4.1 线程池核心参数与完整工作流程线程池的核心构造器是ThreadPoolExecutor参数有七个核心线程数、最大线程数、空闲存活时间、时间单位、阻塞队列、线程工厂、拒绝策略。我见过太多人直接这样初始化Executors.newFixedThreadPool(10); Executors.newCachedThreadPool();这在demo里没问题但生产环境强烈建议自己new ThreadPoolExecutor因为Executors的快捷方法隐藏了关键细节最典型的问题有两个newFixedThreadPool用的是无界LinkedBlockingQueue任务积压时会导致内存无限增长newCachedThreadPool最大线程数是Integer.MAX_VALUE瞬时高并发下可能创建海量线程直接把内存打爆而且线程数无法预估。系统里我更喜欢按下面这种模板来写ThreadFactory threadFactory new ThreadFactoryBuilder() .setNameFormat(order-pool-%d) .build(); ThreadPoolExecutor executor new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(200), threadFactory, new ThreadPoolExecutor.AbortPolicy() );关于核心参数选多少我提供一个经验思路不完全公式化但很实用核心线程数CPU密集任务可以按CPU核数加1I/O密集任务按“核数×21”起步再结合压测数据进行调整。不要迷信公式生产环境最终要靠压测。最大线程数我一般设定为核心线程数的两倍或者根据阻塞队列和拒绝策略推算。如果任务量超过队列承受能力线程数扩充可以暂时兜底。keepAliveTime通常设置60秒或更久避免线程频繁回收。队列容量是核心中的核心它决定了系统的缓冲能力。宁可设置合理的有限队列也不要无界队列。线程池的工作流程我用一条叙事线讲清楚提交任务时如果当前工作线程数小于核心线程数会创建新线程执行如果已经达到核心线程数新任务进入队列等待如果队列也满了才会创建非核心线程执行直到达到最大线程数如果最大线程数也满了就会触发拒绝策略。4.2 阻塞队列选型第一道缓冲的作用阻塞队列的选择直接决定线程池的行为。常见的有四种ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue以及DelayedWorkQueue等变体。ArrayBlockingQueue是有界数组队列容量固定公平性可配置适合需要严格控制内存占用的场景。LinkedBlockingQueue支持有界和无界Executors默认用的就是无界版本容易积累无限任务。SynchronousQueue比较特殊它不存储元素每个put必须等待take相当于“直接递交”这时候线程池会立刻创建非核心线程执行任务适合任务量大、并发峰值需要及时处理的场景。PriorityBlockingQueue是优先级队列决定队列里的任务要按照优先级执行。选择队列的核心原则其实就一句话不要让无限任务拖垮内存。宁可设置一个有界队列加合理的拒绝策略让调用方感知压力也不要在背地里悄悄堆积。系统出现“队列积压但业务没报错”的现象往往就是因为无界队列掩盖了问题。4.3 拒绝策略和线程池监控ThreadPoolExecutor提供了四种内置拒绝策略AbortPolicy直接抛RejectedExecutionException默认策略让调用方知道任务被拒绝。CallerRunsPolicy谁提交谁跑任务会回退到提交者的线程执行天然形成了背压效果。DiscardPolicy静默丢弃任务不抛异常实际生产中容易“丢得无声无息”要非常谨慎。DiscardOldestPolicy丢弃队列里最老的任务然后把新任务加入队列。我的建议是默认用AbortPolicy并且给RejectedExecutionException加一个兜底逻辑比如记录关键日志或发送告警。如果业务的峰值流量是短时的可以用CallerRunsPolicy让提交线程也扛一部分任务一般不会造成灾难性放大。线程池监控是很多团队忽略的环节。ThreadPoolExecutor提供了getPoolSize、getActiveCount、getTaskCount、getCompletedTaskCount等方法可以通过定时任务把这些指标打到监控系统。还有一个很隐蔽的坑线程池异常会被吞掉。如果用execute()提交任务抛出的运行时异常不会影响线程池本身但异常不会自动打到业务日志需要自己包裹一层try-catch或者设置ThreadFactory里的UncaughtExceptionHandler。这几个小细节往往是线上故障排查时最先救命的线索。4.4 AQS看懂并发锁的一把钥匙AQS全称AbstractQueuedSynchronizer是JUC并发包的核心基类。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock很多都是基于AQS实现的。它的核心思路是维护一个volatile修饰的state变量和FIFO等待队列加锁就是CAS修改state抢不到锁的线程进入等待队列挂起。用生活类比来理解AQS像一个游乐园的排队系统state表示当前能同时进几人默认为0表示没人占用线程抢锁成功就把state改成1如果已经被占用新来的线程去排队前面的人玩完释放锁后会唤醒队列头部的下一个线程。AQS有两个模式独占模式和共享模式。ReentrantLock是独占的Semaphore是共享的CountDownLatch也是一种共享等待机制。面试里比较深的问题是“ReentrantLock的可重入怎么实现”答案就在AQS的state上同一线程重复获取时state加1释放一次减1减到0才真正释放锁。理解了AQS再看各种JUC锁会有一通百通的感觉。4.5 virtual threadJava 21后的新选择虚拟线程是Java 19引入、Java 21正式转正的特性Spring Boot 3.2及之后的版本可以直接开启热词里提到的“java21 spring boot 3.5启用虚拟线程”指的就是这个方向。虚拟线程的价值在于处理高并发I/O密集型任务时它大幅降低了线程切换和占用成本。以前每个阻塞请求占一个平台线程平台线程数量有限大量请求都在等待I/O时线程利用率很低。虚拟线程由JVM调度可以非常轻量地创建数十万甚至百万个底层以较少的载体线程执行屏蔽了阻塞等待的成本。Spring Boot中开启方式很简单不同版本差异比较大# Spring Boot 3.2 spring.threads.virtual.enabledtrue但要注意虚拟线程不是万能药它主要适合I/O密集、阻塞等待多的场景。如果是CPU密集任务或者任务内部有大量计算争夺CPU虚拟线程带来的优势有限反而可能因为调度过于频繁增加开销。另外synchronized在虚拟线程里早期有针脚现象后面版本在不断改善但配合Lock tryLock通常会获得更稳定的表现。生产上我建议先在旁路系统试跑观察吞吐和GC后再全量放开。4.6 线程安全容器一览别再背包装类Java并发包最有价值的资产之一是那批线程安全容器建议按功能分模块记忆需求推荐类不推荐Map并发读写ConcurrentHashMapHashMap、HashtableList并发访问CopyOnWriteArrayListArrayList、Vector虽安全但笨重Set并发访问ConcurrentHashMap.newKeySet()HashSet高并发计数器LongAdder、AtomicLongsynchronized 包裹的long阻塞队列ArrayBlockingQueue、LinkedBlockingQueue无界队列ConcurrentHashMap是面试重灾区它内部的实现经历了jdk7分段锁到jdk8 CAS加锁的精简演进。日常使用上它只是保证单次操作线程安全并不保证复合操作比如“先检查再更新”这种还是要靠锁或原子操作。不要以为用了ConcurrentHashMap你的整个业务逻辑就自动线程安全了它只解决容器本身的安全不解决你的逻辑顺序问题。5. 死锁成因与排查技巧实录死锁是一个值得单独开一章的话题因为它是并发编程里最难排查、也最让线上队友崩溃的问题。我先把死锁的本质讲透再给一个能复现的例子和一套排查思路。5.1 一个能复现的经典死锁案例死锁的四个必要条件是互斥、持有并等待、不可剥夺、循环等待。业务上最常见的形式是线程A持有锁1想拿锁2线程B持有锁2想拿锁1两个线程互相等待对方释放谁都不让。下面的代码就是一个标准案例Object lockA new Object(); Object lockB new Object(); // 线程1 synchronized (lockA) { Thread.sleep(100); synchronized (lockB) { System.out.println(线程1拿到了两个锁); } } // 线程2 synchronized (lockB) { Thread.sleep(100); synchronized (lockA) { System.out.println(线程2拿到了两个锁); } }两个线程都sleep了100毫秒就是为了让双方都有机会拿到各自的第一个锁然后把对方需要的第二个锁占住。程序运行后大概率卡死控制台没有任何输出。矛盾的地方在于代码看起来没有任何编译错误也没有报异常它就是不动了。5.2 jstack排查全过程排查死锁第一个工具是jstack。可以先jps找到Java进程ID然后执行jstack命令导出线程快照。如果存在死锁jstack输出末尾通常会有“Found one Java-level deadlock”的明确提示还会标出两个线程分别持有哪把锁、等待哪把锁。我实际排查的顺序是先看占CPU高的线程锁竞争和死锁经常伴随CPU异常然后看BLOCKED状态的线程它们的栈信息会明确显示卡在哪个synchronized或Lock的哪一行如果看到“waiting to lock”和“locked”指向不同对象基本就是锁顺序问题。生产过程用到的三板斧jps先定位进程。jstack threaddump.log保存线程快照。抓取两到三次快照间隔10秒方便对比线程状态变化。如果jstack信息不够直观再考虑用jcmd和jconsole从交互端观察但被管理进程需要开启JMX端口。对于长时间运行的Java进程可以在JVM参数里加上-XX:HeapDumpOnOutOfMemoryError但这是应对OOM的不是专门应对死锁的。注意这里的排查重点是拿线程快照而不是重启进程一重启就等于销毁了现场。5.3 避免死锁的常用方法和面试速查避免死锁的工程手段其实很朴素一个是锁顺序统一所有线程都按相同的顺序加锁破掉循环等待一个是缩小锁范围尽量别同时持有两把锁能拆成两个步骤就拆开再一个是使用带超时的lock.tryLock拿不到锁就放弃主动打破等待。我把这块高频面试题整理成一个速查表背下来应付大部分面试和日常自测足够了问题核心回答要点Synchronized和Lock的区别synchronized自动释放锁Lock手动释放Lock支持可中断、超时、公平锁、多条件volatile和synchronized的区别volatile轻量级保证可见性和有序性但非原子synchronized保证原子性创建线程有几种方式Thread继承、Runnable实现、CallableFutureTask现代还可用线程池和虚拟线程线程状态有哪些NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED线程池参数怎么定核心线程数结合CPU密集/I/O密集队列用有界队列拒绝策略兜底HashMap线程安全吗不安全多线程put丢数据、可能出现死循环用ConcurrentHashMap什么是死锁两个线程相互持有对方需要的锁陷入永久阻塞最后说点我个人摸爬滚打出来的经验和习惯。第一任何和并发相关的代码写完加锁逻辑后一定要自己模拟一遍“两个线程同时进来”的过程脑子过一遍谁先谁后很多时候死锁和竞态在写代码阶段就能发现。第二线程池的线程名一定要起好不要省这个事线上日志里能区分线程来源真的太重要了。第三线上一旦出现可疑卡顿先抓jstack再考虑重启不然你根本不知道刚才发生了什么。多线程不是难在API而是难在想清楚“多个执行流同时跑时数据的每一步变换是否可预期”把基础扎实很多看似复杂的问题都会突然变得清晰。写到这里这篇线程基础与实现方式的内容就完整了。我自己的习惯是看完一篇文章后无论多忙都抽10分钟把里面的示例代码手写一遍哪怕明知能跑也要敲一遍因为很多细节——比如wait必须在synchronized里、join会阻塞当前线程——光看是记不住的只有亲手踩过或亲手写错一遍才会变成肌肉记忆。并发这个世界最迷人的地方也在这里它不惩罚聪明人只惩罚粗心的大意者。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →