Java多线程基础:从Thread到Runnable,看这一篇就够了

7 阅读7分钟

Java多线程基础:从Thread到Runnable,看这一篇就够了

作者:全栈修炼日记
系列:Java并发编程实战
难度:⭐⭐(适合1-3年经验)

前言

作为一个工作两年的Java开发,我发现很多同学(包括曾经的我)对多线程的理解还停留在"会用Thread和Runnable"的阶段。但当面试官问起线程状态、线程安全、为什么要用start()而不是run()时,就开始支支吾吾了。

今天这篇文章,我会用最简单的语言,带你彻底搞懂Java多线程的基础知识。看完这篇,你至少能回答面试中80%的多线程基础问题。


一、为什么需要多线程?

1.1 单线程的痛点

想象一个场景:你写了一个程序,需要做三件事:

  1. 从数据库查询数据(耗时2秒)
  2. 调用第三方API(耗时3秒)
  3. 处理业务逻辑(耗时1秒)

如果用单线程,总耗时 = 2 + 3 + 1 = 6秒

但如果用多线程,三个任务同时执行,总耗时 = max(2, 3, 1) = 3秒

性能提升一倍!

1.2 多线程的应用场景

  • Web服务器:每个请求一个线程,支持并发访问
  • 异步任务:发送邮件、生成报表,不阻塞主流程
  • 大数据处理:分片并行计算,提升效率
  • 实时系统:同时处理多个事件

二、创建线程的两种方式

2.1 方式一:继承Thread类

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程名称:" + Thread.currentThread().getName());
        System.out.println("Hello from MyThread!");
    }
}

// 使用
public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}

输出:

线程名称:Thread-0
Hello from MyThread!

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

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程名称:" + Thread.currentThread().getName());
        System.out.println("Hello from MyRunnable!");
    }
}

// 使用
public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

2.3 两种方式的对比

特性继承Thread实现Runnable
灵活性❌ Java单继承,无法继承其他类✅ 可以继承其他类
资源共享❌ 每个线程独立✅ 多个线程可以共享同一个Runnable对象
解耦性❌ 线程和任务耦合✅ 线程和任务分离
推荐度⭐⭐⭐⭐⭐⭐⭐

结论:优先使用Runnable接口!


三、线程的6种状态(重要!)

Java线程在生命周期中会经历6种状态:

NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED

3.1 状态详解

状态说明如何进入
NEW新建状态创建Thread对象,但未调用start()
RUNNABLE可运行状态调用start()后,等待CPU调度
BLOCKED阻塞状态等待获取锁(synchronized)
WAITING等待状态调用wait()、join()
TIMED_WAITING超时等待调用sleep(time)、wait(time)
TERMINATED终止状态run()方法执行完毕

3.2 状态转换示例

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(2000); // 进入TIMED_WAITING
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        System.out.println("创建后:" + thread.getState()); // NEW
        
        thread.start();
        System.out.println("启动后:" + thread.getState()); // RUNNABLE
        
        Thread.sleep(100);
        System.out.println("睡眠中:" + thread.getState()); // TIMED_WAITING
        
        thread.join(); // 等待线程结束
        System.out.println("结束后:" + thread.getState()); // TERMINATED
    }
}

输出:

创建后:NEW
启动后:RUNNABLE
睡眠中:TIMED_WAITING
结束后:TERMINATED

四、start() vs run():面试必问!

4.1 区别

Thread thread = new Thread(() -> {
    System.out.println("当前线程:" + Thread.currentThread().getName());
});

// 方式1:调用start()
thread.start();
// 输出:当前线程:Thread-0(新线程)

// 方式2:调用run()
thread.run();
// 输出:当前线程:main(主线程)

关键区别:

  • start():启动一个新线程,JVM会调用run()方法
  • run():在当前线程中直接执行,相当于普通方法调用

4.2 start()的底层原理

public synchronized void start() {
    // 1. 检查线程状态(不能重复启动)
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    
    // 2. 加入线程组
    group.add(this);
    
    // 3. 调用native方法,创建新线程
    start0();
}

private native void start0(); // JVM底层实现

记住:start()只能调用一次,重复调用会抛出IllegalThreadStateException!


五、线程的常用方法

5.1 sleep() - 线程休眠

public class SleepDemo {
    public static void main(String[] args) {
        System.out.println("开始时间:" + System.currentTimeMillis());
        
        try {
            Thread.sleep(2000); // 休眠2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("结束时间:" + System.currentTimeMillis());
    }
}

特点:

  • 让当前线程暂停执行
  • 不会释放锁(重要!)
  • 时间到了自动恢复RUNNABLE状态

5.2 join() - 等待线程结束

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("子线程开始");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程结束");
        });
        
        thread.start();
        thread.join(); // 主线程等待子线程结束
        
        System.out.println("主线程继续执行");
    }
}

输出:

子线程开始
子线程结束
主线程继续执行

5.3 yield() - 线程让步

