多线程篇-(二)线程创建、中断与终止

目录

线程(Thread)的创建方法

[一.继承 Thread,重写run方法:](#一.继承 Thread,重写run方法:)

[二.实现 Runnable, 重写 run:](#二.实现 Runnable, 重写 run:)

[三.Thread,重写 run, 使用匿名内部类:](#三.Thread,重写 run, 使用匿名内部类:)

详细解释一下什么是"一次性":

[四.实现 Runnable, 重写 run, 使用匿名内部类:](#四.实现 Runnable, 重写 run, 使用匿名内部类:)

[五.使用 lambda 表达式:](#五.使用 lambda 表达式:)

[下面继续看 Thread 其他属性和方法。](#下面继续看 Thread 其他属性和方法。)

start与run区别

学完运行接着要中断了。

线程终止


众所周知,茴香豆的茴字有四种写法,线程和茴香豆差不多,区别是这两根本没有关系,hhhhh~

那为啥要提茴香豆呢?因为茴字有四种写法,多线程也有四种(其实有五种,只是想用茴字写法引出主题而已)。

线程(Thread)的创建方法

按照之前学过的知识,线程应该是这样创建的

java 复制代码
public class Demo1 {
    public static void main(String[] args) {
        Thread t=new Thread();
        t.start();
    }
}

可真的是这样吗?很显然不是,因为 run 的一瞬间程序就结束了。

什么原因造成的?需要我们进到 Thread 源代码找。

在这个方法中,target 是线程中的一个私有成员属性。如果创建的线程里传入了值,它会自动执行,反之,程序立即结束。

也就是用来告诉线程是否有活。

那如何正确打开线程呢?

一.继承 Thread,重写run方法:

java 复制代码
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("Hello World!!");
    }
}
public class Demo1 {
    public static void main(String[] args) {
        Thread t=new MyThread();
        //执行完这步才是成功创建一个线程
        t.start();
        System.out.println("Hello World!!");
    }
}

这里有的小伙伴可能疑惑,为什么Thread 不用导入包?

因为**java.lang****,它是核心语言包**!

这个包 包含了 Java 最基础、最核心的类。

例如:

  • Object (所有类的父类)

  • String

  • System

  • ExceptionError (异常体系)

  • 基本数据类型的包装类,如 Integer, Double, Boolean.....

  • 还包括 ThreadRunnable

根据 Java 语言规范,所有 Java 编译器都会自动、隐式地为每个源文件导入整个 java.lang 包, 这就好像编译器在每个 .java 文件的开头都自动为你加上了这样一行

java 复制代码
import java.lang.*;

因此你可以直接使用 java.lang 包中的所有类,而无需何 import 语句。

二.实现 Runnable, 重写 run:

java 复制代码
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello World!!");
    }
}
public class Demo2 {
    public static void main(String[] args) {
        Runnable r =new Thread();
        r.start();
        System.out.println("Hello World!!");
    }
}

是不是有小伙伴和我想的一样,直接用 Runnable 开始线程,很可惜,这个想法是错误的。

Runnable 只是一个任务,一段在内CPU执行的逻辑代码,真正创建线程还是要通过 Thread。

java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello World!!");
    }
}
public class Demo2 {
    public static void main(String[] args) {
        Runnable runnable =new MyRunnable();
        Thread t=new Thread(runnable);
        t.start();
        System.out.println("Hello World!!");
    }
}

不过线程里要干啥,可以通过 Runnable 表示,就不用直接重写 Thread 的 run 方法去表示。

这也是好的一点,为什么这么说?

我们希望敲的代码是低耦合的,就是要执行的 任务本身 和 线程 这个概念的关联度尽可能低,后续如果变更代码(通过其他方式),修改会简单许多。不然一改红一片,不仅要拼加时,还容易白费一年的辛苦.......

线程要执行定义的任务,选择放到 Thread 里,或者放到 (Runnable) 中都行。

三.Thread,重写 run, 使用匿名内部类:

java 复制代码
public class Demo3 {
    public static void main(String[] args) {
        Thread t=new Thread(){
            @Override
            public void run() {
                System.out.println("Hello World!!");
            }
        };
        t.start();
        System.out.println("Hello World!!");
    }
}

这个语句做了三件事:

1.创建一个 Thread 的子类,子类叫什么?不知道,匿名的,new Thread 用来告诉编译器要创建一个Thread对象。

2.{ } 里面可以编写子类的定义代码,子类要有哪些属性 哪些方法 重写父类的哪些方法.....

