一、认识线程:什么是线程,为什么需要线程?

1.1 线程的本质:独立的执行流

线程(Thread)是进程内的执行单元,一个线程对应一条独立的执行流。多个线程可以“同时”执行不同的代码逻辑

举个例子:一家公司办理业务时,会计张三、李四、王五分别负责转账、发福利、缴社保——三人都是独立执行流,但共同完成公司的整体任务,其中张三可视为“主线程”,李四和王五是“子线程”。

特性:

  • 每个线程有自己的执行顺序,按代码逻辑逐步执行;
  • 多线程的“同时”是宏观上的,微观上CPU通过时间片轮转实现切换(单核CPU),多核CPU可真正并行执行;
  • 线程依托于进程存在,一个进程至少包含一个主线程(Java程序的main线程就是主线程)。

1.2 为什么需要线程?

线程的存在,本质是为了解决效率问题,主要体现在三个层面:

  1. 应对硬件发展:充分利用多核CPU
    单核CPU的性能提升已遇瓶颈,现在主流服务器、PC都是多核架构。若程序是单线程,即使有8核CPU,也只能用到1核资源;而多线程可将任务拆分到不同核上并行执行,直接提升算力利用率。

  2. 优化IO等待:减少时间浪费
    很多程序存在大量IO操作(读取文件、网络请求),IO操作的速度远慢于CPU运算。

    • 举个例子:一个线程发起网络请求后,需要等待1秒才能收到响应——若此时该线程阻塞等待,CPU就处于空闲状态。而多线程可在一个线程等待IO时,让其他线程执行计算任务,把等待时间充分利用起来。
  3. 比进程更轻量:降低资源开销
    进程是系统分配资源的最小单位,线程是系统调度的最小单位。相比进程,线程的创建、销毁、调度成本更低:

    • 创建线程:无需为其分配独立的内存空间(共享进程的内存),仅需少量栈空间;
    • 销毁线程:释放的资源远少于进程;
    • 调度线程:无需切换进程地址空间,上下文切换速度更快。

1.3 进程与线程的区别

通过“银行办理业务”的例子理解:

对比维度 进程 线程
资源分配 系统分配资源的最小单位(如内存、文件句柄) 不独立分配资源,共享进程的内存空间
内存隔离 进程间内存完全隔离(如A客户的业务数据不会被B客户看到) 同一进程的线程共享内存(如张三、李四共享公司的业务票据)
调度单位 不是调度最小单位 系统调度的最小单位(CPU直接调度线程)
稳定性 一个进程崩溃,不影响其他进程 一个线程崩溃,可能导致整个进程崩溃(如李四的线程出错,公司业务整体中断)

增加线程一定程度上可以提高效率,但是线程数目达到一定程度后,线程的调度开销也会非常明显,而调度开销会拖慢程序性能

1.4 Java线程与操作系统线程的关系

Java中的Thread类并非“自创”线程,而是对操作系统线程的封装

  • 操作系统内核实现了线程的核心机制(如Linux的pthread库);
  • Java的Thread类通过JNI(Java Native Interface)调用操作系统的线程API,创建、管理线程;
  • 简单来说:Java线程是“壳”,操作系统线程是“核”,Java线程的状态、调度最终由操作系统控制。

Java中的Thread在Java.lang中,每个工程都默认导入,无需手动import


二、Java多线程

2.1 主线程与子线程并行

创建一个子线程,与主线程同时循环打印线程名称,两者随机交:)

public class demo01 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        //真正在系统中创建一个线程
        t.start();

        while(true){
            System.out.println("hello main");
            //sleep是Thread类的静态方法,不需要通过实例调用
            Thread.sleep(1000);
        }
    }
}

class MyThread extends Thread{
    //run方法 相当于线程的入口
    @Override
    public void run() {
        while(true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

  • start() vs run()
    • 调用start()会让JVM创建操作系统线程,再执行run()中的逻辑;若直接调用run(),只是普通方法调用,不会创建新线程,这样就会永远卡在run方法的死循环中,main方法内的循环不会进入。;
    • run()是线程的入口,不需要手动调用,在新线程创建好以后自动去执行,相当于回调函数
  • 执行结果:主线程(main)和子线程(Thread-0)的打印会随机交替,因为CPU会在两者间切换调度;
  • 可通过jconsole.exe查看线程状态。左侧都是当前进程中的线程。除了Thread-0外,其他是JVM内置的线程,启动任意一个Java进程,都会自带这些线程
    在这里插入图片描述
  • 堆栈追踪:线程的调用栈,获取线程状态的时刻,线程里的代码执行到哪一行

2.2 创建线程的4种方式

Java提供了多种创建线程的方式,核心是“定义线程要执行的逻辑(run()方法),具体可分为4种:

方式1:继承Thread类

  • 自定义类继承Thread,重写run(),创建实例后调用start()
  • 优点:实现简单,可直接使用this指代当前线程;
  • 缺点:Java单继承限制,若类已继承其他类,则无法使用此方式。

方式2:实现Runnable接口

  • 自定义类实现Runnable接口,重写run(),将实例传入Thread构造器,调用start()
// 1. 实现Runnable接口
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

// 2. 创建线程并启动
public class RunnableDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start(); 
    }
}
  • 优点:能够让执行任务本身和线程这个概念解耦和,后续修改代码(比如不通过线程而通过其他方式执行任务)只需要采用Runnable这样的方案,代码修改更简单(Runnable负责逻辑,Thread负责线程管理)
  • 缺点:需通过Thread.currentThread()获取当前线程,稍显繁琐。

方式3:匿名内部类

Thread

本质上就是方式1。若线程逻辑简单,无需单独定义类,可使用匿名内部类:

Thread r = new Thread(){
	//...
};

这个语句主要干了三件事:

