首页 > Java > java教程 > 正文

Java中如何创建和启动线程

P粉602998670
发布: 2025-09-22 13:45:01
原创
340人浏览过
答案: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
登录后复制
枚举定义了六种线程状态:

阿里妈妈·创意中心
阿里妈妈·创意中心

阿里妈妈营销创意中心

阿里妈妈·创意中心 0
查看详情 阿里妈妈·创意中心
  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
登录后复制
的习惯,会让你的并发编程之路走得更顺畅。

以上就是Java中如何创建和启动线程的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
热门推荐
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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