【JavaEE初阶】 多线程编程核心:解锁线程创建、方法与状态的创新实践密码


我的个人主页
我的专栏: 人工智能领域、java-数据结构、Javase、C语言,MySQL,JavaEE初阶,希望能帮助到大家!!! 点赞👍收藏❤


文章目录

一:初识线程(Thread)

1.1:线程的概念

一个线程就是一条 "执行流"。每个线程各自按顺序执行自己的代码,多个线程之间则 "并发" 执行多份代码。线程(Thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

举一个例子:厨房做菜

想象一个繁忙的厨房正在准备顾客丰盛的晚餐,这个厨房就是一个 "进程"。

进程:一个正在执行的程序实例。它拥有独立的资源,如内存空间、文件句柄等。就像这个厨房,它拥有自己的空间、厨具、食材等资源。
线程:进程内部的一个独立执行流。一个进程可以包含多个线程,它们共享进程的资源。就像厨房里的厨师,每位厨师都是一个独立的"线程"。

这里就可以分为:

单线程厨房,即一个厨师完成所有的做菜流程

多线程厨房:即多位厨师分工合作,每个厨师分工完成各自的任务。

1.2:为什么需要线程?

首先,"并发编程"已成为"刚需"。

  • 单核CPU的发展遭遇瓶颈,要提升算力就需依赖多核CPU,而并发编程能更充分地利用多核CPU资源。
  • 部分任务场景存在"等待IO"的情况,为了在等待IO的时间里处理其他工作,也需要用到并发编程。CPU可以切换到另一个线程去执行,避免了CPU时间的浪费。就像一位厨师在等待水烧开时,可以先去切菜,而不是空等。

其次,尽管多进程也能实现并发编程,但线程比进程更轻量:

  • 创建线程比创建进程更快;
  • 销毁线程比销毁进程更快;
  • 调度线程比调度进程更高效。

但是,即便线程已比进程轻量,人们仍不满足,进而发展出了"线程池"(ThreadPool)和"协程"(Coroutine)。

1.3:那么进程和线程的区别是什么?

  • 进程包含线程,每个进程至少存在一个线程,即主线程。

  • 进程之间不共享内存空间,而同一进程内的线程共享该进程的内存空间。

  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

  • 一个进程崩溃通常不会影响其他进程,但一个线程崩溃可能导致整个进程(及其中所有线程)一同崩溃。

1.4:Java 的线程和操作系统线程的关系

线程是操作系统层面的概念,其机制由操作系统内核实现,并向用户层提供API供调用(例如Linux的pthread库)。

Java标准库中的Thread类,可视为对操作系统提供的线程API进行了进一步的抽象与封装。

二:多线程程序的编写

2.1:继承 Thread 类

java 复制代码
package ThreadDemo;
class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new MyThread();
        thread.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

可以看到hello threadhello main 都打印出来了,但是不是交替出现的。这里就体现了操作系统调度线程是随机的.

每个线程都是独立的工作流,多个线程之间是并发执行的

当我们这里将主线程换成thread.run() ,因为上面的是死循环只看到了打印hello thread

在这里我们可以利用Java中的jconsole 命令观察线程

右键点击以管理员身份运行,选择我们正在运行的线程,点击连接即可

其中main 就是我们的主线程,Thread-0就是我们通过MyThread类创建的线程,其他的线程都是我们jvm自带的,来负责对线程进行一些背后的操作。

点击main可以看到详细信息

这里是使用thread.start()情况下当我们使用thread.run()就没有Thread-0线程了。

2.2:实现 Runnable 接口

java 复制代码
package ThreadDemo;
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);
            }
        }
    }
}
public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
        while(true){
            System.out.println("hello main");
            thread.sleep(1000);
        }
    }
}

注意:

runnable 没有 start 方法.

要想启动线程, 需要搭配 Thread. runnable.start();

2.3:匿名内部类创建 Thread 子类对象

java 复制代码
package ThreadDemo;
public class demo3 {
    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);
        }

    }
}

2.4:匿名内部类创建 Runnable 子类对象

java 复制代码
package ThreadDemo;
public class demo4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=new Runnable(){

