Java 多线程(一)—— 线程的创建与属性 Java 线程的创建与属性

线程的创建

方式一:继承 Thread 类,重写 run 方法

class MyThread extends Thread {
 @Override
 public void run() {
 System.out.println("hello Thread");
 }
}

方式二:实现 Runnnable 接口

class MyRunnable implements Runnable {
 @Override
 public void run() {
 System.out.println("hello runnable");
 }
}

方式三:使用匿名内部类

Thread t1 = new Thread() {
 @Override
 public void run() {
 System.out.println("hello Thread");;
 }
 };
 
 Runnable r = new Runnable() {
 @Override
 public void run() {
 System.out.println("hello runnable");
 }
 };
 
 Thread t2 = new Thread(r);

匿名内部类在JVM的创建流程:首先根据你实现的方法创建一个类,这个类是没有名字的,也就是匿名,接着实例化对象,将对象的引用赋值给左边的变量。

方式四:使用 Lambda 表达式创建 Thread 对象

Thread t3 = new Thread(() -> {
 System.out.println("hello Thread t3"); 
 });
Runnable run = () -> {
 System.out.println("hello");
 };

lambda 表达式用在函数式接口上,Runnable 本身就是函数式接口,所以可以使用lambda 表达式,Thread 直接传入 lambda 表达式实际就是传入 Runnable 接口的实现,编译器会进行自动校验和匹配,大家不用担心。

上述方式我们只是创建了 Thread 对象,并没有真正创建线程,我们需要使用.start() 方法才能真正创建线程出来,我们来看一下线程的并发执行:

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);
 }
 }
 });
 t.start();
 while(true) {
 System.out.println("hello Main");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 }
 }

你会发现这里会循环打印“hello Thread” 和 “hello Main”,但是这两个顺序是不确定的,也就说明线程的调度是随机的,抢占式执行的


如果我们没有使用 .start() 而是直接使用run() 方法,那结果会如何?

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);
 }
 }
 });
 t.run();
 while(true) {
 System.out.println("hello Main");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 }
 }

你会发现会一直打印“hello Thread”,为什么?
因为此时直接使用.run() 方法是不会在进程中创建线程的,而是像普通的类调用自己的方法一样,由于是在主线程中直接调用 run 方法,所以上面的打印是在主线程中进行的,所以后面的 hello main 无法打印,因为前面已经发生死循环了。

这里简单说明一下,在Java 中,使用 main 方法就会自动开启主线程(你也可以叫做 Main 线程),所以在没有学习多线程之前,我们编写的代码都是单线程的。

run() 方法 是线程的入口方法,在线程创建好之后,就会自动调用这个方法,不需要我们手动去调用
run() 方法相当于回调函数。

start() 方法一个线程对象只能使用一次

查看线程信息的工具:jconsole

首先打开自己的 jdk 目录,点击 bin 然后找到 jconsole

一般在C:\Program Files\jdk-17\bin 下

然后我们在 IDEA 创建线程:

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);
 }
 }
 });
 t.start();
 while(true) {
 System.out.println("hello Main");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 }
 }

进行本地连接:

选择不安全连接:

点击线程:

之后我们就可以看到下面的线程调用栈了:

在这里我们可以看到主线程 main 还有 Thread-0 线程,还有其他一些线程是 JVM 自带的线程。

Thread 类的构造方法

Thread()

无参构造方法,在上面已经使用过。

Thread(Runnable target)

传入 Runnable 对象来创建线程,上面也使用过

Thread(String name)

给线程命名,这是为了我们后续如果要查看线程的信息的时候,一个有意义的名字可以让我们更好地在 Jconsole 找到。

Thread(Runnable target, String name)

和 Thread(String name) 异曲同工

Thread(ThreadGroup group, Runnable target)

线程可以被用来分组管理,分好的组为线程组。

线程的属性

前台线程与后台线程

前台线程控制着整个进程的生命周期,当所有的前台线程都结束了,整个Java进程才会结束。

后台线程又被称为守护线程,后台线程主要由JVM产生的,例如垃圾回收。

我们自己创建的线程都是属于前台线程,当然我们也可以通过setDaemon() 方法来设置,daemon 有守护的意思,所以我们可以传入 true 来设置后台进程,但是这个方法必须放在start()之前

Thread t = new Thread(() -> {
 while(true) {
 System.out.println("hello Thread");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 }
 });
 t.setDaemon(true);
 t.start();

线程的中断

在上面的表格中我们看到一个方法isInterruprted() ,这个方法是用来判断某个线程是否中断了,这里的中断实际上就是终止的意思,也就是不会再恢复了,即线程终止。

模拟实现线程终止

我们可以通过一个标志位来实现线程的终止。

public class Demo1 {
 private static boolean flag = true;
 public static void main(String[] args) throws InterruptedException {
 Thread t = new Thread(() -> {
 while(flag) {
 System.out.println("hello Thread");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 }
 });
 t.start();
 flag = false;
 while(true) {
 System.out.println("hello Main");
 Thread.sleep(1000);
 }
 }
}

