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

目录

线程(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();
    }
}

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

相关推荐
jnrjian1 小时前
Library Cache Load Lock library cache pins are replaced by mutexes
java·后端·spring
hoiii1872 小时前
基于MATLAB实现内点法解决凸优化问题
开发语言·matlab
abcnull2 小时前
传统的JavaWeb项目Demo快速学习!
java·servlet·elementui·vue·javaweb
risc1234562 小时前
【lucene】PostingsEnum跟TermsEnum 的区别是啥?
java·lucene
小江的记录本2 小时前
【Kafka核心】Kafka高性能的四大核心支柱:零拷贝、批量发送、页缓存、压缩
java·数据库·分布式·后端·缓存·kafka·rabbitmq
SamDeepThinking2 小时前
程序员过35岁之前,应该完成的三件事
java·后端·程序员
大数据三康2 小时前
Java字符统计:从输入到输出的完整解析
java·学习·循环结构
特种加菲猫2 小时前
多态:让代码拥有“千变万化”的能力
开发语言·c++