3.创建这个匿名内部类的实例,把实例的引用赋值给 t 。

这种写法可以少定义一些类,如果某个代码是"一次性",就可以使用匿名内部类的写法。

详细解释一下什么是"一次性":

1.代码逻辑的单一使用场景,你定义的这个类(或这段代码逻辑)在整个程序中,只会在这一个特定的地方被使用一次,以后复用的可能性极低。

上述代码来说, 重写了 run() 方法、不断打印 "hello thread" 的线程,它的逻辑非常明确且唯一。几乎不可能在程序的其他 地方需要一个行为一模一样 的线程。为它单独创建个 .java 文件,显然不合适。

反面例子 :如果你有一个类,负责验证用户信息的各种规则,这个类很可能在"用户注册"、"用户登录"、"修改信息"等多个地方被调用。这就不是一次性的,应该定义成一个独立的、可复用的类。

2.实现的简洁性与临时性,这个类的实现非常简短 ,通常只是为了实现一个简单的接口(如 Runnable, Comparator),或者重写一个方法。如果逻辑变得复杂,再使用匿名内部类就会导致代码可读性急剧下降,这时就应该把它提取成一个命名的独立类。

所以,使用匿名内部类时先考虑几个问题:

1.这段逻辑我以后还会在其他地方原样再用吗? 否

2.这段逻辑是不是很简单(比如主要就是重写一个方法)? 是

3.这段逻辑是不是和当前方法的其他变量/状态紧密相关? 是

如果你也是这么想的,那可以使用匿名内部类,这避免了代码库被大量仅使用一次的"碎片类"污染,让代码更集中、更易读。

四.实现 Runnable, 重写 run, 使用匿名内部类:

java 复制代码
public class Demo4 {
    public static void main(String[] args) {
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World!!");
            }
        };
        Thread t=new Thread(runnable);
        t.start();
        System.out.println("Hello World!!");
    }
}

和前面第二个一样,目的是为了降低耦合,分离任务 和 线程 的概念。

五.使用 lambda 表达式:

这东西本质是一个"匿名函数"。

什么意思?

它的侧重点在于用来完成什么行为逻辑,而不是在声明一个"新的类型",但是java中严格要求方法必须依托于 类 存在,为了解决这个问题,大佬们想出了一个叫"函数式接口"的东西。下面的代码便用了这个接口

java 复制代码
public class Demo5 {
    public static void main(String[] args) {
        Thread t =new Thread(()->{
            while(true){
                System.out.println("Hello World!!");
            }
        });
        t.start();
    }
}

针对三 四改进,引入lambda表达式。

( ) -> { } 这个语法和 run( ) { } 的作用完全一样,因为写法更简便所以更常使用。

下面继续看 Thread 其他属性和方法。

java 复制代码
public class Demo3 {
    public static void main(String[] args) {
        Thread t=new Thread("sayHello"){
            @Override
            public void run() {
                while(true){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("Hello World!!");
                }
            }
        };
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("Hello World!!");
                }
            }
        };
        Thread t2=new Thread(runnable,"sayHello1");
        Thread t3 =new Thread(()->{
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("Hello World!!");
            }
        },"sayHello2");
        t.start();
        t2.start();
        t3.start();
    }
}

如何查看线程是否真的是这些命名?需要借助jconsole,不知道在哪的按照下方提示找。

先让程序跑起来,再看。

看到结果有什么发现?居然不是按顺序执行的,这说明什么?原因放到后面解释。

现在有一个新问题,main线程去哪了?

写个代码找找。

java 复制代码
        t.start();
        t2.start();
        t3.start();
        System.out.println("main线程开始");
        System.out.println("main线程结束");

把代码放到开始三个线程后面,运行看看。

从结果看出,其他线程执行前 main 线程已经结束了。

上面我们给了1s的睡眠时间,改成0会怎样?

答:CPU会狂转,接着能听到风扇呼呼声。结果是看不到 main开始和结束输出的,瞬间就刷没了。

那为什么主线程结束了程序还在运行?

这就是多线程和单线程的区别,以前认为main方法执行完,程序进程就结束了,但是只针对单线程程序。

要让多线程结束,需要 Thread 类里的一个方法。

必须在开始前设定,否则会报错

java 复制代码
        t.setDaemon(true);
        t2.setDaemon(true);
        t3.setDaemon(true);
        t.start();
        t2.start();
        t3.start();

那是不是还有前台线程?

哎,还真有。这两区别在于,一个能让进程结束,一个只能结束线程本身(不影响进程的其他线程运行)。

