JavaEE-多线程初阶(2)

目录

1.创建线程的五种写法

[1.1 继承Thread类](#1.1 继承Thread类)

[1.2 实现Runnable接口](#1.2 实现Runnable接口)

[1.3 使用匿名内部类](#1.3 使用匿名内部类)

[1.4 使用Runnable,匿名内部类](#1.4 使用Runnable,匿名内部类)

[1.5 引入lambda表达式](#1.5 引入lambda表达式)

2.Thread类及常见方法

[2.1 认识Thread](#2.1 认识Thread)

[2.2 Thread的常见构造方法](#2.2 Thread的常见构造方法)

[2.3 Thread的几个常见属性](#2.3 Thread的几个常见属性)

关于后台线程

关于是否存活isAlive()

线程组ThreadGroup

[2.4 启动一个线程 -start()](#2.4 启动一个线程 -start())

[2.5 中断一个线程](#2.5 中断一个线程)

使用自定义变量

变量捕获

使用Thread自带的属性

[2.6 等待一个线程](#2.6 等待一个线程)

[2.7 获得当前线程的引用](#2.7 获得当前线程的引用)

[2.8 休眠当前线程:sleep](#2.8 休眠当前线程:sleep)


1.创建线程的五种写法

1.1 继承Thread类

详细见上篇文章

1.2 实现Runnable接口

详细见上篇文章

1.3 使用匿名内部类

创建Thread内部类,并在Thread内部类里面重写run方法:

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

【注意】

使用匿名内部类可以少定义一些类(比如上面代码就省去了MyThread类)。

一般如果某个代码是"一次性的",就可以使用匿名内部类。

1.4 使用Runnable,匿名内部类

创建Runnable内部类,并在其内部重写run方法,最后作为构造方法的参数传入Thread类:

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

1.5 引入lambda表达式

针对写法三写法四 进一步改进,引入lambda表达式

进入Thread类的定义文件可以发现Thread类是实现了Runnable接口的

再接着进入Runnable接口,会发现Runnable其实是一个函数式接口:

对于函数式接口,可以使用lambda表达式来重写run方法:

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

一般推荐使用lambda表达式的写法。

2.Thread类及常见方法

2.1 认识Thread

Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。
⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,⽽ Thread 类的对象就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。

2.2 Thread的常见构造方法

对上述五个构造方法的解释:

1.创建线程对象

2.使用Runnable对象创建线程对象

3.创建线程对象,并命名

4.使用Runnbale对象创建线程对象,并命名

5.线程可以用来分组管理,分好的组即为线程组

对三个线程分别命名为t1,t2,t3:

java 复制代码
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (true){
                System.out.println("Hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t1");
        Thread t2=new Thread(()->{
            while (true){
                System.out.println("Hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t2");
        Thread t3=new Thread(()->{
            while (true){
                System.out.println("Hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t3");
        t1.start();
        t2.start();
        t3.start();
    }

运行代码,并在jconsole中观察线程,会发现t1,t2,t3线程,这三个线程就是上面代码创建出来并重命名的线程:

2.3 Thread的几个常见属性

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

• ID 是线程的唯⼀标识,不同线程不会重复
• 名称是各种调试⼯具⽤到的
• 状态表示线程当前所处的⼀个情况,后面我们会进⼀步说明
• 优先级高的线程理论上来说更容易被调度到
• 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运⾏。
• 是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
• 线程的中断问题,下⾯我们进⼀步说明


关于后台线程

现有两个线程,将t2线程设置为后台线程(也叫守护线程)t1线程运行五秒t2线程死循环 ,当t1线程 结束时,不论t2(后台线程) 有没有结束,整个进程都会结束:

java 复制代码
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("Hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1线程结束...");
        },"t1");
        Thread t2=new Thread(()->{
            while (true){
                System.out.println("Hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t2");
        t2.setDaemon(true);
        t1.start();
        t2.start();
    }

t1 这种能影响到进程继续存在 的线程就被成为前台线程

一般自己创建的线程(包括main主线程)默认都是前台线程,可以通过setDaemon方法来修改。

使用jconsole观察线程,会发现除了我们自己创建的线程以外,JVM还自带了很多线程:

这些线程都是后台线程,当进程结束了,这些线程也就随之结束了。

JVM提供的这些线程都是属于有特殊功能的线程,会跟随整个进程持续执行的。

比如:垃圾回收线程


关于是否存活isAlive()

JAVA代码中创建的Thread对象,和系统中的线程是一 一对应的关系

但是,Thread对象的生命周期和系统中线程的生命周期是不同的。

(可能存在,Thread对象还存活,但是系统中的线程已经销毁的情况)案例如下:

java 复制代码
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println("Hello Thread");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        while(true){
            System.out.println(t1.isAlive());
            Thread.sleep(1000);
        }
    }

线程组ThreadGroup

Thread(ThreadGroup group, String name)

线程组(不做详细介绍)

就是把多个线程放在一个组里

统一针对这个线程组里的所有线程进行一些属性设置

(比如:统一设置成后台线程)

2.4 启动一个线程 -start()

Java标准库/JVM提供的方法

本质上是调用操作系统的API

每个Thread对象,都是只能start一次的

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

如果一个对象多次创建线程(start):

结果是报错:

Java中期望,Thread对象和操作系统中的线程是一一对应

2.5 中断一个线程

中断一个线程,其实就是终止一个线程(该线程以后不会再恢复了

在操作系统中,"中断"一次还有别的含义,不要混淆

使用自定义变量

先看一个案例:

在类里面定义一个成员变量isInterrupted,在main方法里通过改变isInterrupted的值来控制t1线程的终止:

java 复制代码
public class Demo3 {
    public static boolean isInterrupted=false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            while(!isInterrupted){
                System.out.println("Hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程结束...");
        });
        t1.start();
        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);
        }
        isInterrupted=true;
    }
}

如果不用成员变量,而是把isInterrupted定义在main里面(作为局部变量),再执行代码就会报错:

首先解释一下这个变量该如何修改正确:

1.用final修饰isInterrupted变量:

2.变量isInterrupted实际上为final,意思就是:isInterrupted不能被修改

变量捕获

会有这样的现象,是因为**:lambda里面希望使用外面的变量,就会触发"变量捕获"**。

对于**"变量捕获"**的解释:

1.产生变量捕获的原因

lambda是回调函数,他要在操作系统真正创建出线程之后才会被执行。

因此很有可能会发生:当这个线程刚创建好的时候,main线程就已经结束了,isInterrupted变量也已经被销毁了。

2."变量捕获"

为了解决上述的问题,Java的做法是,把lambda外面的变量拷贝一份到lambda里面,如此一般外面的变量无论销毁与否,都不会影响到lambda里面的执行。这个过程就是**"变量捕获"**

3.为什么不能修改变量

拷贝,就是把一个变量的值拷贝给另一个变量,本质上这两个变量是没有关联的。

由于这两个变量没有关联,修改了lambda外面的变量并不会影响到lambda里面的变量。

因此Java大佬干脆压根就不让程序员修改这个变量。

这就是为什么被捕获的变量必须为final或者effectively final
而使用成员变量的isInterrupted可以被修改,是因为:

1.lambda本质上是函数式接口,相当于一个内部类

2.isInterrupted变量是外部类的成员。

3.内部类本来就可以访问外部类的成员

4.成员变量的生命周期是由GC(垃圾回收线程)来管理的。在lambda里不必担心该变量生命周期失效的问题,也就不需要发生"变量捕获",也就不必限制final之类。

使用Thread自带的属性

Java的Thread对象中提供了现成的变量,直接进行判定,不需要再自己创建了。

但是由于lambda里的定义是在new Thread之前的,也就是在Thread t声明之前,因此不能直接使用t1:

此时就需要用到获得当前线程引用的方法:Thread.currentThread()

这个方法可以返回当前线程的引用(相当于this):

java 复制代码
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                try {
                    System.out.println("Hello Thread");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程结束...");
        });
        t1.start();
        Thread.sleep(3000);
        System.out.println("main线程尝试终止t1线程");
        t1.interrupt();
    }

执行结果:

执行后系统抛出异常,异常原因:sleep被中断。

解释:

当t1.interrupt()执行时,此时t1线程内大概率还在处于sleep(1000)的状态,interrupted()的执行强行唤醒了sleep,因此就会抛出异常,异常被try catch捕获,然后抛出一个RuntimeException。

将抛出的RuntimeException给改成break就不会抛出异常了,但是sleep被唤醒的异常依旧存在:

为什么要使用break?如果不加break(直接空着),这个线程就会继续死循环执行

原因:

正常来说,调用Interrupt方法就会修改isInterrupted方法内部的标志位,设为true

由于上述代码中,是把sleep给强行唤醒了,这种提前唤醒的情况下,sleep就会在唤醒后,把isInterrupted的标志位设置回false

因此,while循环条件达成,会继续进行死循环执行

至于为什么要这样设计,个人认为是想让程序员拥有更多选择:

程序员可以自行决定,这个线程是要立即结束,要是等会再结束,还是不结束...

2.6 等待一个线程

方法:join()

作用:从此处开始阻塞等待,等到一个线程结束了再继续执行

案例:

java 复制代码
    public static void main(String[] args) throws InterruptedException {
        Thread t1=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);
                }
            }
            System.out.println("t1线程结束...");
        });
        t1.start();
        t1.join();
        System.out.println("main线程结束...");;
    }

​​​​​​​

执行结果:

但是这样设计,只要线程t1不结束,主线程的join就会一直等待下去,这样并不科学。

因此Java提供了带参数的方法,可以指定超时时间(最大等待时间)

当等待的时间超出了设置好的超时时间,不论t1线程是否结束,main线程都继续执行。

join方法还有带两个参数的版本:

一般不会用到纳秒这个参数:在计算机中,很难进行ns(纳秒)级别的精确时间的测量(误差比较大)。尤其是,线程本身的开销往往就会达到ms级别。

1s=1000ms

1ms=1000ns

1us=1000ns

2.7 获得当前线程的引用

方法:public static Thread currentThread();

作用:返回当前线程对象的引用(这个方法在哪个线程里就返回哪个线程的引用)

java 复制代码
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("t1:"+Thread.currentThread().getName());
        });
        t1.start();
        System.out.println("main:"+Thread.currentThread().getName());
    }

2.8 休眠当前线程:sleep

因为线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。
语法:

java 复制代码
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(1000*3);
        System.out.println(System.currentTimeMillis());
    }

如果哪里有疑问的话欢迎来评论区指出和讨论,如果觉得文章有价值的话就请给我点个关注还有免费的收藏和赞吧,谢谢大家

相关推荐
追风林9 分钟前
mac 本地docker-mysql主从复制部署
mysql·macos·docker
aloha_78911 分钟前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java41 分钟前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山41 分钟前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
睡觉谁叫~~~1 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程1 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
观音山保我别报错1 小时前
C语言扫雷小游戏
c语言·开发语言·算法
程序媛小果1 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot
小屁孩大帅-杨一凡2 小时前
java后端请求想接收多个对象入参的数据
java·开发语言
m0_656974742 小时前
C#中的集合类及其使用
开发语言·c#