Java多线程详解③(全程干货!!!)Thread Runnable

这里是Themberfue


· 上一节我们使用了最基础的方式来创建一个线程,并实现了多线程代码的运行。

· 这一节我们更进一步的介绍其他创建线程的方式。

Thread

我们先来看一段通过Thread类创建线程的代码

java 复制代码
public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        // 创建匿名内部类,继承了Thread类
        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("My main");
            Thread.sleep(1000);
        }
    }
}

· 上述代码创建与之前说到的略有不同,这次而是直接通过new Thread类来实现。

· 但其实本质上和上节的无二区别,这是通过匿名内部类创建,该匿名内部类继承了Thread,并重写了 run 方法,随后启动该线程。

· 随后启动线程,线程执行的逻辑就是匿名内部类重写run方法里的代码逻辑。

· 这样写的好处之一就是不用创建过多的类,方便后续更改其执行逻辑。


Runnable

· 前面我们提到,直接在Thread类里重写run方法,执行里面的逻辑,那么代码的耦合性会增大,这是不利于后续进一步编程的。

· 所以实现Runnable接口重写run方法,随后通过Thread的构造方法,执行run方法里的逻辑。这是一种解耦合的方式,它将线程和任务的概念分开了,不至于耦合在一起。

java 复制代码
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        // 匿名内部类,实现了Runnable接口
        Runnable runnable = new Runnable() {
            @Override
            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("My main");
            Thread.sleep(1000);
        }
    }
}

· 与前一个代码示例类似,也是通过匿名内部类的方式创建一个任务,该匿名内部类实现了Runnable接口,重写了run方法。

· 随后提供Thread类的构造方法将这个任务交给我们创建的这个线程去执行,可以实现解耦合的效果。


Lambda

· Lambda表达式想必大家不陌生,这是一种匿名函数接口,其最主要的用途就是作为 "回调函数"

· 但是在Java中,其必须依托于类才可以使用lambda表达式,这和其他语言还是有区别的

· lambda表达式本质是一个函数式接口( () -> {} ),创建了一个匿名的函数式接口的子类,创建出相应的实例,并且重写了里面的方法

· Thread同样支持lambda表达式的方式重写run方法,由于Runnable是接口,不是一个类,所以不能提供该方法创建

java 复制代码
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        // 使用lambda表达式
        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("My main");
            Thread.sleep(1000);
        }
    }
}

Thread的构造方法

· Thread类提供了丰富的构造方法,等待我们去挖掘,我们先了解四个

|--------------------------------------|---------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用Runnable对象创建线程对象 |
| Thread(String name) | 创建线程对象并且给其命名 |
| Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并且给其命名 |

我们可以在创建线程时给其命名,便于后续出现线程安全问题时,可以快速定位到哪个线程出现了问题。

java 复制代码
public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "T1"); // 给线程命名
        t1.start();
    }
}

运行上述代码,通过 jconsole 查看该线程的名字。


Thread提供的方法

· 我们先来看 Thread 的几个常见属性,表格如下

|---------|-----------------|
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否为后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |

· ID 是线程的唯一标识,就和MySQL的主键一个性质,在这个进程不可能出现重复的 ID

· 名称可以根据 Thread 的构造方法进行手动设置,默认为 Thread-0,从0开始,依此递增

· 线程的状态主要分为就绪状态和阻塞状态,但其又细分了数个状态,这我们放在以后细说

· 优先级高的线程更容易被系统先调度

· 后台线程是什么?后台线程也称守护线程,它们的存在不影响进程的结束,如果进程结束了,那么它们也随即结束了,在一个进程运行好后,JVM会默认创建几个后台线程

· 手动创建的线程,比如 main 线程,它们默认都是前台线程,如果存在多个前台线程,必须所有前台线程结束后,进程才会结束

· 是否存活?也就是线程此时是否在执行中

· 是否被中断?线程是否被中断后停止运行


后台线程

前面的提到的 后台线程,默认自己创建的线程都是前台线程,不过我们通过 Thread类提供的 setDaemon 来将其设置为后台线程。

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

        // 设置为后台线程(守护线程) 必须在start之前
        t.setDaemon(true);

        t.start();

        for (int i = 0; i < 3; i++) {
            System.out.println(1);
            Thread.sleep(1000);
        }
    }
}

