多线程详解

多线程详解


一、先搞懂:什么是多线程?

1.1 通俗理解

多线程 = 一个程序同时干多件事,就像你 一边听音乐、一边刷手机、一边喝水 —— 不是“做完一件再做另一件”,而是“同时进行”(本质是CPU快速切换,视觉上同步)。

举个Java程序的例子:

  • 单线程:打开一个记事本,只能先打字,再保存,不能同时打字和保存

  • 多线程:打开微信,既能同时接收消息,又能发朋友圈、听语音,互不影响

核心目的:提高程序效率,避免某一个操作(比如等待网络、读取文件)卡住整个程序。

1.2 进程 vs 线程

很多新手会混淆这两个概念,用一句话说清:

进程是“一个完整的程序”(比如微信、记事本),线程是“进程里的一个任务”(比如微信接收消息、发朋友圈)

对比项进程线程
本质独立的程序单元进程内的执行任务
资源占用多(独立内存、CPU)少(共享进程资源)
切换速度

简单记:一个进程可以包含多个线程,线程是进程的“小弟”,共享大哥的资源,一起干活


二、Java 中创建多线程的 3 种方式

Java 提供了 3 种创建线程的方式,从简单到复杂排序,新手先掌握前 2 种即可。

2.1 方式一:继承 Thread 类(最基础)

步骤:① 继承 Thread 类 ② 重写 run() 方法(线程要干的活) ③ 创建对象,调用 start() 方法启动线程

完整可运行代码(复制就能测):

// 1. 继承 Thread 类
class MyThread extends Thread {
    // 2. 重写 run() 方法:线程要执行的逻辑
    @Override
    public void run() {
        // 这里写线程要干的活,比如循环打印
        for (int i = 0; i < 5; i++) {
            // Thread.currentThread().getName() 获取当前线程名称
            System.out.println(Thread.currentThread().getName() + ":执行第 " + (i+1) + " 次");
            try {
                // 让线程休眠 500 毫秒(模拟干活耗时),新手可忽略
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
​
// 测试类
public class ThreadDemo1 {
    public static void main(String[] args) {
        // 3. 创建线程对象
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        // 4. 给线程起名字(可选,方便区分)
        thread1.setName("线程A");
        thread2.setName("线程B");
        
        // 5. 启动线程(必须用 start(),不能用 run()!)
        thread1.start();
        thread2.start();
    }
}

运行结果(参考)

线程A:执行第 1 次
线程B:执行第 1 次
线程A:执行第 2 次
线程B:执行第 2 次
线程A:执行第 3 次
线程B:执行第 3 次
线程A:执行第 4 次
线程B:执行第 4 次
线程A:执行第 5 次
线程B:执行第 5 次
​

新手注意:启动线程必须用 start\(\) 方法,不能直接调用 run\(\) —— 调用 run\(\) 只是普通方法调用,不会开启多线程,还是单线程执行。

2.2 方式二:实现 Runnable 接口(推荐)

步骤:① 实现 Runnable 接口 ② 重写 run() 方法 ③ 创建 Runnable 实现类对象 ④ 把对象传给 Thread 类,调用 start() 启动

为什么推荐?Java 只能单继承,继承 Thread 类后就不能继承其他类了,而实现接口可以多实现,更灵活。

完整可运行代码:

// 1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    // 2. 重写 run() 方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":执行第 " + (i+1) + " 次");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
​
// 测试类
public class ThreadDemo2 {
    public static void main(String[] args) {
        // 3. 创建 Runnable 实现类对象
        MyRunnable runnable = new MyRunnable();
        
        // 4. 把对象传给 Thread,创建线程
        Thread thread1 = new Thread(runnable, "线程C");
        Thread thread2 = new Thread(runnable, "线程D");
        
        // 5. 启动线程
        thread1.start();
        thread2.start();
    }
}
​

运行结果和方式一类似,都是两个线程交替执行,核心区别是“实现接口”更灵活。

2.3 方式三:实现 Callable 接口(带返回值,进阶)

前两种方式的 run() 方法没有返回值、不能抛异常,Callable 接口解决了这个问题,适合需要获取线程执行结果的场景(新手了解即可)。

完整可运行代码:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
​
// 1. 实现 Callable 接口,指定返回值类型(这里是 Integer)
class MyCallable implements Callable<Integer> {
    // 2. 重写 call() 方法(有返回值、可抛异常)
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        // 线程任务:计算 1-5 的和
        for (int i = 1; i <= 5; i++) {
            sum += i;
            System.out.println(Thread.currentThread().getName() + ":计算 i=" + i);
            Thread.sleep(500);
        }
        return sum; // 返回计算结果
    }
}
​
// 测试类
public class ThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3. 创建 Callable 实现类对象
        MyCallable callable = new MyCallable();
        
        // 4. 包装成 FutureTask(用于获取返回值)
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        
        // 5. 启动线程
        Thread thread = new Thread(futureTask, "计算线程");
        thread.start();
        
        // 6. 获取线程返回值(会阻塞,直到线程执行完成)
        Integer result = futureTask.get();
        System.out.println("线程执行结果:1-5 的和 = " + result);
    }
}
​

运行结果会先打印计算过程,最后输出总和 15,适合需要获取线程执行结果的场景(比如多线程计算任务)。


三、多线程的核心问题:线程安全

3.1 什么是线程安全?

多个线程同时操作同一个资源(比如一个变量、一个文件)时,会出现“数据错乱”的问题,这就是线程不安全。

举个例子:两个线程同时给一个变量 count 加 1,预期结果是 2,但实际可能是 1(因为两个线程同时读取到 0,都加 1 后变成 1)。

线程不安全演示代码(复制测试,会出现数据错乱):

class UnsafeThread implements Runnable {
    // 共享资源:两个线程同时操作这个变量
    private int count = 0;
​
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            count++; // 线程不安全的操作
        }
    }
