0

0

Java中如何创建和启动线程

P粉602998670

P粉602998670

发布时间:2025-09-22 13:45:01

|

383人浏览过

|

来源于php中文网

原创

答案:Java中创建和启动线程需定义任务并调用start()方法。可通过实现Runnable接口或继承Thread类定义任务,前者更灵活且推荐;启动时调用start()而非run(),因start()由JVM创建新线程并执行run(),而直接调用run()仅在当前线程执行,无并发效果。

java中如何创建和启动线程

Java中创建和启动线程,核心思路其实很简单:你需要定义一个线程要执行的任务(也就是它要“跑”的代码),然后把这个任务交给一个线程对象,最后让这个线程对象“动起来”。具体来说,我们通常通过实现

Runnable
接口或者继承
Thread
类来定义任务,再通过调用线程对象的
start()
方法来真正启动它,让它在独立的执行路径上运行。

解决方案

在Java里创建并启动线程,最常见且推荐的做法有两种:实现

Runnable
接口,或者继承
Thread
类。

1. 实现

Runnable
接口

这是更灵活也更推荐的方式。你定义一个类去实现

Runnable
接口,然后重写它的
run()
方法。这个
run()
方法里就是线程要执行的业务逻辑。

立即学习Java免费学习笔记(深入)”;

class MyRunnableTask implements Runnable {
    private String taskName;

    public MyRunnableTask(String name) {
        this.taskName = name;
    }

    @Override
    public void run() {
        // 这就是线程要执行的任务代码
        System.out.println(Thread.currentThread().getName() + " 正在执行任务: " + taskName);
        try {
            // 模拟任务执行耗时
            Thread.sleep(100);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 的任务被中断了。");
            Thread.currentThread().interrupt(); // 重新设置中断状态
        }
        System.out.println(Thread.currentThread().getName() + " 任务 " + taskName + " 完成。");
    }
}

// 如何启动:
// MyRunnableTask task1 = new MyRunnableTask("任务A");
// Thread thread1 = new Thread(task1, "工作线程-A"); // 将任务封装进Thread对象
// thread1.start(); // 启动线程

这种方式的好处在于,你的任务类可以继续继承其他类,因为Java是单继承的。同时,多个

Thread
对象可以共享同一个
Runnable
实例,这在需要共享数据或状态的场景下非常有用。

2. 继承

Thread

另一种方式是直接创建一个类继承

Thread
类,然后重写其
run()
方法。

class MyThreadWorker extends Thread {
    private String workerName;

    public MyThreadWorker(String name) {
        super(name); // 调用父类构造器设置线程名
        this.workerName = name;
    }

    @Override
    public void run() {
        // 线程的业务逻辑
        System.out.println(Thread.currentThread().getName() + " 启动,作为工作者: " + workerName);
        try {
            Thread.sleep(150);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 被中断了。");
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() + " 完成工作。");
    }
}

// 如何启动:
// MyThreadWorker worker1 = new MyThreadWorker("专属工人-1");
// worker1.start(); // 直接启动Thread子类实例

这种方式虽然也能工作,但由于Java的单继承限制,如果你的类还需要继承其他父类,就不能再继承

Thread
了。所以,我个人更倾向于使用
Runnable
接口。

3. 使用Lambda表达式(Java 8+)

对于简单的任务,我们还可以结合Lambda表达式来创建和启动线程,代码会更加简洁:

// new Thread(() -> {
//     System.out.println(Thread.currentThread().getName() + " 正在执行一个匿名任务。");
//     // ... 任务逻辑 ...
// }).start();

无论哪种方式,关键都是调用

start()
方法。

为什么不直接调用run()方法,而是要用start()?

这个问题其实挺基础,但很多初学者可能都会在这里犯迷糊。说实话,我刚开始学的时候也纳闷过,

run()
方法就在那儿,为什么非要绕个弯子去调用
start()
呢?

答案是:

start()
方法才是真正创建新线程的关键,而
run()
方法只是一个普通的方法调用。

当你调用

thread.start()
时,JVM(Java虚拟机)会做几件事:

  1. 它会向操作系统申请创建一个新的线程。
  2. 这个新线程会被分配它自己的调用(call stack)。
  3. 一旦新线程创建成功并准备就绪,JVM会安排它去执行你
    Runnable
    Thread
    子类中定义的那个
    run()
    方法。这个
    run()
    方法就会在新创建的线程上独立运行。

但如果你直接调用

thread.run()
呢?那它就和调用任何其他普通Java方法没什么两样了。
run()
方法会在当前的线程上执行,根本不会创建新的线程。这意味着你的代码仍然是单线程执行的,完全失去了多线程并发的意义。