            public void run(){
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t=new Thread(runnable);
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

2.5:lambda 表达式创建 Runnable 子类对象

java 复制代码
package ThreadDemo;
public class demo4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable=new Runnable(){

            public void run(){
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t=new Thread(runnable);
        t.start();
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }

    }
}

三:在多线程下-增加了运行速度

使用 System.nanoTime() 可记录系统的纳秒级时间戳:

  • 串行(serial):单线程依次完成一系列运算
  • 并行(concurrency):通过两个线程同时执行相同运算

以一个代码为例:

java 复制代码
package ThreadDemo;
public class demo6 {
        private static final long count = 10_0000_0000;
        public static void main(String[] args) throws InterruptedException {
            // 使⽤并发⽅式
            concurrency();
            // 使⽤串⾏⽅式
            serial();
        }
        private static void concurrency() throws InterruptedException {
            long begin = System.nanoTime();

            // 利⽤⼀个线程计算 a 的值
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int a = 0;
                    for (long i = 0; i < count; i++) {
                        a--;
                    }
                }
            });
            thread.start();
            // 主线程内计算 b 的值
            int b = 0;
            for (long i = 0; i < count; i++) {
                b--;
            }
            // 等待 thread 线程运⾏结束
            thread.join();

            // 统计耗时
            long end = System.nanoTime();
            double ms = (end - begin) * 1.0 / 1000 / 1000;
            System.out.printf("并发: %f 毫秒%n", ms);
        }
        private static void serial() {
            // 全部在主线程内计算 a、b 的值
            long begin = System.nanoTime();
            int a = 0;
            for (long i = 0; i < count; i++) {
                a--;
            }
            int b = 0;
            for (long i = 0; i < count; i++) {
                b--;
            }
            long end = System.nanoTime();
            double ms = (end - begin) * 1.0 / 1000 / 1000;
            System.out.printf("串⾏: %f 毫秒%n", ms);
        }
    }

需要注意的是 :多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的。

四:Thread类及常见的方法

在 Java 中,Thread 类是 JVM 用来管理线程的核心类。每个线程在 JVM 中都对应一个唯一的 Thread 对象,通过这个对象可以对线程的状态、行为进行控制和管理。

4.1:Thread常见的构造方法
构造方法声明 参数说明 用途
Thread() 无参数 创建一个线程对象,需通过重写 run() 方法定义线程任务(默认线程名由 JVM 生成,如 Thread-0)。
Thread(Runnable target) target:实现 Runnable 接口的对象(封装线程任务) 创建线程并关联任务,推荐使用(解耦线程与任务逻辑,避免单继承限制)。
Thread(String name) name:线程名称(字符串) 创建线程并指定名称,便于调试(如日志输出、线程监控时区分不同线程)。
Thread(Runnable target, String name) target:任务对象;name:线程名称 同时指定任务和线程名称,兼顾任务解耦与调试便利性。
Thread(ThreadGroup group, Runnable target) group:线程组;target:任务对象 将线程加入指定线程组(线程组用于批量管理线程,如统一设置优先级、中断等)。

说明:

  • 最常用的构造方法是 Thread(Runnable target)Thread(Runnable target, String name),通过传入 Runnable 对象分离任务逻辑,更符合面向对象设计。
  • 线程名称可通过 setName() 后续修改,但建议在构造时指定,便于早期调试。
  • 线程组(ThreadGroup)主要用于线程的批量管理,日常开发中使用较少,除非需要对一组线程进行统一操作(如销毁所有子线程)。

那么如何为一个线程修改一下名字呢?以下面代码为例细述

java 复制代码
public class demo6{
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while(true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"我的线程1");
          t.start();
    }
}

通过jconsole就可以看到我刚刚修改的线程名字

注意:为什么这个线程中没有看到main,是因为main线程已经执行完了。

  • 在这个代码中start执行完毕,main方法就运行完了
  • main方法结束,对应的main线程就结束了
  • 也就是对于一个线程来说,线程对应的"入口方法"执行完毕,对应的线程就会自动销毁。
4.2:Thread几个常见的属性
属性 获取方法 说明
ID getId() 线程的唯一标识,不同线程不会重复
名称 getName() 用于调试工具区分线程,可自定义
状态 getState() 表示线程当前所处的情况(如 NEWRUNNABLETERMINATED 等)
优先级 getPriority() 优先级高的线程理论上更容易被调度(范围 1~10,默认 5)
是否后台线程 isDaemon() JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活 isAlive() 简单理解为 run 方法是否运行结束
是否被中断 isInterrupted() 标识线程是否被中断(需结合 interrupt() 方法协作处理)

