Day35 | Java多线程入门

本文开始,我们进入Java多线程模块。

现代计算机的CPU基本都是多个核心,每个核心都可以独立执行计算任务。

所谓的多线程编程,就是允许我们的程序创建多个执行流(线程),然后把它们分配到不同的CPU核心上同时运行,从而充分利用硬件资源,提高程序的响应速度和处理能力。

一、基本概念

在学习Java的多线程之前,我们先弄清楚一些基本的概念。

进程:

操作系统资源分配的最小单位,拥有独立的内存空间。一个程序运行起来就是一个进程。

你在电脑上双击wechat.exe启动微信的时候,系统就创建了一个微信进程。这个进程有自己的内存空间和资源。

线程:

线程是进程中的执行单元,同一个进程可以有很多个线程,这些线程共享内存资源,用来同时执行多个任务。

微信里面一个线程负责收消息,另外一个线程可能负责渲染聊天界面,他们都是属于同一个微信进程的。

虚拟线程:

JDK21正式引入了虚拟线程这个类似协程的机制。虚拟线程由Java自己调度,不需要操作系统的管理。

大家都在微信上发消息、看朋友圈、登录,如果微信服务器给每个用户分配一个轻量的虚拟线程,就不会像传统的线程那样,每个用户都占用一个系统线程,资源就不会那么紧张。

并发:

多个任务在同一时间段交替执行,提高效率,看起来是同时进行,但实际上可能是轮流执行。

你在微信上发语音的同时,微信还能加载朋友圈,其实两个操作在一个线程里交替完成。

并行:

多个任务在同一时刻真正同时执行,依赖于多核CPU支持。

打开微信和打开浏览器两个进程分别运行在不同CPU核心上,彼此互不干扰,同时运行。

二、创建线程

接下来我们看一下在Java中如何创建线程。

2.1 继承Thread

这是最直观的创建线程的方式。Thread类是Java提供的用来创建和控制线程的核心类。

他代表的就是一个独立执行的任务单元。

java 复制代码
package com.lazy.snail.day35;

/**
   * @ClassName Day35Demo 
   * @Description TODO
   * @Author lazysnail
   * @Date 2025/7/17 10:38
   * @Version 1.0
   */