· Java 代码中创建的 Thread 对象,和系统中的线程是一一对应的。

· 但是,Thread对象的生命周期和系统中的线程的生命周期可能不是对应的,会存在,Thread对象还存活,但是线程已经被系统销毁的情况。

· 通过如下代码加以演示

java 复制代码
public class Demo8 {
    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();

        while (true) {
            // 查看线程是否运行中
            System.out.println(t.isAlive());
            Thread.sleep(1000);
        }
    }
}

· 上述代码中,3秒后,t 线程执行完逻辑就会被操作系统给销毁,但是 main线程在同样打印3 次 t 线程的存活状态后,可能还会出现第四次打印 t 线程的存货状态,切输出为 true

· 这是为什么?不难发现,线程在系统的调度的是随机的,三秒后,虽然 t 线程确实好像执行完了逻辑,但此时 main 线程被调度了,也就打印了 t 线程的存活状态,为 true

· 好比 t 线程还留有一口气,没有完全被销毁


线程终止

· 如果线程正常执行完逻辑,而后线程被销毁,这并不叫 "线程终止" ,这只是线程正常退出了

· 我们可以通过 isInterrupted() 方法来查看线程是否终止了,通过 interrupt() 方法来终止线程

· 不过,在使用上述方法前,我们先来看一个现象,请看下面的代码

java 复制代码
public class Demo9 {
    private static boolean isFinished = false;

    public static void main(String[] args) throws InterruptedException {
        // boolean isFinished = false;
        Thread t = new Thread(() -> {
            while (!isFinished) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        Thread.sleep(3000);

        isFinished = true;
        System.out.println("t线程结束了");
    }
}

· 上述代码,为什么使用成员变量来控制一个线程是否终止,而不是简单的使用局部变量?

· 如果设置为局部变量,那么其必须是常量或者不可变变量,这是为什么?

· lambda 想要使用外面的变量,会触发 "变量捕获" 的语法

· 由于 lambda 本质上是回调函数,其执行时机,可能是很久之后,这是相对计算机的时间的,在完全创建完线程后可能才执行里的代码逻辑。但此时 main 线程的逻辑可能已经结束了,那么就意味着这个变量被销毁了,此时再去捕获的话就会失败。

· 所以 JVM 会在先对这个变量进行一个临时拷贝,拷贝到 lambda 里面,既然是拷贝的变量,那么这两个变量就是形参和实参的关系,一个变量的修改不会影响到另外一个变量,也就不希望对这个变量进行修改

· 所以 JVM 只允许使用常量或者不可变变量
· 若将其设置为成员变量,由于 lambda 本质上是一个函数式接口,相当于一个内部类

· 成员变量也就是外部类的成员,内部类本身就可以访问外部类的成员变量,不需要对其进行变量捕获操作

· 成员变量的销毁时交给 GC (garbage collection) 机制来回收的,不必担心变量提取被销毁的问题


使用 Thread 类提供 isInterrupted() 和 interrupt() 就可以就实现上述操作

java 复制代码
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //  Thread.currentThread()表示 t 线程,类似于 this
           while (!Thread.currentThread().isInterrupted()) {
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   // throw new RuntimeException(e);
                   break;
               }
           }
        });
        t.start();

        System.out.println(Thread.currentThread().getName());
        Thread.sleep(3000);
        System.out.println("t线程终止");
        t.interrupt();
    }
}

好的~~

本节的内容就讲到这里,下期将带来更多有关多线程的精彩内容~~

😍😍😍

相关推荐
黄名富2 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想4 分钟前
JMeter 使用详解
java·jmeter
言、雲7 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
OopspoO11 分钟前
qcow2镜像大小压缩
学习·性能优化
TT哇14 分钟前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
A懿轩A35 分钟前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
汪洪墩36 分钟前
【Mars3d】设置backgroundImage、map.scene.skyBox、backgroundImage来回切换
开发语言·javascript·python·ecmascript·webgl·cesium
云空42 分钟前
《QT 5.14.1 搭建 opencv 环境全攻略》
开发语言·qt·opencv
Yvemil742 分钟前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java
Anna。。44 分钟前
Java入门2-idea 第五章:IO流(java.io包中)
java·开发语言·intellij-idea