  1. 创建一个Thread子类,没有名字
  2. { }; 内可以编写子类定义的代码,包括子类应该有哪些属性,哪些方法,重写父类方法等
  3. 创建了匿名内部类的实例,并且把实例的引用赋值给t
// 匿名内部类继承Thread
public class demo03 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();

        while(true) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

Runnable
Runnable runnable = new Runnable() {
		//...
};

与上面的Thread类似,同样做了三件事:

  1. 创建一个Runnable子类,没有名字
  2. { }; 内可以编写子类定义的代码,包括子类应该有哪些属性,哪些方法,重写方法等
  3. 创建了匿名内部类的实例,并且把实例的引用赋值给t

方式4:Lambda表达式

Runnable是函数式接口(只有一个抽象方法),可通过Lambda表达式简化:

// 单行逻辑
Thread t3 = new Thread(() -> System.out.println("Lambda线程1"));

// 多行逻辑
Thread t4 = new Thread(() -> {
    System.out.println("Lambda线程2");
    System.out.println("多行逻辑示例");
});

t3.start();
t4.start();
  • 优点:代码最简洁,适合短期使用的线程;
  • 缺点:逻辑复杂时可读性较差。

注意:lambda表达式如果使用了外部变量,就会触发变量捕获,这个变量应该是被final修饰或者赋值一次后不再修改

在这里插入图片描述

lambda表达式是回调函数,执行时机可能在很久之后,操作系统真正创建出线程才会执行。很有可能当这个线程创建好,main线程已经结束,对应的isFinished已经销毁。

为了防止上述问题,Java会把被捕获的变量拷贝一份,给lambda表达式使用,这样外部变量是否销毁就不会影响lambda表达式的执行。

拷贝意味着这个变量不适合修改,本质上这个外部变量和被拷贝的变量是两个变量,一个修改,另一个不会改变。因此这个变量就应该是不变的:被final修饰或赋值一次不再改变

  • 如果这个外部变量是引用类型呢?

在这里插入图片描述

这段代码中,test是局部变量,会随着main线程结束而销毁;而创建的Test()对象的生命周期由JVM中的垃圾回收管理,不存在main线程结束就销毁。因此可以修改这个对象的值,但是不可以修改test这个变量的指向。

  • 如果把外部变量改成成员变量

此时不再是“变量捕获”,而是“内部类访问外部类的成员”。

lambda表达式本质上是函数式接口,相当于一个内部类,内部类本来就可以访问外部类的成员,因为这个成员变量的生命周期也是由JVM垃圾回收管理,不需要担心生命周期失效的问题,也就不必拷贝,不需要限制final


三、Thread类详解:常用方法与线程属性

3.1 Thread的常见构造方法

通过构造方法,可指定线程的名称、关联的Runnable逻辑等,常用构造方法如下:

构造方法 说明
Thread() 创建一个默认名称的线程(如Thread-0
Thread(Runnable target) 创建线程,关联Runnable逻辑(核心逻辑由targetrun()实现)
Thread(String name) 创建指定名称的线程(便于调试,如Thread t = new Thread("转账线程")
Thread(Runnable target, String name) 关联Runnable逻辑,并指定线程名称

示例:

// 1. 默认构造
Thread t1 = new Thread();

// 2. 关联Runnable
Thread t2 = new Thread(new MyRunnable());

// 3. 指定名称
Thread t3 = new Thread("用户登录线程");

// 4. 关联Runnable+指定名称(推荐)
Thread t4 = new Thread(new MyRunnable(), "订单处理线程");

3.2 Thread的核心属性

属性 含义 获取方法 关键说明
ID 线程唯一标识 getId() 每个线程的ID唯一,由JVM分配,不可修改
名称 线程名称 getName() 可通过构造方法指定,默认Thread-n,用于调试(如jconsole中显示)
状态 线程当前状态 getState() 线程有6种状态(如NEWRUNNABLE),后续详细讲解
优先级 线程调度优先级 getPriority() 范围1-10,默认5,优先级高的线程更易被调度(但不保证)
后台线程 是否为后台线程 isDaemon() 后台线程:JVM退出时不等待其完成;前台线程:JVM需等待所有前台线程完成才退出
存活状态 是否存活 isAlive() 存活:线程已启动且未终止(run()未执行完);非存活:NEWTERMINATED状态
中断状态 是否被中断 isInterrupted() 表示线程是否收到中断信号,后续讲解“中断线程”时详细说明

前台线程 vs 后台线程

以下图为例

在这里插入图片描述

  • 前台线程:可以决定整个进程是否结束。所有前台线程都结束时,整个进程就可以结束了。main线程以及其他手动创建线程默认是前台线程,可以通过isDaemon()来需改属性
  • 后台线程:图中除了main和Thread-0以外都是JVM自带的线程,属于有特殊功能的线程,会跟随整个进程持续执行。这些属于后台线程,他们不能决定整个进程是否结束。如果前台线程全部结束,进程结束,那么后台线程也会随之结束。

示例:获取线程属性

public class ThreadAttrDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 运行中");
                    Thread.sleep(1000); // 休眠1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 即将终止");
        }, "测试线程");