举个例子,你有一个

MyRunnableTask
实例
task

  • new Thread(task).start();
    :这会启动一个全新的线程,让
    task.run()
    在新线程上跑。
  • task.run();
    :这只是在当前线程(比如主线程)上顺序执行
    task
    run()
    方法,没有任何并发可言。

所以,记住,

start()
是启动线程的“发令枪”,它告诉JVM“我需要一个新的执行路径”,而
run()
仅仅是新路径上要执行的“指令集”。

线程的生命周期是怎样的?有哪些状态?

线程的生命周期,在我看来,就像是一个人的成长过程,从出生到消亡,中间会经历各种状态。理解这些状态对于调试和优化多线程程序至关重要。Java的

Thread.State
枚举定义了六种线程状态:

AI Web Designer
AI Web Designer

AI网页设计师,快速生成个性化的网站设计

下载
  1. NEW (新建) 当一个

    Thread
    对象被创建,但尚未调用
    start()
    方法时,它就处于NEW状态。就像一个人刚出生,但还没开始他的人生旅程。

    Thread t = new Thread(() -> System.out.println("Hello")); // t 处于 NEW 状态
  2. RUNNABLE (可运行/运行中) 当线程调用了

    start()
    方法后,它就进入了RUNNABLE状态。这意味着线程可能正在JVM中运行,或者正在等待操作系统调度器分配CPU时间片。Java并不区分“可运行”和“正在运行”,都归为RUNNABLE。

    t.start(); // t 进入 RUNNABLE 状态
  3. BLOCKED (阻塞) 当线程试图获取一个内部锁(

    synchronized
    关键字)但该锁被其他线程持有时,线程会进入BLOCKED状态。它在等待进入一个
    synchronized
    块或方法。

    // 假设有两个线程同时尝试进入一个同步方法
    public synchronized void syncMethod() {
        // ...
    }
    // 如果一个线程在执行syncMethod,另一个线程调用syncMethod就会进入BLOCKED状态
  4. WAITING (等待) 线程进入WAITING状态通常是因为调用了以下方法之一:

    • Object.wait()
      (不带超时参数)
    • Thread.join()
      (不带超时参数)
    • LockSupport.park()
      处于WAITING状态的线程会一直等待,直到被其他线程显式地唤醒(例如通过
      notify()
      notifyAll()
      join
      的线程执行完毕)。
    // 线程A调用 obj.wait();
    // 线程B调用 obj.notify(); 才能唤醒线程A
  5. TIMED_WAITING (定时等待) 与WAITING类似,但它会等待一个指定的时间。如果时间到了,即使没有被其他线程唤醒,线程也会自动回到RUNNABLE状态。进入TIMED_WAITING状态的方法包括:

    • Thread.sleep(long millis)
    • Object.wait(long millis)
    • Thread.join(long millis)
    • LockSupport.parkNanos(long nanos)
    • LockSupport.parkUntil(long deadline)
    Thread.sleep(1000); // 线程进入 TIMED_WAITING 状态
  6. TERMINATED (终止) 当线程的

    run()
    方法执行完毕,或者因未捕获的异常而退出时,线程就进入了TERMINATED状态。线程一旦进入这个状态,就不能再被重新启动了。

    // 当 t 的 run() 方法执行完毕后,t 就会进入 TERMINATED 状态

理解这些状态以及它们之间的转换条件,对于诊断多线程程序的性能问题(比如死锁、活锁、线程饥饿)非常关键。在实际开发中,我经常会用JStack或者IDE的调试工具来查看线程的当前状态,这能帮助我快速定位问题。

在多线程编程中,有哪些常见的线程安全问题及解决方案?

多线程编程就像是在一个厨房里,多个厨师(线程)同时操作食材(共享资源)。如果大家各干各的,不注意协作,就很容易出问题。线程安全问题是多线程编程中避不开的坎,我个人觉得,理解这些问题以及对应的解决方案,是写出健壮并发程序的基石。

