当前位置:首页 > 文章列表 > 文章 > java教程 > Java创建线程的三种方式详解

Java创建线程的三种方式详解

2025-12-24 21:35:33 0浏览 收藏

怎么入门文章编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《Java创建启动线程的方法详解》,涉及到,有需要的可以收藏一下

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

Java中如何创建和启动线程

Java中创建和启动线程,核心思路其实很简单:你需要定义一个线程要执行的任务(也就是它要“跑”的代码),然后把这个任务交给一个线程对象,最后让这个线程对象“动起来”。具体来说,我们通常通过实现Runnable接口或者继承Thread类来定义任务,再通过调用线程对象的start()方法来真正启动它,让它在独立的执行路径上运行。

解决方案

在Java里创建并启动线程,最常见且推荐的做法有两种:实现Runnable接口,或者继承Thread类。

1. 实现Runnable接口

这是更灵活也更推荐的方式。你定义一个类去实现Runnable接口,然后重写它的run()方法。这个run()方法里就是线程要执行的业务逻辑。

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会安排它去执行你RunnableThread子类中定义的那个run()方法。这个run()方法就会在新创建的线程上独立运行。

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

举个例子,你有一个MyRunnableTask实例task

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

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

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

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

  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 包: 提供了更高级、更灵活的锁机制,比如ReentrantLockReadWriteLock

    • ReentrantLock 可重入锁,比synchronized更灵活,可以尝试获取锁(tryLock())、可中断地获取锁(lockInterruptibly())、公平锁等。
      // ReentrantLock lock = new ReentrantLock();
      // lock.lock();
      // try {
      //     // 访问共享资源
      // } finally {
      //     lock.unlock();
      // }
    • ReadWriteLock 读写锁,允许多个读线程同时访问,但写线程是独占的。这对于读多写少的场景性能提升非常明显。
  3. java.util.concurrent.atomic 包: 提供了原子操作类,如AtomicIntegerAtomicLongAtomicReference等。这些类利用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包提供了许多线程安全的集合类,如ConcurrentHashMapCopyOnWriteArrayListBlockingQueue等。这些集合内部已经处理了线程安全问题,使用它们通常比手动加锁更高效、更安全。

    // 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通常接受RunnableCallable作为任务。所以,养成实现Runnable的习惯,会让你的并发编程之路走得更顺畅。

今天带大家了解了的相关知识,希望对你有所帮助;关于文章的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~

按钮组间距不均?用space-between轻松解决按钮组间距不均?用space-between轻松解决
上一篇
按钮组间距不均?用space-between轻松解决
海外看优酷视频的实用方法分享
下一篇
海外看优酷视频的实用方法分享
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3411次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3619次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3653次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    4788次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    4020次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码