        // 启动前获取属性(此时线程为NEW状态,未存活)
        System.out.println("启动前 - 名称:" + thread.getName());
        System.out.println("启动前 - ID:" + thread.getId());
        System.out.println("启动前 - 状态:" + thread.getState());
        System.out.println("启动前 - 是否存活:" + thread.isAlive());
        System.out.println("启动前 - 是否后台线程:" + thread.isDaemon());

        // 启动线程
        thread.start();

        // 等待线程执行完成
        while (thread.isAlive()) {}

        // 线程终止后获取属性
        System.out.println("终止后 - 状态:" + thread.getState());
        System.out.println("终止后 - 是否存活:" + thread.isAlive());
    }
}

在这里插入图片描述

  • 启动前:状态为NEWisAlive()false
  • 终止后:状态为TERMINATEDisAlive()false
  • 后台线程默认是false(前台线程),若要设置为后台线程,需在start()前调用thread.setDaemon(true)

3.3 启动线程

启动线程必须调用start(),而非run()。这是因为start()做了两件关键事情:

  1. 向JVM申请创建操作系统线程 ,将Java线程与操作系统线程关联;
  2. 操作系统线程创建成功后,由操作系统调度执行run()方法中的逻辑。

若直接调用run(),只是普通的方法调用,不会创建新线程——相当于主线程执行了run()中的代码,没有并发效果。

每个Thread对象和操作系统中的线程是一一对应的,每个Thread对象start方法只能调用一次

在这里插入图片描述

3.4 终止线程

线程启动后,若想让其停止,不能直接调用stop()方法,会强制终止线程,导致资源泄漏,需通过“协作式”方式停止。

常用的终止方式有两种:

方式1:通过共享标记位终止

定义一个volatile修饰的布尔变量作为标记,线程循环判断标记位,若为true则终止执行。

public class InterruptByFlag {
    // 共享标记位:volatile保证内存可见性
    private static volatile boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!isQuit) { // 循环判断标记位
                System.out.println("子线程:正在转账...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("子线程:收到中断信号,停止转账");
        }, "转账线程");

        t.start();

        // 主线程等待3秒后,设置标记位为true(中断子线程)
        Thread.sleep(3000);
        isQuit = true;
        System.out.println("主线程:发送中断信号");
    }
}

方式2:通过Thread的interrupt()方法终止

Thread类内置了中断标记位,可通过interrupt()方法设置标记,线程通过Thread.interrupted()Thread.currentThread().isInterrupted()判断标记。

不可以在lambda表达式中直接调用这个线程的isInterrupted()方法,因为lambda的定义在new Thread()之前

在这里插入图片描述
Thread提供了currentThread()方法来访问当前线程(类似于this),currentThread()是静态方法,可以通过类名直接调用,返回结果是当前线程。

public class demo11 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            //方法在lambda表达式中(也就是t线程的入口方法)调用,返回结果就是t的名称:Thread-0
            System.out.println(Thread.currentThread().getName());
        });
        t.start();
        //方法在main方法调用,返回结果就是main线程的名称:main
        System.out.println(Thread.currentThread().getName());
    }
}

核心方法

