一、线程与JVM的亲密关系

想象一下JVM是一个大工厂,线程就是工厂里干活的工人。每个工人都有自己的工具箱(线程栈),里面装着干活需要的工具(局部变量、方法调用记录)。JVM负责给这些工人分配工作空间,而线程就是在JVM这个地盘上奔跑的执行单元。

重点来了:每个Java线程启动时,JVM都会给它分配一块私有内存区域(线程栈),这块区域存储着:

  • 方法调用栈帧(记录执行到哪一步了)
  • 局部变量(比如临时计算的数字)
  • 操作数栈(JVM执行指令的工作台)
// 技术栈:Java 11
public class ThreadStackDemo {
    public static void main(String[] args) {
        // 创建一个新线程(新工人)
        Thread worker = new Thread(() -> {
            int taskId = 1;              // 栈帧中的局部变量
            System.out.println("处理任务:" + taskId);
        });
        worker.start();  // JVM在这里分配线程栈
    }
}
// 注释:lambda表达式中的代码会在新线程栈中执行

二、线程栈的存储秘密

线程栈的大小可以通过-Xss参数调整(默认1MB),但这里有个隐藏陷阱:栈深度太大会导致StackOverflowError。比如递归调用没写好:

// 技术栈:Java 11
public class StackCrash {
    static void recursiveCall(int count) {
        System.out.println("深度:" + count);
        recursiveCall(count + 1);  // 无限递归,直到栈空间耗尽
    }
    
    public static void main(String[] args) {
        recursiveCall(1);  // 最终抛出StackOverflowError
    }
}
// 注释:每个方法调用都会在栈中压入新帧

关联技术:通过jstack命令可以查看线程栈状态,这是排查死锁的利器:

jstack <pid>  # 打印所有线程的调用栈

三、并发编程的基石

线程栈的存在让并发成为可能。看个经典的生产者-消费者例子:

// 技术栈:Java 11
import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumer {
    private static final int MAX_SIZE = 5;
    private final Queue<Integer> queue = new LinkedList<>();

    synchronized void produce() throws InterruptedException {
        while (queue.size() == MAX_SIZE) {
            wait();  // 释放锁,当前线程进入等待队列
        }
        queue.add(1);
        notifyAll();  // 唤醒其他线程
    }

    synchronized void consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        queue.poll();
        notifyAll();
    }
}
// 注释:wait()会让线程暂时退出竞争,进入WAITING状态

注意事项

  1. synchronized锁的是对象头中的Mark Word
  2. 每个等待的线程都会被封装成Monitor对象

四、现代并发工具实战

Java后来引入了更强大的java.util.concurrent包。比如用ThreadPoolExecutor管理线程:

// 技术栈:Java 11
import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,                      // 核心线程数
            5,                      // 最大线程数
            60, TimeUnit.SECONDS,   // 空闲线程存活时间
            new ArrayBlockingQueue<>(10)  // 任务队列
        );
        
        executor.execute(() -> {
            System.out.println("任务在线程:" + Thread.currentThread().getName());
        });
        
        executor.shutdown();  // 优雅关闭
    }
}
// 注释:线程池中的工作线程会复用线程栈空间

五、技术选型与避坑指南

应用场景对比
| 技术 | 适用场景 | 缺点 | |---------------|-------------------------|-----------------------| | synchronized | 简单的互斥控制 | 性能较差 | | ReentrantLock | 需要尝试获取锁的场景 | 必须手动释放 | | ThreadPool | 大量短期异步任务 | 参数配置复杂 |

终极建议

  1. 避免在锁内调用外部方法(容易造成死锁)
  2. 线程池队列建议使用有界队列
  3. 推荐使用ThreadLocal保存线程私有数据
// 技术栈:Java 11
public class ThreadLocalSafe {
    private static final ThreadLocal<SimpleDateFormat> formatter =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    
    public static void main(String[] args) {
        // 每个线程获取自己独立的SimpleDateFormat实例
        System.out.println(formatter.get().format(new Date()));
    }
}
// 注释:ThreadLocal内部使用Thread.currentThread()作为key