public class YieldDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("线程1:" + i);
                Thread.yield(); // 让出CPU
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("线程2:" + i);
            }
        });
        
        t1.start();
        t2.start();
    }
}

特点:

  • 让当前线程让出CPU时间片
  • 只是建议,不保证一定让出
  • 让出后立刻重新竞争CPU

六、线程优先级

6.1 优先级范围

Thread.MIN_PRIORITY = 1   // 最低优先级
Thread.NORM_PRIORITY = 5  // 默认优先级
Thread.MAX_PRIORITY = 10  // 最高优先级

6.2 设置优先级

public class PriorityDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("低优先级线程");
        });
        
        Thread t2 = new Thread(() -> {
            System.out.println("高优先级线程");
        });
        
        t1.setPriority(Thread.MIN_PRIORITY); // 设置为1
        t2.setPriority(Thread.MAX_PRIORITY); // 设置为10
        
        t1.start();
        t2.start();
    }
}

注意:

  • 优先级只是建议,不保证一定先执行
  • 不同操作系统对优先级的支持不同
  • 不要依赖优先级来保证执行顺序!

七、守护线程(Daemon Thread)

7.1 什么是守护线程?

守护线程是为其他线程服务的线程,当所有非守护线程结束时,JVM会自动退出,守护线程也会被强制终止。

典型例子:GC线程

7.2 使用示例

public class DaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread daemon = new Thread(() -> {
            while (true) {
                System.out.println("守护线程运行中...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        daemon.setDaemon(true); // 设置为守护线程
        daemon.start();
        
        Thread.sleep(2000);
        System.out.println("主线程结束");
        // 主线程结束后,守护线程也会自动终止
    }
}

输出:

守护线程运行中...
守护线程运行中...
守护线程运行中...
守护线程运行中...
主线程结束

八、实战案例:多线程下载文件

8.1 需求

模拟下载一个100MB的文件,分成4个线程并行下载,每个线程下载25MB。

8.2 代码实现

public class MultiThreadDownload {
    private static final int TOTAL_SIZE = 100; // 总大小(MB)
    private static final int THREAD_COUNT = 4; // 线程数
    
    public static void main(String[] args) throws InterruptedException {
        int partSize = TOTAL_SIZE / THREAD_COUNT; // 每个线程下载的大小
        
        Thread[] threads = new Thread[THREAD_COUNT];
        
        for (int i = 0; i < THREAD_COUNT; i++) {
            final int threadId = i;
            final int startPos = i * partSize;
            final int endPos = (i + 1) * partSize;
            
            threads[i] = new Thread(() -> {
                System.out.println("线程" + threadId + "开始下载:" + startPos + "MB - " + endPos + "MB");
                
                // 模拟下载
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                System.out.println("线程" + threadId + "下载完成!");
            });
            
            threads[i].start();
        }
        
        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }
        
        System.out.println("文件下载完成!");
    }
}

输出:

线程0开始下载:0MB - 25MB
线程1开始下载:25MB - 50MB
线程2开始下载:50MB - 75MB
线程3开始下载:75MB - 100MB
线程0下载完成!
线程1下载完成!
线程2下载完成!
线程3下载完成!
文件下载完成!

九、常见面试题

Q1:创建线程有几种方式?

答: 两种基本方式:

  1. 继承Thread类
  2. 实现Runnable接口(推荐)

扩展:还可以用Callable + FutureTask(有返回值)、线程池等。

Q2:start()和run()的区别?

答:

  • start():启动新线程,JVM调用run()方法
  • run():在当前线程中执行,相当于普通方法调用

Q3:线程有哪几种状态?

答: 6种状态:

  1. NEW(新建)
  2. RUNNABLE(可运行)
  3. BLOCKED(阻塞)
  4. WAITING(等待)
  5. TIMED_WAITING(超时等待)
  6. TERMINATED(终止)

Q4:sleep()和wait()的区别?

答:

  • sleep():Thread类的方法,不释放锁,时间到自动恢复
  • wait():Object类的方法,释放锁,需要notify()唤醒

Q5:如何停止一个线程?

答:

  • ❌ 不要用stop()(已废弃,不安全)
  • ✅ 用标志位控制:
public class StopThreadDemo {
    private volatile boolean running = true;
    
    public void stop() {
        running = false;
    }
    
    public void run() {
        while (running) {
            // 执行任务
        }
    }
}

十、总结

本文我们学习了Java多线程的基础知识:

创建线程:Thread vs Runnable,优先用Runnable
线程状态:6种状态及转换关系
核心方法:start()、run()、sleep()、join()、yield()
守护线程:为其他线程服务,主线程结束时自动终止
实战案例:多线程下载文件

下一篇预告:《深入理解ThreadPoolExecutor的7个参数》


参考资料


如果这篇文章对你有帮助,欢迎点赞、收藏、关注!
有问题欢迎评论区讨论,我会及时回复。

作者:从CRUD到架构师
系列:Java并发编程实战
下一篇:《深入理解ThreadPoolExecutor的7个参数》