方法 说明
thread.interrupt() 给线程thread发送中断信号:若线程阻塞(如sleepwait),则抛出InterruptedException;若线程运行中,则设置中断标记位
Thread.interrupted() 判断当前线程的中断标记位,调用后清除标记位(返回true后,标记位变为false
Thread.currentThread().isInterrupted() 判断当前线程的中断标记位,调用后不清除标记位

示例:使用interrupt()中断线程

public class demo11 {
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    // 阻塞时收到中断信号,会抛出异常
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    //如果出现异常,用break来解决
                    break;
                }
            }
        });

        t.start();
        Thread.sleep(3000);
        System.out.println("main线程尝试终止t线程");
        t.interrupt();
        System.out.println("线程结束");
    }
}

注意

  • 若线程处于sleepwaitjoin等阻塞状态,interrupt()会导致线程抛出InterruptedException,并清除中断标记位;
  • 异常抛出后,线程不会自动终止,需在catch块中手动处理。
    • 这样让程序员有更多的操作空间:可以选择立即终止线程(break),或处理一部分逻辑再终止(写一些代码再break),也可以让线程继续执行(啥都不写);
    • 这里也可以看出Java中的线程终止不是强制性的,main让t终止,t不一定终止,选择权在t手里。
  • 相比共享标记位,interrupt()的优势是:即使线程处于阻塞状态,也能立即响应终止(无需等待阻塞结束)。

3.5 等待线程

多线程之间并发执行是随机调度的,而站在程序员的视角,我们不希望太多不可控情况的出现,join()方法可以控制多个线程之间结束的先后顺序

核心方法

方法 说明
thread.join() 当前的线程阻塞,直到thread线程执行完成后,才继续执行
thread.join(long millis) 调用线程阻塞最多millis毫秒,若thread仍未执行完,也会继续执行
thread.join(long millis,long nanos) 同上,时间精确到纳秒

在应用程序中很难进行纳秒级别的时间测量,并且线程本身的调度就有可能达到毫秒级别

public class demo12 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for(int i = 0;i<3;i++){
                try {
                    Thread.sleep(3000);
                    System.out.println("hello thread");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t线程结束");
        });
        t.start();

        //main线程等待t线程,t结束后main继续执行
        t.join();
        System.out.println("主线程结束");
    }
}

输出结果:
在这里插入图片描述

3.6 休眠线程

Thread.sleep(long millis)让当前线程休眠指定毫秒数,休眠期间线程会释放CPU资源,进入TIMED_WAITING状态。

  • 休眠时间是“最小保证时间”:实际休眠时间可能大于millis,因线程调度有延迟;
  • 休眠时若收到中断信号(interrupt()),会抛出InterruptedException并清除中断标记;
  • sleep()是静态方法,只能休眠当前线程,不能休眠其他线程。
  • 休眠时不可以被调度,休眠后处于可调度状态,但是不一定立即执行
  • sleep(0) :表示让出当前的线程,立即放弃CPU资源,等待操作系统重新调度

四、线程的状态:6种状态及流转逻辑

线程从创建到终止,会经历多种状态的变化。Java定义了6种线程状态,通过Thread.State枚举表示,理解这些状态是排查线程问题(死锁、阻塞)的关键。

4.1 6种线程状态及含义

通过代码打印所有状态:

public class ThreadStateEnum {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

输出结果:

NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
状态 含义 典型场景
NEW 线程已创建,但未启动,未调用start() Thread t = new Thread();后,t的状态为NEW
RUNNABLE 线程可运行,包含正在运行和等待CPU调度两种情况 调用start()后,线程进入RUNNABLEsleep()结束后也会回到此状态
BLOCKED 线程阻塞,等待获取锁(如synchronized锁) 线程A持有锁,线程B尝试获取同一把锁,线程B进入BLOCKED
WAITING 线程无限期等待,需其他线程唤醒 调用object.wait()(无超时)、thread.join()(无超时)后进入此状态
TIMED_WAITING 线程限时等待,超时后自动唤醒 调用Thread.sleep(1000)object.wait(1000)thread.join(1000)后进入此状态
TERMINATED 线程已终止(run()执行完或抛出未捕获异常) 线程执行完run()中的逻辑后,状态变为TERMINATED

4.2 线程状态流转图

在这里插入图片描述

  1. NEWRUNNABLE

    • 触发条件:调用start()方法;
  2. RUNNABLETERMINATED

    • 触发条件:run()执行完成或抛出未捕获异常;
  3. RUNNABLETIMED_WAITINGRUNNABLE

    • 触发条件:调用sleep(ms)wait(ms)等限时等待方法,超时后自动唤醒;
  4. RUNNABLEWAITINGRUNNABLE

    • 触发条件:调用wait()join()等无限期等待方法,需其他线程调用notify()唤醒;
  5. RUNNABLEBLOCKEDRUNNABLE

    • 触发条件:尝试获取synchronized锁,但锁被其他线程持有;
    • 类比:李四需要使用柜台(锁),但柜台被王五占用(王五持有锁),李四只能在柜台外等待(BLOCKED),王五用完后,李四进入柜台(获取锁,回到RUNNABLE)。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