常见的线程安全问题:

  1. 竞态条件 (Race Condition): 这是最常见的问题。当多个线程尝试访问和修改同一个共享资源(比如一个变量、一个集合)时,如果操作的最终结果依赖于这些线程执行的相对时序,就可能发生竞态条件。结果往往是不可预测的、错误的。 例子: 多个线程同时对一个计数器

    i++
    i++
    不是原子操作,它包含读取、修改、写入三个步骤。如果线程A读取了10,线程B也读取了10,然后各自加1写入11,那么最终结果就不是12,而是11。

  2. 死锁 (Deadlock): 两个或更多的线程被无限期地阻塞,互相等待对方释放资源。这就像两个哲学家,每人拿着一只筷子,都在等对方放下另一只筷子才能吃饭,结果谁也吃不了。 死锁发生的四个必要条件:

    • 互斥条件: 资源不能共享,只能被一个线程占用。
    • 请求与保持条件: 线程已经持有了至少一个资源,但又请求新的资源,同时不释放已持有的资源。
    • 不剥夺条件: 已经分配给一个线程的资源不能被强制性地剥夺。
    • 循环等待条件: 存在一个线程资源的循环链,每个线程都在等待链中下一个线程所持有的资源。
  3. 活锁 (Livelock): 线程虽然没有被阻塞,但它们却在不断地改变状态以响应其他线程,导致没有任何实际的进展。它比死锁更隐蔽,因为线程看起来是活跃的,但实际上是无效的忙碌。 例子: 两个人过窄桥,同时走到中间,都想给对方让路,于是同时向左,又同时向右,结果谁也过不去。

  4. 饥饿 (Starvation): 一个或多个线程由于调度策略不公平,或者优先级太低,或者总是得不到所需的资源,而导致它们永远无法获得CPU时间或资源来执行任务。

解决方案:

针对这些问题,Java提供了丰富的工具和机制:

  1. synchronized
    关键字: 这是Java内置的同步机制,可以用于方法或代码块。它确保在任何给定时间,只有一个线程可以执行被
    synchronized
    保护的代码。它提供了互斥性和内存可见性。

    // 同步方法
    public synchronized void increment() {
        count++;
    }
    
    // 同步代码块
    public void update() {
        synchronized (this) { // 或 synchronized (someObject)
            // 访问共享资源的代码
        }
    }

    synchronized
    用起来很方便,但它是一种“粗粒度”的锁,有时候可能会导致性能瓶颈

  2. java.util.concurrent.locks
    包: 提供了更高级、更灵活的锁机制,比如
    ReentrantLock
    ReadWriteLock

    • ReentrantLock
      可重入锁,比
      synchronized
      更灵活,可以尝试获取锁(
      tryLock()
      )、可中断地获取锁(
      lockInterruptibly()
      )、公平锁等。
      // ReentrantLock lock = new ReentrantLock();
      // lock.lock();
      // try {
      //     // 访问共享资源
      // } finally {
      //     lock.unlock();
      // }
    • ReadWriteLock
      读写锁,允许多个读线程同时访问,但写线程是独占的。这对于读多写少的场景性能提升非常明显。
  3. java.util.concurrent.atomic
    包: 提供了原子操作类,如
    AtomicInteger
    AtomicLong
    AtomicReference
    等。这些类利用CAS(Compare-And-Swap)操作实现无锁(Lock-Free)的线程安全。它们比使用锁的性能更高,因为避免了线程上下文切换的开销。

    // AtomicInteger counter = new AtomicInteger(0);
    // counter.incrementAndGet(); // 原子地执行 i++
  4. volatile
    关键字:
    volatile
    主要保证了内存可见性,即一个线程对
    volatile
    变量的修改,对其他线程是立即可见的。它也能防止指令重排序。但它不保证原子性,所以不能替代
    synchronized
    或原子类来解决竞态条件。

    // public volatile boolean flag = false;
    // 当一个线程修改 flag 为 true 时,其他线程能立即看到这个变化。
  5. 并发集合 (Concurrent Collections):

    java.util.concurrent
    包提供了许多线程安全的集合类,如
    ConcurrentHashMap
    CopyOnWriteArrayList
    BlockingQueue
    等。这些集合内部已经处理了线程安全问题,使用它们通常比手动加锁更高效、更安全。

    // ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
    // map.put("key", "value"); // 线程安全
  6. 不可变对象 (Immutable Objects): 如果一个对象在创建后其状态就不能再被修改,那么它就是线程安全的。例如

    String
    类就是不可变的。创建不可变对象可以彻底避免共享状态的竞态条件。

  7. ThreadLocal
    为每个线程提供一个独立的变量副本。这样每个线程操作的都是自己的副本,互不影响,自然也就没有线程安全问题。

    // private static ThreadLocal<Integer> threadCount = ThreadLocal.withInitial(() -> 0);
    // threadCount.get(); // 获取当前线程的副本
    // threadCount.set(1); // 设置当前线程的副本

在实际开发中,选择哪种解决方案,往往需要根据具体场景、性能要求以及代码的复杂性来权衡。我通常会优先考虑使用并发集合和原子类,如果不行再考虑

ReentrantLock
,最后才是
synchronized
。避免死锁则需要仔细设计锁的获取顺序,以及尽量减少锁的持有时间。

如何选择实现Runnable接口还是继承Thread类?