通过外部类的成员来控制线程,会出发线程的内部类访问外部类成员的语法


如果我们使用的是局部变量:会怎么样?

你会发现这是不允许的,为什么?

lambda 表达式中的是回调函数,线程创建之后执行这个回调函数是很久之后的事情了,就有可能 main 线程结束了,随之 flag 这个变量也别销毁了,那么线程 t 在后面就无法获取到 flag 这个数值了。

为了解决这个问题,Java 使用变量捕获的方法,就是把被捕获的变量拷贝一份到 lambda 里面,这样外面的线程是否销毁就不会影响 lambda 里面的执行了。因为是拷贝,所以意味着这个变量是不适合进行修改的,你在另一个线程进行修改是不会影响到另一个线程已经拷贝好的数据的。

所以Java 这边就不允许你进行修改,所以这种变量天生自带 final 属性或者事实 final 属性,什么叫做事实 final,就是这个变量没有被 final 修饰,但是这个变量没有进行过修改,着就是事实 final 属性。

public static void main(String[] args) throws InterruptedException {
 boolean flag = true;
 Thread t = new Thread(() -> {
 while(flag) {
 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);
 }
 }

像上面这种使用及局部变量的,会触发线程的“捕获变量”的语法

interrupt() 与 isInterrupted()

我们可以自己手动终止线程。

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);
 }
 }
 });
 t.start();
 t.interrupt();
 }

当我们运行完之后:

为什么会抛出异常?

因为我们在线程中写了一个sleep 方法,当你终止线程的时候, sleep 可能还没执行到 1s,就被interrupt 终止了,就会抛出 InterruptedException e 异常。

如何解决这个异常?我们可以选择不抛异常出来,当外面捕获到异常的时候,可以选择 break 这个循环。

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) {
 break;
 }
 }
 });
 t.start();
 t.interrupt();
 }

我们明明在t.start() 后面使用了t.interrupt() 终止了线程,可是为什么还会打印出一个“hello Thread" ?
t.interrupt() 是在方法 main 线程里的,由于线程的调度是随机的,所以 main 线程和 t 线程都是随机调度的,上面的运行结果就是属于 t 线程先执行了,然后才执行到 main 线程里的 终止代码。


当然你也可以选择不 break 掉,你可以选择什么都不做:

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) {
 }
 }
 });
 t.start();
 Thread.sleep(2000);
 t.interrupt();
 }

你会发现居然会一直打印”hello Thread",为什么?
原因就在 sleep 中,当线程中有 sleep 方法的时候,如果你进行终止线程,那么 sleep 大概率会因为没有到达 1s 的设定休眠时间而被唤醒,这时候 sleep 会再次把 线程的终止状态修改为执行状态,也就是把 线程的isInterrupted() 属性由 true 修改回 false

Java这样实现,有什么好处?
首先线程可以有三种选择可以执行,第一种就是上面的继续执行,无视 终止指令。
第二种就是在捕获到 异常后,可以在处理代码部分写上一些代码,获取阶段性的结果,也就是如果线程终止了我们可以得到线程终止之后的预期结果,而不是零零散散的随机的结果。
第三种就是直接终止线程,这种我们在 catch 代码中抛出异常或者如果是循环的话直接写上 break 即可。


补充说明:Thread.currentThread().isInterrupted() 为什么要这样写?
因为 lambda 的定义是在 Thread 实例化之前的,毕竟我们知道要先重写完 run 方法才能进行对象的实例化,因此 lambda 表达式中是不知道 t 的存在的,所以我们要通过Thread.currentThread() 方法来获取当前的线程对象
currentThread() 方法是静态方法,所以直接使用类名.方法来调用即可。

线程存活

线程的销毁不意味着线程对象的销毁。

我们在终止完线程之后,还是可以通过线程对象来获取线程的属性的:

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) {
 break;
 }
 }
 },"test");
 t.start();
 Thread.sleep(2000);
 t.interrupt();
 System.out.println(t.getName());
 }

原因:线程的销毁工作是由操作系统完成的,而对象的销毁是由 JVM 的 GC(垃圾回收机制)实现的,在Java中线程的销毁并不意味着线程对象也被销毁了。


线程是否存活,简单的理解,就是run方法是否运性结束了

public static void main(String[] args) throws InterruptedException {
 Thread t = new Thread(() -> {
 for (int i = 0; i < 3; i++) {
 System.out.println("hello Thread");
 }
 });
 t.start();
 Thread.sleep(1000);
 System.out.println(t.isAlive());
 }

为什么要加上 sleep ,因为要保证 t 线程结束了,再进行打印,因为线程的调度是随机的。

作者:熵减玩家原文地址:https://blog.csdn.net/liwuqianhzc/article/details/142886475

%s 个评论

要回复文章请先登录注册