这里讲一下是否为后台进程,引入思考那么前台线程又是什么?

前台线程:线程没有运行完,进程就不会结束(即线程可以阻止进程结束)
后台线程:线程没有运行完,进程可以结束(即线程不能阻止进程结束)

应用场景:当一个线程做的任务很重要,必须要完成这个任务,这样就应该设置为前台线程。

如果一个线程做的任务无关紧要/周期性无期限执行,就可以设置为后台线程。

我们可以通过.setDaemon()设置线程为前台还是后台,但是这个操作必须在start之前。


以下面代码为例,详细描述一下现成的存活isAlive()

java 复制代码
package ThreadDemo;
public class demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        System.out.println("是否存活:"+t.isAlive());
        Thread.sleep(4000);
        System.out.println("是否存活:"+t.isAlive());
    }
}

可以看到判断线程是否存活,看线程的状态即可。

  • 线程正在执行,isAlive()就是true的。
  • 线程执行完毕,isAlive()就是false的。
4.3:启动一个线程 - start()

以这个代码为例

java 复制代码
package ThreadDemo;
public class demo8 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("hello thread");
        });
        t.start();
        System.out.println("hello main");
    }
}

当我们执行几次结果后发现,都是先打印hello main 在打印hello thread那么是不是都是一直这个顺序呢?

当start后,main线程和t线程两个执行流,是并发执行 的关系。由于操作系统对于线程的调度是随机的,所有也有可能出现先打印hello thread的情况。

重点:一个线程对象 只能start一次

当一个线程对象出现一次以上start就会出现如下图所示报错

4.4:中断一个线程

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记

此处针对lambda表达式定义,实际上实在new Thread()之前的,就会出现t没有进行初始化 的报错信息。

我们可以使用Thread提供的一个静态方法currentThread

java 复制代码
package ThreadDemo;
public class demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            //由于currentThread方法是在start之后在执行的
            //并且是在t线程中执行的,返回的结果就是指向t线程对象的引用
            while(!Thread.currentThread().isInterrupted()){
            System.out.println("hello thread");
            }
        });
        t.start();
        Thread.sleep(2000);
    //在main线程中尝试中止t线程
        t.interrupt();
    }
}

这样就可以使我们的t线程结束

但是我们在循环中加入sleep

就会触发异常

在这里如果t线程正在休眠,此时的main调用Interrupt方法,就会把sleep提前给唤醒。sleep被提前唤醒,就触发异常,就把isInterrupted标志位给重置为了false。这样我们的线程就不会结束

那么在这种情况下该怎么结束线程呢?

①:可以在e.printStackTrace();后面加上一个break,让线程立即结束

java 复制代码
package ThreadDemo;
public class demo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            //由于currentThread方法是在start之后在执行的
            //并且是在t线程中执行的,返回的结果就是指向t线程对象的引用
            while(!Thread.currentThread().isInterrupted()){
            System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        });
        t.start();
        Thread.sleep(2000);
    //在main线程中尝试中止t线程
        t.interrupt();
    }
}

当然也可以把这个e.printStackTrace();注释掉,这样异常就不会打印了

②:也可以通过ideal自动生成的catch: throw new RuntimeException(e),也可以在break之前在添加其他逻辑

这几种方式,本质上都是t线程自己决定自己是否要终止,相当于main只是对t提供了一个"建议",而不是:"强制执行的".

使用Interrupt方法的时候:

  • 1:t 线程没有进行sleep等阻塞操作,t 的isInterrupted()方法返回true,通过循环结束t 线程。
  • 2:t 线程进行sleep等阻塞操作 ,还是会返回true,但是sleep如果被提前唤醒InterruptException,同时也会把isInterrupted()返回的结果设置为false,此时就需要手动去决定是否要结束线程。
4.5:线程等待-join()

mian线程等待t 线程结束

java 复制代码
package ThreadDemo;
public class demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            for(int i=0;i<3;i++){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        System.out.println("主线程等待之前");
        t.join();
        System.out.println("主线程等待之后");
    }
}

t 线程等待 main 线程结束