这确实是一个老生常谈的问题,但它背后的考量却很实际。我个人的经验是,在绝大多数情况下,实现

Runnable
接口是更优的选择

我们来深入分析一下:

1. 实现

Runnable
接口的优势:

  • 避免Java单继承的限制: 这是最主要的原因。Java只支持单继承,如果你的类已经继承了另一个父类,那么它就不能再继承
    Thread
    类了。而实现接口则没有这个限制,你的任务类可以同时继承其他类并实现
    Runnable
    接口。这在复杂的业务场景中非常重要。
    // class MyBusinessLogic extends SomeBaseClass implements Runnable { ... }
    // 这种组合在继承Thread时是不可能实现的。
  • 任务与执行者解耦: 实现
    Runnable
    接口意味着你定义的是一个“任务”(What to do),而不是一个“线程”(How to do it)。
    Thread
    类代表的是一个执行者,它封装了线程的创建和管理逻辑。这种解耦让你的代码更清晰,更符合面向对象的设计原则。一个
    Runnable
    实例可以被多个
    Thread
    实例共享,或者被线程池复用。
    // MyRunnableTask task = new MyRunnableTask();
    // new Thread(task).start(); // 一个任务
    // new Thread(task).start(); // 另一个线程执行同一个任务实例
  • 更适合资源共享: 当多个线程需要处理同一个任务实例中的数据时,实现
    Runnable
    接口可以方便地将同一个
    Runnable
    实例传递给多个
    Thread
    对象。这样,这些线程就可以共享
    Runnable
    实例的成员变量,从而实现数据共享。
    // Counter counter = new Counter(); // 共享的计数器实例
    // new Thread(new MyRunnable(counter)).start();
    // new Thread(new MyRunnable(counter)).start();
    // 两个线程操作同一个 counter 对象
  • 更好的可测试性: 任务(
    Runnable
    )是纯粹的业务逻辑,不涉及线程创建和管理,因此更容易进行单元测试。

2. 继承

Thread
类的劣势:

  • 单继承限制: 这是最大的缺点,前面已经提到了。
  • 耦合度高: 任务逻辑与线程本身紧密耦合在一起。如果你想复用任务逻辑,但又不想每次都创建一个新的
    Thread
    子类实例,就会比较麻烦。
  • 不适合线程池: 在使用线程池时,通常是提交
    Runnable
    (或
    Callable
    )任务,而不是
    Thread
    子类实例。线程池内部会管理
    Thread
    对象,你只需要提供任务。

什么时候可以考虑继承

Thread
类?

坦白说,我几乎很少直接继承

Thread
类。但如果你的场景非常简单,你的类不需要继承其他任何类,并且这个线程的生命周期和任务逻辑是完全一体的,不需要解耦,那么继承
Thread
也并非不可。它可能在某些极度简单的示例代码中看起来更直接。

总结我的选择偏好:

在实际开发中,我几乎总是会选择实现

Runnable
接口。它提供了更好的灵活性、可维护性和扩展性。尤其是在现代Java应用中,我们更多地会使用
ExecutorService
(线程池)来管理线程,而
ExecutorService
通常接受
Runnable
Callable
作为任务。所以,养成实现
Runnable
的习惯,会让你的并发编程之路走得更顺畅。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1051

2023.08.02

go语言 面向对象
go语言 面向对象

本专题整合了go语言面向对象相关内容,阅读专题下面的文章了解更多详细内容。

58

2025.09.05

java面向对象
java面向对象

本专题整合了java面向对象相关内容,阅读专题下面的文章了解更多详细内容。

63

2025.11.27

c++中volatile关键字的作用
c++中volatile关键字的作用

本专题整合了c++中volatile关键字的相关内容,阅读专题下面的文章了解更多详细内容。

76

2025.10.23

lambda表达式
lambda表达式

Lambda表达式是一种匿名函数的简洁表示方式,它可以在需要函数作为参数的地方使用,并提供了一种更简洁、更灵活的编码方式,其语法为“lambda 参数列表: 表达式”,参数列表是函数的参数,可以包含一个或多个参数,用逗号分隔,表达式是函数的执行体,用于定义函数的具体操作。本专题为大家提供lambda表达式相关的文章、下载、课程内容,供大家免费下载体验。

215

2023.09.15

python lambda函数
python lambda函数

本专题整合了python lambda函数用法详解,阅读专题下面的文章了解更多详细内容。

193

2025.11.08

Python lambda详解
Python lambda详解

本专题整合了Python lambda函数相关教程,阅读下面的文章了解更多详细内容。

61

2026.01.05

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1958

2023.10.19

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.3万人学习

Java 教程
Java 教程

共578课时 | 81.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号