Java多线程与JVM概览
一、多线程基础
1.1 线程与进程(先搞清概念)
- 进程(Process):操作系统中正在运行的程序,每个进程有自己独立的一块内存空间。可以简单理解成「一个正在运行的软件」。
- 线程(Thread):进程中的执行单元,一个进程可以有多个线程,这些线程共享进程里的内存。
可以打个比方:
- 进程 = 一家公司;
- 线程 = 公司里的员工;
- 公司有自己的办公楼(进程内存),员工在同一栋楼里办公(共享内存),但每个员工同时只能做一件事。
在 Java 中,多线程的核心支持来自:
java.lang.Thread类:最基础的线程类。java.lang.Runnable接口:用来抽象「要执行的任务」。java.util.concurrent包:提供更高级的并发工具(线程池、锁、原子类等)。
初学阶段的重点:先理解概念和基本用法,知道什么时候需要多线程,而不是一上来就研究复杂的并发问题。
二、创建线程的几种方式
多线程的本质是:把一段代码交给另外一个线程去执行。在 Java 中,大体有三种常见方式。
2.1 继承 Thread 类(最直观)
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("线程执行run函数:" + i);
}
}
public static void main(String[] args) {
System.out.println("Java多线程案例");
// 多线程的本质是:把一段代码交给另外一个线程去执行。
MyThread t = new MyThread();
t.start(); // 启动线程,并且会自动执行run函数
for (int i = 0; i < 1000; i++) {
System.out.println("主线程函数------:" + i); // 会交替执行
}
}
}
适合场景:示例程序、简单小工具。
缺点:Java 只允许单继承,继承了 Thread 之后,就不能再继承其他父类了。
2.2 实现 Runnable 接口(推荐做法)
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("子线程中打印" + i);
}
}
public static void main(String[] args) {
// 创建实现类对象
MyRunnable runnable = new MyRunnable();
// 创建线程对象
Thread t = new Thread(runnable);
// 开启线程
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主函数中打印----" + i);
}
}
}
2.2.1 案例:龟兔赛跑
// 龟兔赛跑
public class RunnableDemo1 implements Runnable {
private static String winner; // 胜利者
private static int steps = 10000;
public static void main(String[] args) {
RunnableDemo1 runnableDemo1 = new RunnableDemo1();
new Thread(runnableDemo1, "兔子").start();
new Thread(runnableDemo1, "乌龟").start();
}
// 判断比赛是否完成
public boolean gameOver(int count) {
if (winner != null) {
return true;
} else if (count == steps) {
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
return false;
}
// 重写run函数
@Override
public void run() {
for (int i = 0; i <= steps; i++) {
if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0 && i != 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
boolean flag = gameOver(i); // 游戏是否结束
// 兔子每10米休息200ms
if (flag) break;
System.out.println(Thread.currentThread().getName() + "--->跑了" + i + "步");
}
}
}
与继承 Thread 相比:
- 更灵活,可以避免 Java 单继承的限制(类还可以继承别的父类)。
- 「任务逻辑」与「线程对象」分离,代码结构更清晰:
Runnable只负责要做什么,Thread负责谁来做。
小结:在实际项目中,优先考虑 Runnable + 线程池,而不是直接继承
Thread。
2.3 使用线程池(ExecutorService)(项目中最常用)
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
pool.submit(() -> {
System.out.println("线程池任务:" + Thread.currentThread().getName());
});
}
pool.shutdown();
2.4 实现 Callable 接口(了解)
Callable 接口类似于 Runnable,但它有返回值,并且可以抛出异常。通常配合线程池和 Future 使用。
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyCallable implements Callable<Boolean> {
public static void main(String[] args) {
System.out.println("Thread多线程练习一");
// 多个线程下载多个文件
MyCallable thread1 = new MyCallable("baidu1.png", "https://bookmanagementdatabase.oss-cn-hangzhou.aliyuncs.com/Java.jpg");
MyCallable thread2 = new MyCallable("baidu2.png", "https://bookmanagementdatabase.oss-cn-hangzhou.aliyuncs.com/MySQL.jpg");
MyCallable thread3 = new MyCallable("baidu3.png", "https://bookmanagementdatabase.oss-cn-hangzhou.aliyuncs.com/JavaScript%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97.jpg");
// 线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(thread1);
pool.submit(thread2);
pool.submit(thread3);
// 关闭
pool.shutdown();
}
private String name;
private String url;
// 构造器
public MyCallable(String name, String url) {
this.name = name;
this.url = url;
}
@Override
public Boolean call() {
WebDownloader downloader = new WebDownloader();
try {
downloader.download(url, name);
System.out.println("下载了文件" + name);
} catch (IOException e) {
throw new RuntimeException(e);
}
return true;
}
}
// 下载器
class WebDownloader {
// 下载方法
public void download(String url, String name) throws IOException {
FileUtils.copyURLToFile(new URL(url), new File(name)); // copyURLToFile:将文件链接保存为文件
}
}
注意:上述代码使用了
commons-io库进行文件下载。
优点:
- 复用线程,减少创建和销毁线程的开销。
- 便于统一管理与监控。
三、线程状态与常用方法
3.1 线程主要状态(用流程来记)
- NEW(新建):刚创建的线程对象,还没调用
start()。 - RUNNABLE(可运行):已经调用
start(),可能正在运行,也可能在就绪队列里等 CPU。 - BLOCKED(阻塞):在等待锁(例如
synchronized)时会进入阻塞。 - WAITING / TIMED_WAITING(等待 / 计时等待):
- 调用了
wait()、join()、LockSupport.park()等方法; TIMED_WAITING带有超时时间,比如sleep(1000)。
- 调用了
- TERMINATED(终止):线程执行完
run()方法后,进入终止状态。
可以想象一个简单流程:
new Thread(...)→ NEW- 调用
start()→ RUNNABLE - 运行过程中遇到
sleep()/wait()/ 获取不到锁 → WAITING / TIMED_WAITING / BLOCKED - 代码执行结束(
run()方法返回) → TERMINATED
3.2 常用方法(记住用法和坑)
start():启动新线程,底层会去调用run()。run():线程要执行的代码逻辑,一般不用手动调用run()来「启动线程」。sleep(long millis):让当前线程暂停一段时间,不会释放锁。join():在一个线程中调用另一个线程的join(),表示「等你先执行完」。interrupt():给线程打一个「中断标记」,让线程自己决定如何结束。
常见坑:
t.run()和t.start()的区别:前者只是普通方法调用,在当前线程里执行;后者才是真正启动一个新线程。
四、synchronized 与 volatile(并发入门)
4.1 一个简单的「线程安全」问题示例
下面这段代码,看起来没问题,但在多线程下会出错:
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 不是原子操作
}
public int getCount() {
return count;
}
}
如果多个线程同时调用 increment(),最后的结果往往小于期望值(比如期望 10000,实际不到 10000),这就是线程安全问题。
原因是 count++ 实际上分成三步:读值 → 加一 → 写回,中间可能被其他线程「插队」。
4.2 synchronized 关键字(最基础的锁)
- 用于保证原子性(一段代码整体要么执行完,要么不执行)和一定程度的可见性。
- 三种常见写法:
- 同步实例方法:
public synchronized void foo() {} - 同步静态方法:
public static synchronized void bar() {} - 同步代码块:
synchronized(锁对象) { ... }
- 同步实例方法:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
加上 synchronized 后,同一时刻只有一个线程能执行 increment(),从而保证结果正确。
注意:
synchronized的锁对象要选好,一般使用this、某个固定的对象字段,或者类对象(静态方法)。
4.3 volatile 关键字(解决可见性)
- 保证可见性:一个线程修改变量后,其他线程能「立刻」看到新值。
- 禁止某些指令重排序优化(一定程度上保证有序性)。
- 不能保证复合操作(如
count++)的原子性,所以不能用来替代synchronized。
public class FlagExample {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void work() {
while (running) {
// do something
}
}
}
如果不加 volatile,有可能一个线程改了 running 的值,另一个线程一直「看不到」,导致死循环。
五、原子类与并发工具
5.1 原子类(Atomic)
位于 java.util.concurrent.atomic 包。
AtomicInteger、AtomicLong等。- 通过 CAS(比较并交换)实现线程安全的自增、自减等操作。
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // ++count
count.getAndIncrement(); // count++
5.2 Lock 接口
java.util.concurrent.locks.Lock 提供比 synchronized 更灵活的锁机制。
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
六、JVM 内存结构概览
这一节是为了帮助你大致理解「Java 程序在内存里是怎么放的」,不用死记具体细节。
6.1 常见内存区域(简化版)
- 堆(Heap)
- 存放所有对象实例和数组,几乎所有用
new出来的东西都在这里。 - 由垃圾回收器(GC)自动管理。
- 存放所有对象实例和数组,几乎所有用
- 虚拟机栈(Stack)
- 每个线程一个栈。
- 每调用一个方法,就会在栈里压入一个「栈帧」,里边有:局部变量、参数、返回地址等。
- 方法区(Method Area)/ 元空间
- 存放类的元数据(类信息、常量池、静态变量、JIT 编译后的代码等)。
- 程序计数器(PC 寄存器)
- 每个线程一个。
- 记录当前线程下一条要执行的字节码指令地址。
可以把 JVM 想象成一栋大楼:
- 堆 = 大仓库(所有对象都在这里)。
- 每个线程的栈 = 员工各自的工具包,里面有当前在干什么、需要用到哪些局部变量。
- 方法区 = 说明书/设计图,里面写着「这个类长什么样」「有哪些方法」。
七、垃圾回收(GC)概念
7.1 GC 主要思想(记住一句话)
- 核心目标:找出不再被引用的对象,把它们占用的内存回收掉。
- Java 开发中不需要手动释放内存(不像 C/C++ 的
free/delete),减轻了很多负担。 - 但仍然可能发生「内存泄露」:对象明明已经没用了,却还被某些地方引用着,导致 GC 无法回收。
7.2 常见 GC 思路(了解即可)
- 标记-清除(Mark-Sweep)
- 第一步:从「GC Roots」出发(比如栈上的变量、静态变量),找到所有能直接或间接访问到的对象,做标记。
- 第二步:清除所有没被标记的对象,回收它们的内存。
- 复制算法(Copying)
- 把堆分成两块:from / to。
- 将存活对象从 from 复制到 to,然后一次性清掉 from。
- 适合「大部分对象很快就死掉」的新生代。
- 标记-整理(Mark-Compact)
- 先标记存活对象,然后把它们往一端移动,最后清理边界外的内存,减少内存碎片。
- 分代收集(Generational GC)
- 现实经验:大部分对象「生得快,死得也快」,只有少部分活得久。
- JVM 把堆分成「新生代」「老年代」,针对不同区域选择不同的回收算法,提高效率。
八、JVM 调优与排查(入门向)
这里只做简单了解,知道有这些工具即可:
- 常见 JVM 参数
-Xms:初始堆大小。-Xmx:最大堆大小。-Xss:每个线程栈大小。
- 常见诊断工具
jps:查看 Java 进程。jstack:查看线程栈,排查死锁。jmap:查看堆内存、生成 heap dump。
在日常开发中:
- 重点是写出内存友好、线程安全的代码。
- 碰到性能、内存问题时,再深入用工具分析。
