Java多线程与JVM概览

时游大约 9 分钟

一、多线程基础

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() 方法后,进入终止状态。

可以想象一个简单流程:

  1. new Thread(...) → NEW
  2. 调用 start() → RUNNABLE
  3. 运行过程中遇到 sleep() / wait() / 获取不到锁 → WAITING / TIMED_WAITING / BLOCKED
  4. 代码执行结束(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 包。

  • AtomicIntegerAtomicLong 等。
  • 通过 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。

在日常开发中:

  • 重点是写出内存友好、线程安全的代码。
  • 碰到性能、内存问题时,再深入用工具分析。
上次编辑于:
贡献者: 15327360835
Loading...