一、 什么是Java线程?为什么需要它?
线程这东西,说白了就是让一个程序能够与此同时干两件事的“分身”。在单核CPU时代,我们通过时间片轮转来模拟并行,而在多核时代,Java线程真正实现了物理层面的并行处理。那会儿写个多线程任务,你得对着编译器的倒计时喊三声,整点整点转个弯,然后才能去干另一件。这时候我务必说,那个时代的我还比较纯粹,那时候的线程是那种挺纯粹的、为了抢 CPU 工夫而生的工具,没有那么多花里胡哨。
目前想想,要是把代码比作人,多线程就是一个人与此同时拿着两把剑,但软件层面实际上是监着两个人,保证两个人不抢同一把剑,就连有时候得防止两个人干出一样的事,害得系统“分身乏术”。理解Java线程操作流程的第一步,就是要理解这种“分身”的本质:它们共享同一个内存空间(堆),但拥有独立的栈空间。
进程 vs 线程
进程是资源分配的最小单位,线程是CPU调度的最小单位。线程比进程更轻量,创建和切换开销更小。
并行 vs 并发
并行是真正的同时执行(多核),并发是逻辑上的同时执行(单核时间片轮转)。Java多线程主要解决并发问题。
为什么用多线程
提高CPU利用率,缩短响应时间,模拟现实世界(如多个用户同时访问服务器)。
二、 java线程怎么写?核心创建方式
说到如何写的,真没那些虚头巴脑的铺垫。直接上代码吧,别整啥“起初、其次”。想象一下你手里拿着一根拖把,想把地板刷得干干净利落净,但又得顺便把阳台拖一遍。你把拖把折成了两半,一半去刷客厅,一半去刷阳台。这时候你得给每半拖把起个名字,比如叫 bj 和 py。这就好比 Java 里的 Thread,你得给它起个名字,比如 bjThread 和 pyThread。
1. 继承 Thread 类
这是最直观的方式,但Java是单继承,限制了扩展性。
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("正在执行线程任务: " + getName());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.setName("bjThread");
t1.start(); // 启动线程,不是run()
}
}
2. 实现 Runnable 接口(推荐)
最核心的逻辑实际上就在那一行:Thread t1 = new Thread(bj, "bj")。这里把名字传进去,就像给每个人起个名字。紧接着 Thread t2 = new Thread(py, "py")。有了名字,每个线程就有了自己的身份。你得给它们干活。你得让它们点不同的“按钮”要么访问不同的“频道”。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("正在执行Runnable任务: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread t1 = new Thread(task, "bjThread");
Thread t2 = new Thread(task, "pyThread");
t1.start();
t2.start();
}
}
3. 实现 Callable 接口(有返回值)
如果需要一个线程执行完后返回结果,或者抛出检查异常,需要使用 Callable 和 Future。
import java.util.concurrent.;
public class MyCallable implements Callable {
@Override
public String call() throws Exception {
return "任务完成,返回结果";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new MyCallable());
System.out.println(future.get()); // 获取结果,阻塞等待
executor.shutdown();
}
}
三、 Java线程生命周期详解
理解Java线程操作流程必须掌握其状态转换。Java线程有六种状态,由 java.lang.Thread.State 枚举定义。
NEW
线程被创建,但尚未调用 start() 方法。此时线程还没有开始执行,只是存在于内存中。
RUNNABLE
调用了 start() 后,线程进入可运行状态。它可能在等待CPU时间片,也可能正在执行。这是Java中RUNNABLE状态包含操作系统的Ready和Running两种状态。
BLOCKED
线程在等待获取监视器锁(monitor lock),以便进入同步代码块或方法。通常发生在多个线程竞争同一把锁时。
WAITING
线程无限期等待另一个线程执行特定操作。例如调用 Object.wait()、Thread.join() 或 LockSupport.park() 时进入此状态。
TERMINATED
线程执行完毕 run() 方法,或者因异常退出。线程生命周期结束,无法再次启动。
线程状态转换图
new Thread() 后,线程处于新建状态。
调用 start() 方法,线程进入可运行状态,等待CPU调度。
获得CPU时间片后进入Running状态。若等待锁进入Blocked状态。若调用wait()进入Waiting状态。
run()方法执行完毕,线程正常结束。
四、 线程池:生产环境的必备选择
举个例子,假设你要写个造管理指令的系统。你能够创建一个造者线程 p1,负责生成订单数据,与此同时创建一个花者线程 c1,负责把订单发给仓库。这时候,你不需求显式地去 wait() 或 join()。你能够用 Runnable 接口把两个线程“塞”到同一个 ExecutorService 队列里。
这个队列就像一个停车场,所有的线程都得按顺序排队。当队列满了,系统会随机把线程吐出来干活。这时候,p1 生成订单,c1 收到订单并处理。要是 p1 生成的订单忒珍贵,c1 不得不排个长队等着。这时候,p1 在生成订单的与此同时,还得去维护一下“订单已生成”这个标志位。要是 p1 没做完,c1 就得持续排队;要是 c1 快完了,p1 的请求就得重新进队列,让 c1 持续排队。这就是典型的 Join 在起功能,它保证了顺序,防止了乱序。
核心线程池参数
ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
常用线程池类型
- FixedThreadPool: 固定大小的线程池,适用于负载较重的服务器。
- SingleThreadExecutor: 单线程池,保证任务顺序执行。
- CachedThreadPool: 缓存线程池,适合执行大量短期异步任务。
- ScheduledThreadPool: 定时任务线程池,支持延迟和周期性执行。
五、 同步与并发控制
还有一点贼关键,就是隔离性。有时候,一个线程生成的订单数据可能挺大,就连带着敏感信息。这时候你不能直接把大对象塞给 c1。你能够用 CompletableFuture 要么 Future 这种机制。 p1 做完后,把结局分发给 c1。 c1 拿到结局后,接着处理。这个流程里, p1 和 c1 是独立的,互不干扰。
1. synchronized 关键字
Java中最基本的同步机制,可以用于方法或代码块。保证同一时刻只有一个线程执行同步代码。
2. volatile 关键字
保证变量的可见性,禁止指令重排序。但不能保证原子性,适用于“写多读少”且操作简单的场景。
3. ReentrantLock
更灵活的锁机制,支持尝试获取锁、超时获取锁、公平锁等。适合复杂同步场景。
4. 死锁预防
自然,线程这东西挺好办出幺蛾子。比如死锁。两个线程等着对方手里的锁,结局两人都没动静,整个程序就僵住了。这时候你得去检查 ThreadLocal 变量,检查是不是哪位在偷偷改了自动提交的事务,害得双方都当作对方没提交。还有,别把线程池弄得忒小,不然任务排不到 queue,线程就白忙活;也别弄得忒大,不然 CPU 利用率上不去。得找个平衡点。
六、 高级特性:CompletableFuture
小时候我总当作线程是那种挺了得的魔法,能瞬间让程序跑得飞快。目前回头看,它只是一般/平平的并行计算工具。真正的魅力不在于它有多快,而在于它如何把紧耦合的任务拆解开,让不同逻辑的模块能互相协作。当你把 p1 的 orderService 和 c1 的 inventoryService 分给两个线程时,你实际上是在说:“嘿,这两个模块别看逻辑上相关联,但它们能够并行执行,只要结局一致就行。”
import java.util.concurrent.CompletableFuture;
public class CompletableFutureDemo {
public static void main(String[] args) {
// 异步执行任务
CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
return "订单数据";
});
// 依赖前一个任务的结果
CompletableFuture future2 = future1.thenApply(result -> {
return result + " - 处理中";
});
// 获取最终结果
future2.thenAccept(System.out::println);
}
}
七、 总结与建议
写多线程别总想着堆参数,参数多了反而好办乱。核心就是:名字、队列、隔离、顺序。名字是身份,队列是秩序,隔离是边界,顺序是契约。把这些搞明白了,线程就是火。
在实战中,建议优先使用线程池,避免频繁创建和销毁线程的开销。合理使用同步工具类(如CountDownLatch, CyclicBarrier, Semaphore)来协调线程间的执行顺序。对于复杂的异步流程,CompletableFuture 提供了优雅的组合方式。最后,务必注意线程安全,避免共享数据的竞态条件,必要时使用 ConcurrentHashMap、AtomicInteger 等并发工具类。