public class Day35Demo {
    public static void main(String[] args) {
        System.out.println("主线程开始...");
        MyThread thread1 = new MyThread();
        thread1.setName("我的线程");
        thread1.start();
        System.out.println("主线程结束。");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程 " + Thread.currentThread().getName() + " 正在运行: " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

MyThread是我自定义的线程类,继承了Thread。

这个类中只做了一件事情,重写run方法。这个方法中的内容其实就是你想让线程帮你执行的逻辑。

在main方法中创建了自定义线程的对象,通过对象调用start方法,那么这个线程就启动了。

初学者容易把run方法和start方法搞混。记住run方法是你需要交给线程执行的任务逻辑。

start方法是启动线程,相当于激活这个线程。

如果你直接用线程对象调用了run方法,像这样:thread1.run。

就不会创建新的线程去执行你的逻辑,只是在当前线程中执行run()方法体,变成了普通的同步方法调用。

正常启动线程的输出

直接调用了run方法的输出

两者输出的对比,后者其实是按照代码顺序执行的。前者主线程和新启动的线程独立执行。

main方法是一个独立的线程,当你在main方法某处创建启动了新的线程,这个新线程在某一时刻也会执行自己的逻辑。 正常启动线程的输出也不是一成不变的,你的机器上或者你多运行几次,可能输出的顺序就会发生改变。因为线程的调度权在操作系统手里。操作系统会根据系统负载,CPU状态等等情况合理的调度线程。我们是没办法预测多线程精确的执行顺序的。

2.2 实现Runnable

我们都知道Java有单继承的限制,这在某些情况下就不够灵活。

所以在有需要的情况下,我们一般都会选择实现Runnable的方式。

java 复制代码
package com.lazy.snail.day35;

/**
 * @ClassName Day35Demo2
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/7/17 11:19
 * @Version 1.0
 */
public class Day35Demo2 {
    public static void main(String[] args) {
        System.out.println("主线程开始...");
        MyRunnable task = new MyRunnable();
        Thread thread1 = new Thread(task, "我的线程");
        thread1.start();
        System.out.println("主线程结束。");
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程 " + Thread.currentThread().getName() + " 正在运行: " + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

MyRunnable实现Runnable接口,类中也只做了一件事情,就是实现run方法。

在main方法中,需要创建一个Thread实例,然后把Runnable实现类的实例喂给他。

启动线程一样是通过线程实例的start方法。

这种写法的好处就在于把要执行的任务(Runnable)和执行任务的载体(Thread)解耦了。

如果你去翻看过Thread的源码,你会发现Thread类也实现了Runnable接口

从本质上来说,Thread是用来构建线程的,Runnable是用来包装任务的。

三、线程的生命周期

生命周期就是一个事物从诞生到消亡的过程中所经历的一系列阶段。

线程从创建到终止会经历不同的状态。

对于开发者来说,这些状态可以让我们清晰精准的控制程序的运行。

对于操作系统和虚拟机来说,这些状态可以让他们合理的调度CPU资源。

Thread.State是Thread类中的线程状态枚举。

在Java层面,线程的状态被抽象成了6个:

NEW:新建,new Thread()之后,线程对象已经创建,但是还没调用start()方法。

RUNNABLE:可运行,调用start()方法后,线程就准备就绪了,等待CPU的调度。这个状态包含了我们通常理解的正在运行和准备运行两种状态。

BLOCKED:阻塞,线程在等待锁资源,比如想要进入一个synchronized块,此时锁被其他线程持有,只能等着。

WAITING:无限等待,线程无限期的等待另一个线程的通知。wait()方法和join()方法都可以让线程记入WAITING状态。

TIMED_WAITING:限时等待,在指定时间后会自动唤醒的等待状态。sleep(long)、wait(long)、join(long)等方法可以让线程进入这个状态。

TERMINATED:终止,线程的run()方法执行完或因为异常退出,线程生命周期就结束了。

这几个状态是Java虚拟机在Java层面提供的逻辑视图,主要是方便我们在写多线程代码的时候更好的理解和控制线程。

在操作系统层面,比如Linux中,他更关心的是调度和资源的控制。

Running:正在CPU上执行。

Runnable (Ready):可运行,等待被调度。

Blocked/Sleeping:因等待资源、IO、锁等而挂起。

Zombie:已退出,但父进程尚未回收。

Stopped:被外部信号暂停。

这几个状态是Linux操作系统调度器和内核真实管理线程时使用的状态。

二者对比,虽然不完全等价,但是存在一定的映射关系。

一个大致的映射关系

Java线程状态 操作系统线程可能状态(粗略)
NEW 不存在(还未调用start())
RUNNABLE Ready/Running(可运行或正在运行)
BLOCKED Sleeping(等待锁)
WAITING Sleeping(无限等待,需其他线程唤醒)
TIMED_WAITING Sleeping(限时挂起)
TERMINATED Zombie(Java层已销毁,可能系统层还未清理)

Java中的RUNNABLE包括了操作系统的Ready + Running,没有细分。

四、Thread类的常用方法

Thread类是学习Java多线程的一个基础,他是创建、启动、管理线程的核心类,几乎所有多线程操作的入口都离不开它。

Thread对象内部包装了一个原生操作系统线程(JVM映射),是Java抽象线程和系统调度机制之间的桥梁。

后续的线程池、虚拟线程、并发工具类等等的学习都需要建立在对Thread机制的理解之上。

下面我们一起看下Thread类中一些常见的方法:

start()

这个方法用来启动线程,触发 run() 方法执行。只有调用start(),线程才会真正并发执行。

java 复制代码
Thread t = new Thread(() -> {
    System.out.println("子线程正在运行...");
});
t.start();

需要注意的是start()只能调用一次;重复调用会抛出IllegalThreadStateException。

它会在新的线程中执行run(),不要手动调用run()。

run()

线程启动之后执行的实际逻辑代码,要么是自己覆写的,要么是通过Lambda传入的。

java 复制代码
Thread t = new Thread(() -> {
    System.out.println("这是run()方法里的代码");
});
t.start();

run()本身不具有并发性,直接调用run()只是普通方法执行,不会启动线程。

sleep(long millis)

让当前线程休眠指定毫秒数,不释放锁。

java 复制代码
System.out.println("开始");
Thread.sleep(1000);
System.out.println("1秒后执行");

Thread.sleep()休眠的是当前的线程,不是目标线程。

会抛出InterruptedException,需要捕获或者抛出。

不会释放资源,容易出现并发问题。

join()

等待另一个线程执行完成后再继续执行当前线程。

java 复制代码
Thread t = new Thread(() -> {
    try {
        Thread.sleep(1000);
        System.out.println("子线程执行完毕");
    } catch (InterruptedException e) {}
});

t.start();
t.join();
System.out.println("主线程继续执行");

主线程等待子线程执行完成。

这个方法会让当前线程进入WAITING状态。

也可以通过join(long timeout)方法,让线程进入TIMED_WAITING状态。

interrupt()

中断线程的休眠或等待状态,这个方法不会强制停止线程,只是设置一个中断标志。

java 复制代码
Thread t = new Thread(() -> {
    try {
        Thread.sleep(5000);
        System.out.println("醒来继续执行");
    } catch (InterruptedException e) {
        System.out.println("被中断了!");
    }
});
t.start();

Thread.sleep(1000);
t.interrupt();

这个方法对处于sleep()、wait()、join()状态的线程有效,会抛出InterruptedException。

对于正常运行中的线程,只是设置一个标志位,要手动检查:

java 复制代码
while (!Thread.currentThread().isInterrupted()) { ... }

isAlive()

判断线程是否仍处于活动状态(已启动但还没终止)。

java 复制代码
Thread t = new Thread(() -> {});
System.out.println(t.isAlive());
t.start();
System.out.println(t.isAlive());

setPriority(int newPriority)

设置线程的优先级(范围是1~10,默认是5)。

java 复制代码
t.setPriority(Thread.MAX_PRIORITY);

JVM不保证高优先级线程一定先执行,只是作为调度建议。

在现代操作系统下基本没什么作用,不推荐依赖它来做控制。

getName()/setName(String name)

获取或设置线程的名称,主要是用来输出日志和调试。

java 复制代码
Thread t = new Thread(() -> {});
t.setName("我的线程");
System.out.println(t.getName());

currentThread()

用来获取当前正在执行的线程对象。

java 复制代码
Thread t = Thread.currentThread();
System.out.println("当前线程:" + t.getName());

setDaemon(boolean on)

把线程设置成守护线程。

当JVM里只剩下守护线程的时候,JVM会直接退出,不会等待守护线程执行完。

像日志记录、监控这种后台服务比较常用。

垃圾回收线程(GC)就是一个守护线程。

java 复制代码
public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("守护线程运行中...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
            }
        });

        t.setDaemon(true);
        t.start();

        try {
            Thread.sleep(1000);
            System.out.println("主线程结束");
        } catch (InterruptedException e) {}
    }

主线程结束之后,JVM不会管守护线程,守护线程的while循环也不再执行了。

结语

今天讲了一些Java多线程的基础知识,这些都是并发程编程的基础。

并发编程也不是什么高大上或者进阶的东西,可以把他看成一个简单的模块。

掌握多线程要转变一下思维,之前都是独自按顺序完成任务,现在把任务分发给更多的人一起完成。

怎么让这些人按照有序的规则,高效的协作,共同完成任务。

这些规则其实就是我们关心的,我们需要掌握的,一旦掌握了,那么你写的代码就是安全、高效的。

下一篇预告

Day36 | Java中的线程池技术

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

更多文章请关注我的公众号《懒惰蜗牛工坊》

相关推荐
啊哈灵机一动2 小时前
手把手实现 Gin + Socket.IO 实时聊天功能
后端
qq_12498707532 小时前
基于微信小程序的科技助农系统的设计与实现(源码+论文+部署+安装)
java·大数据·spring boot·后端·科技·微信小程序·毕业设计
『六哥』2 小时前
IntelliJ IDEA 安装教程
java·ide·intellij-idea·intellij idea
艾迪的技术之路2 小时前
【实践】2025年线上问题解决与总结-1
java
哈哈老师啊2 小时前
Springboot新冠检测信息管理系统10m6v(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
华仔啊2 小时前
ArrayList 和 LinkedList 的区别?一篇讲透,从此开发和面试都不再纠结
java·后端
回家路上绕了弯2 小时前
分布式系统重试策略详解:可靠性与资源消耗的平衡艺术
分布式·后端
王中阳Go2 小时前
别再卷 Python 了!Go + 字节 Eino 框架,才是后端人转 AI 的降维打击(附源码)
后端·面试·go
superman超哥2 小时前
Rust 表达式与语句的区别:函数式思维与控制流设计
开发语言·后端·rust·rust表达式·rust语句·函数式思维·控制流设计