举个栗子:

张三李四王五几个人去参加一个活动,活动从上午11点持续到下午6点。三人玩了几个小时感觉非常累,于是和主持人打声招呼就回去了。

其中这三人的行为影响到活动进行了吗?显然没有,那就称三人为 "后台进程" ,只结束线程本身,无法决定进程是否结束。主持人便是 "前台线程" ,在台上宣布一声,活动结束,进程结束。


那如何分辨线程是前台还是后台?java 会在程序运行时给各个线程分配一个表示身份的 id ,类似于CPU给进程的 PID。通过方法获取对应线程的情况。

java 复制代码
        //获取线程id
        System.out.println(t.getId());
        System.out.println(t2.getId());
        System.out.println(t3.getId());
        //获取是否后台线程
        System.out.println(t.isDaemon());
        System.out.println(t2.isDaemon());
        System.out.println(t3.isDaemon());

三个线程都是前台,因此main线程结束也无法阻断程序继续执行。

用alive方法获取线程是否存活。

java 复制代码
        System.out.println(t.isAlive());
        System.out.println(t2.isAlive());
        System.out.println(t3.isAlive());

每个Thread对象都只能start一次,多了就会报错,好比一个针头只能一个人用一次。

每次想创建一个新线程都得创建一个新的 Thread 对象(不能重复利用)

直接报一个线程非法状态异常。

至于为什么报错先出来了,是因为CPU随机调度的原因,这也解释上面用 jconsole 观察时并没有"按顺序"输出的原因。

(编译N次终于先调用成功的......)

start与run区别

答:这两都没啥关系,也就谈不上"区别"。start 调用系统接口,run 是线程入口方法。

学完运行接着要中断了。

在类里面定义一个布尔值的成员变量,把whlie循环判断条件改为布尔变量即可。

利用"内部类访问外部类的成员"语法完成中断命令。

java 复制代码
    private static boolean isFinished = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while (!isFinished){
                System.out.println("Hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        isFinished=true;
    }

那么问题来了,获取布尔值的变量能否定义成局部变量?

简单来说,lambda 希望使用外面的变量触发 "变量捕获" 的语法。

上面说过,lambda 是 回调函数,在操作系统真正创建出线程之后才会执行,而 main函数可能在线程创建前就结束了,导致 lambda 在获取变量之前,main线程结束,isFinished 被销毁 ,变量无法被捕获。

所以,java解决方法是把被捕获的变量拷贝一份给lambda,外边的变量是否被销毁,也就影响不到lambda 的执行了。

线程终止

java 的 Thread 对象中提供了现成变量,直接进行判定。

直接用线程调用会怎样?

报错.....

原因看图示

所以,需要使用另一个更安全的方法终止线程。

静态方法,相当于 this 关键字,哪个线程调用,获取对应线程的 Thread 引用。

java 复制代码
Thread.currentThread().isInterrupted()

判断线程是否是被终止了。

java 复制代码
t.interrupt();

则是主动进行终止操作。

java 复制代码
public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t =new Thread(()->{
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("Hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            };
        });
        t.start();
        Thread.sleep(1000);
        t.interrupt();
    }
}

---------------------------------------------------------------完---------------------------------------------------------------

相关推荐
ps酷教程8 小时前
Jackson 解决没有无参构造函数的反序列化问题
java
NiceCloud喜云8 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
AI玫瑰助手9 小时前
Python函数:默认参数的定义与注意事项
开发语言·python·信息可视化
油炸自行车9 小时前
Claude Code 错误:API Error: 400 Failed to deserialize the JSON body into the
开发语言·javascript·json·trae·claude code·api error 400
肩上风骋9 小时前
C++14特性
开发语言·c++·c++14特性
_日拱一卒9 小时前
LeetCode:994腐烂的橘子
java·数据结构·算法·leetcode·深度优先
隔窗听雨眠9 小时前
Nginx网关响应慢排查手记
java·服务器·nginx
智慧物业老杨10 小时前
智慧物业合同周期管理系统:从风险预警到智能交接的全流程数智化落地方案
java·人工智能·python
源码宝10 小时前
MES系统源码:Java8 + SpringBoot2.7 + MySQL8 + Redis,后端源码清爽易扩展
java·后端·源码·springboot·mes系统·源码二开·mes源码
JAVA社区10 小时前
Java高级全套教程(十)—— SpringCloudAlibaba超详细实战详解
java·开发语言·spring cloud·面试·职场和发展