​
    // 获取最终结果
    public int getCount() {
        return count;
    }
}
​
public class ThreadUnsafeDemo {
    public static void main(String[] args) throws InterruptedException {
        UnsafeThread unsafeThread = new UnsafeThread();
        
        // 两个线程同时操作 count
        Thread t1 = new Thread(unsafeThread);
        Thread t2 = new Thread(unsafeThread);
        
        t1.start();
        t2.start();
        
        // 等待两个线程执行完成(新手可忽略,用于确保结果准确)
        t1.join();
        t2.join();
        
        // 预期结果:2000,实际结果大概率小于 2000(数据错乱)
        System.out.println("最终 count 值:" + unsafeThread.getCount());
    }
}
​

运行后会发现,最终 count 值往往小于 2000,这就是线程不安全的问题。

3.2 新手必学:解决线程安全的 2 种简单方式

方式一:synchronized 关键字(锁)

通俗理解:给“共享资源的操作”加一把锁,同一时间只有一个线程能执行这个操作,其他线程排队等待。

修改上面的代码,添加 synchronized,实现线程安全:

class SafeThread implements Runnable {
    private int count = 0;
​
    // 方式1:给方法加锁(整个方法都被锁住)
    @Override
    public synchronized void run() {
        for (int i = 0; i < 1000; i++) {
            count++;
        }
    }
​
    // 方式2:给代码块加锁(只锁共享资源操作,更高效)
    // @Override
    // public void run() {
    //     synchronized (this) { // this 表示当前对象,锁的是共享资源所在的对象
    //         for (int i = 0; i < 1000; i++) {
    //             count++;
    //         }
    //     }
    // }
​
    public int getCount() {
        return count;
    }
}
​
public class ThreadSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        SafeThread safeThread = new SafeThread();
        Thread t1 = new Thread(safeThread);
        Thread t2 = new Thread(safeThread);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        // 此时结果一定是 2000,线程安全
        System.out.println("最终 count 值:" + safeThread.getCount());
    }
}
​

新手建议:先掌握“给方法加锁”,简单易记;后续再学习“代码块加锁”(更高效)。

方式二:使用线程安全的类

Java 自带一些线程安全的类,比如 AtomicInteger(原子类),专门用于解决“变量自增/自减”的线程安全问题,不用自己加锁。

示例代码:

import java.util.concurrent.atomic.AtomicInteger;
​
class AtomicThread implements Runnable {
    // 线程安全的原子类,替代普通 int
    private AtomicInteger count = new AtomicInteger(0);
​
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            count.incrementAndGet(); // 原子自增,线程安全
        }
    }
​
    public int getCount() {
        return count.get(); // 获取值
    }
}
​
public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicThread atomicThread = new AtomicThread();
        Thread t1 = new Thread(atomicThread);
        Thread t2 = new Thread(atomicThread);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("最终 count 值:" + atomicThread.getCount()); // 一定是 2000
    }
}
​

四、新手必记:多线程核心总结

  1. 多线程本质:一个程序同时执行多个任务,提高效率(CPU快速切换,视觉上同步)。

  2. 创建方式

    • 继承 Thread 类:简单,不能多继承

    • 实现 Runnable 接口:推荐,灵活可多实现

    • 实现 Callable 接口:进阶,带返回值、可抛异常

  3. 线程安全

    • 问题:多个线程操作同一资源,导致数据错乱

    • 解决:synchronized 加锁 或 使用线程安全类(如 AtomicInteger)

  4. 新手避坑

    • 启动线程用 start\(\),不是 run\(\)

    • 不要让多个线程操作同一个普通变量(避免线程不安全)

    • 线程休眠用 Thread\.sleep\(毫秒\),单位是毫秒


五、新手学习建议

1. 先把前两种创建线程的方式,逐行敲一遍代码,观察线程交替执行的效果,理解多线程的“并行”感;

2. 测试线程不安全的代码,看看数据错乱的现象,再用synchronized 修复,感受“锁”的作用;

3. 不用急于学复杂的线程池、锁机制,先掌握基础用法,后续学习 Spring、MyBatis 时,会遇到多线程的实际应用;

4. 尝试写一个简单的多线程案例:比如两个线程分别打印 1-10,观察交替执行的结果。


博客结尾

多线程是 Java 基础中的重点,也是新手的难点,但只要结合生活案例 + 多敲代码,就能轻松理解。

如果运行代码时遇到问题,或者有不懂的地方,欢迎在评论区留言,一起交流学习~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值