java 复制代码
package ThreadDemo;
public class demo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread myThread=Thread.currentThread();
        Thread t=new Thread(()->{
            try {
                System.out.println("t线程等待之前");
                myThread.join();
                System.out.println("t线程等待之后");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t.start();
        for(int i=0;i<10;i++){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

注意这里需要获取main线程的引用:Thread myThread=Thread.currentThread();

  • 哪个线程调用的join,该线程就是等的一方
  • join前面是那个引用,该线程就是被等的一方

join默认的是死等,但是join还有重载版本,可以指定最大的等待时间

方法声明 等待时间参数说明
void join(long millis) millis:等待时间(单位:毫秒),0 表示无限等待(与无参 join() 效果一致)
void join(long millis, int nanos) millis:毫秒数(主单位),nanos:纳秒数(范围 0-999999,辅助精度),两者总和为总等待时间

以上面代码为例,加入3000毫秒的最大等待时间

五:线程的状态
5.1:线程的状态是一个枚举类型 Thread.State

在 Java 中,线程(Thread)的生命周期包含以下 6 种状态,定义在 Thread.State 枚举类中,如下图所示:

状态名称 说明
NEW(新建) 线程对象已创建,但尚未调用 start() 方法,未开始执行。
RUNNABLE(可运行) 线程已调用 start() 方法,正在 JVM 中执行,或等待 CPU 资源(就绪状态)。
BLOCKED(阻塞) 线程因竞争同步锁(synchronized)被阻塞,等待获取锁。
WAITING(等待) 线程无限期等待另一个线程的特定操作(如 wait()join() 无参版),需被显式唤醒。
TIMED_WAITING(计时等待) 线程在指定时间内等待(如 sleep(time)wait(time)join(time)),超时后自动唤醒。
TERMINATED(终止) 线程执行完毕(run() 方法结束)或因异常终止。

可通过 Thread.getState() 方法获取线程当前状态。

java 复制代码
package ThreadDemo;
public class demo12 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{

        });
        System.out.println(t.getState());
        t.start();
        t.join();
        System.out.println(t.getState());
    }
}

可以看到还没有开始start时候就是NEW状态,线程执行完了就是TERMINATED
RUNNABLE,当在t.start(); 后加入
System.out.println(t.getState()+" "+t.isAlive());

此时就可以看到RUNNABLE状态,代码中不触发阻塞类操作,都是RUNNABLE状态

其他三个都是阻塞状态,以下面代码为例

java 复制代码
package ThreadDemo;
public class demo12 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println(t.getState()+" "+t.isAlive());
        t.start();
        System.out.println(t.getState()+" "+t.isAlive());
        t.join();
        System.out.println(t.getState()+" "+t.isAlive());
    }
}

再利用jconsole就可以看到我们main线程的状态

以及Thread-0的状态,此时t线程使在join操作所以是 TIMED_WAITING状态。


状态转换关键点:

  • NEWRUNNABLE:调用 start() 方法后进入可运行状态。
  • RUNNABLEBLOCKED:竞争同步锁失败时进入阻塞状态,获取锁后回到 RUNNABLE
  • RUNNABLEWAITING/TIMED_WAITING:调用 wait()join() 等方法时进入等待状态,被唤醒或超时后回到 RUNNABLE
  • 所有状态 → TERMINATED:线程执行完成或异常终止后进入终止状态,无法再切换到其他状态。

相关推荐
百锦再16 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
会跑的兔子17 小时前
Android 16 Kotlin协程 第二部分
android·windows·kotlin
键来大师17 小时前
Android15 RK3588 修改默认不锁屏不休眠
android·java·framework·rk3588
合作小小程序员小小店18 小时前
web网页开发,在线%考试管理%系统,基于Idea,vscode,html,css,vue,java,maven,springboot,mysql
java·前端·系统架构·vue·intellij-idea·springboot
多多*18 小时前
maven常用的命令
java·log4j·maven
xie_pin_an18 小时前
MyBatis-Plus 实战:MPJLambdaWrapper 多表联查用法全解析
java·spring boot·spring·mybatis
ᐇ95919 小时前
Java LinkedList集合全面解析:双向链表的艺术与实战
java·开发语言·链表
luyun02020219 小时前
Windows 11操作更丝滑,绝了
java·运维·figma
码银19 小时前
【数据结构】顺序表
java·开发语言·数据结构
Boop_wu19 小时前
[Java EE] 计算机基础
java·服务器·前端