【多线程】Thread类

news2024/11/26 2:00:12

1. Java中如何进行多线程编程?

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(如 Linux 中的 pthread 库)。

所以本身关于线程的操作,是依赖操作系统提供的的 API,而 Java 的 JVM 已经把很多操作系统提供的功能封装好了,我们就不需要学习系统原生的 API,只需要学习 Java 提供的 API 就好了。

在 Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进一步的抽象和封装!

可以认为,Java 操作多线程最核心的类就是 Thread 类!


2. 简单使用多线程

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

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

上述就是我们第一个多线程代码,使用多线程打印 "hello world"

这里是第一个创建线程的方式,继承 Thread 类,重写 run 方法!

上述代码中的 t.start(); 这里的工作就是创建了一个新的线程,而这个线程负责执行 t 对象中的 run 方法.

start 方法创建一个新的线程,本质上就是调用操作系统的API,通过操作系统内核创建新线程的 PCB,并且把要执行的指令交给这个 PCB,当 PCB 调度到 CPU 上执行的时候,也就执行到了线程的 run 方法中的代码了!

注意:这里可能有个让人误解的地方,start 方法里是没有调用 run 方法的,start 只是创建了一个线程,由新创建的线程去调用 run 方法!

上述我们代码的执行流程就是:主线程(main线程) 中调用 t.start(); 创建了一个新线程,这个新线程调用 t.run(); 如果 run 方法执行完结束了,这个新的线程也会随之销毁。


3. start 和 run 的区别

start 方法是真正创建了一个线程(从系统这里创建的),线程是一个独立的执行流.

run 方法只是描述了线程要干什么样的活,如果直接在 main 方法调用 run,此时是不会创建新线程的,这个 run 方法会在 main 线程中执行:

public static void main(String[] args) {
        MyThread t = new MyThread();
        t.run();
}

上述这种情况,只是单纯在 main 线程中执行 t 对象里的 run 方法罢了!

提问:new 一个 Thead 对象是在干嘛呢?

其实也就是创建一个对象罢了,只不过这个对象能够通过 start 方法创建一个线程罢了!


4. jconsole 工具

我们也可以通过 jdk 自带的工具 jconsole 查看当前的 java 进程中的所有线程(bin 目录下):

因为进程和线程之间是包含关系,当要查看线程的时候,需要先连接上指定的进程,才能看指定进程中所拥有的线程。

此处可以看到一个是 main 线程,也就是主线程,还有一个是我们创建的线程这个默认起了个名字 Thread-0,除了这两个线程之外,其他的线程都是 JVM 自带的,这里我们不用过多关心,后续还会使用这个工具进行查看线程的阻塞状态等


5. Java 中创建线程的写法

● 继承 Thread 重写 run

这里上述我们简单使用多线程的时候已经见过了,这里就不过多讲述了。

● 实现 Runnable 接口 重写 run

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("hello world");
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        // 描述一个任务
        Runnable runnable = new MyThread();
        // 把任务交给线程通过 start 方法来执行
        Thread t = new Thread(runnable);
        t.start();
    }
}

上述的 runnable 对象,只是描述了一个任务,这里的写法最主要就是解耦合,目的让线程和线程要干的活之间分离开。

● 使用匿内部类 继承 Thread

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("hello world");
            }
        };
        t.start();
    }
}

这里创建了一个 Thread 的子类,但是是没有名字(匿名)的,Thrad() 后面大括号中表示子类重写父类 Thread 的 run 方法,最后让 t 引用指向该实例。

● 使用匿名内部类 实现 Runnable

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello world");
            }
        });
        t.start();
    }
}

这样的写法本质上和上一个写法相同,此处只是创建了一个匿名内部类,实现了 Runnable 接口重写了 run 方法,同时创建了类的实例,把这个匿名的 Runnable 对象作为参数传递给了 Thread 的构造方法。

● 使用 Lambda 表达式

public class ThreadDemo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello world");
        });
        // Thread t = new Thread(() -> System.out.println("hello world")); 等价上面
        t.start();
    }
}

此处是通过 Lambda 表达式来描述任务,直接把 Lambda 传给 Thread 构造方法,这里跟上种方法没有啥区别,只是语法的不同而已,因为 Runnable 这个接口就是一个函数式接口,才能使用这种语法,具体内容见 Lambda 章节。

上述介绍的几种写法,离不开 Thread 类,只不过是使用了不同的方法来描述 Thread 里的任务是啥,只是语法规则的不同,本质上都是一样的方法,这些方法创建出来的线程都是一样的,随着后面学习的深入,会见识到其他创建线程的方法但大体都是大同小异。

6. Thread 类方法介绍

6.1 构造方法

方法

说明

Thread()

创建线程对象

Thread(Runnable target)

使用 Runnable 对象创建线程对象

Thread(String name)

创建线程对象,并命名

Thread(Runnable target, String name)

使用 Runnable 对象创建线程对象,并命名

如果使用中直接 Thread t = new Thread(); t.start(); 这样的话相当于执行了一个空的 run 方法:

这里是 Thread 源码中的 run,此处的 target 就是一个 Runnable 类型的,所以要想创建的线程能正常的执行 run方法,要不继承 Thread 类重写 run,要不实现 Runnable 接口重写 run。

上述介绍的构造方法中,最后一个方法,是可以给线程起个名字,取名是为了方便调试,线程默认的名字叫做 Thread-0,Thread-1....

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("hell world");
        try {
            Thread.sleep(10_0000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "myThread");
    t.start();
}

此处加上 Thread.slepp(),让创建的线程进行休眠,为了是让我们通过 jconsole 工具更好的观察:

这里通过查看,确实发现给我们创建的线程取名为 myThread

提问:这里main线程为什么没了呢?

注意看上述代码,main 线程执行完 t.start() 之后,后面就没有任何需要执行的代码了,对于主线程来说,main 方法执行完了也就被销毁了,而且每个线程是一个独立的执行流,main线程的销毁,不影响 myThread 线程的继续执行!

6.2 Thread 类常见属性

属性

对应获取方法

ID

getId()

名称

getName()

状态

getState()

优先级

getPriority()

是否后台线程

isDaemon()

是否存活

isAlive()

是否被中断

isInterrupted()

● ID 是线程的唯一标识,不同的线程 ID 都不同

● 名称 是线程的名字,创建线程对象通过构造方法指定的名称,如果没指定就是默认的名字

● 状态 是线程所处的状态,有很多种,具体我们后续讲解

● 优先级 理论上优先级越高的线程越容易被调度到

● 是否是后台线程(是否是守护线程) 后面会讲解

● 是否被中断,可以通过一些手段中断线程,我们后续讲解

上述的方法获取线程对应的属性,大家可以下来自行尝试一下,这里就不做过多演示了!

6.3 什么是守护线程?

这里守护线程就是后台线程,为什么叫做守护呢?这个是历史遗留翻译的问题,守护这个词语从字面意思确实不好理解,这里更习惯把守护线程叫作后台线程

后台线程(守护线程),不会阻止进程的结束,即使后台线程的工作没有做完,进程也是可以结束的!
前台线程(非守护线程),会阻止进程的结束,如果前台线程的工作没有做完,进程是不能结束的!

注意:我们默认创建的线程都是前台线程!包括 main 方法也是一个前台线程(非守护线程)!

像我们上面通过 jconsole 工具看到的线程,除了自己创建的线程和 main 线程,剩下的都是 JVM 自带的线程,而 JVM 自带的线程都是后台线程(守护线程)

这里也可以使用 setDaemon() 这个方法来将创建的线程设置成后台线程(守护线程):

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("hello world");
    });
    t.setDaemon(true); // 将创建的线程对象对应的线程设置为后台线程
    t.start();
}

这里将线程 t 设置成守护线程后,此时进程的结束与 t 就无关了!

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        while (true) {
            System.out.println("hello");
        }
    });
    t.setDaemon(true); // 将创建的线程对象对应的线程设置为后台线程
    t.start();
    System.out.println("main 方法执行结束!");
}

这段代码,已经将 t 线程设置成后台线程了,此时这个 t 线程要执行的任务是一个死循环,但是真的会一直执行吗?

通过打印不难发现,t 线程并不会永无止境的循环下去,因为将线程设置成守护线程后,我们启动的进程是否结束,就与 t 线程没有关系了,而进程的结束,进程里面对应的线程也会直接结束!

如果没有将 t 设置成守护线程呢?此时就会永无止境的打印 hello !大家可以自行下去测试!

6.5 什么情况线程才是存活的?

上述介绍 Thread 类常见属性的时候,有一个属性是通过调用 isAlive 方法判断线程是否存活,那么线程存活到底是什么意思呢?

简单来说,在线程执行 run 方法的时候,就是存活的,执行 run 方法之前,或者执行完 run 方法之后,线程就不是存活的了!

那么这里我们就要弄清楚,线程是什么时候去执行 run 方法的?

其实在之前就讲到过,只有当线程对象,调用 start 方法后,才会真正的创建一个线程,然后线程去执行对应的 run 方法!

至于线程是否存活,那么就从三个点进行分析,start 之前,是肯定没有存活的,start 之后线程就会执行 run 方法,所以此时线程肯定是存活的,run 方法结束后,线程肯定是没有存活的!

下面就通过一段代码来验证下上述的结论:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        for (int i = 0; i < 100_0000; i++) {

        }
    });
    System.out.println("start之前: " + t.isAlive());
    t.start();
    System.out.println("start之后: " + t.isAlive());
    Thread.sleep(100);
    System.out.println("run 方法执行完毕后 : " + t.isAlive());
}

上述代码就是让 t 线程干一件事,执行一百万次空循环,启动线程之后,令 main 线程等待 100 毫秒,此处的 100 毫秒足够执行完 run 方法中的内容了!

通过打印结果能发现,只有在执行 run 方法的时候,isAlive() 结果才是 true。

此处需要注意,线程把 run 方法执行完了,此时线程销毁,对应的 PCB 随之释放,但是 t 这个对象还不一定被释放,此时 isAlive() 也是 false,所以线程存在与否,与线程对象无关!

6.6 什么是线程中断?

中断的意思是,不是让线程立即就结束,而是通知线程应该要结束了,是否真的结束还取决于线程这里代码的具体写法,这里我们简单来举一个例子:

6.6.1 自定义一个标志位

public class ThreadDemo01 {
    public static Boolean flag = false; // false表示不终止
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!flag) {
                System.out.println("hello world");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        flag = true;
    }
}

上述代码,执行完 t.start() 之后,main 线程休眠 30 秒后,将 flag 变量修改为 ture,表示告诉线程 t 应该要终止了,线程 t 下次循环发现 !flag 为假,就会退出这个循环!

上述这种自定义的方式,有一个缺点,如果上述代码线程中的 sleep 休眠时间太久,就可能不能及时感知到外面的 flag 已经发生改变了!

这里只是通过修改 flag 的方式,告诉线程,应该要结束了,但是这个线程会立马结束吗?其实还是取决于这个线程内部执行的代码,比如上述 t 线程执行的代码中,在 while 循环外再加上其他代码,此时也就不会立马就结束了!

6.6.2 Thread 自带标志位

可以自定义标志位的同时,也可也使用当前线程自带的标志位:

Thread.currentThread().isInterrupted();

前面的是 Thread 类的静态方法,获取线程对象的引用,在哪个线程中调用的,就获取对应的线程的实例,后面的 isInterrupted 则相当于是获取标志位的值,如果为 true 则表示线程该终止了,如果为 false 则表示不用终止,线程继续执行!

同时可以通过 interrupt() 这个方法,就可以通知对应线程该终止了!

下面就把上述的代码改成自带的标志位的模式:

public class ThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello world");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt(); //通知线程该终止了
    }
}

此时发现了一个奇怪现象,调用 interrupt 方法后,居然触发了 t 线程的 sleep 方法的异常?而且 t 线程并没有终止,这是怎么一回事呢?要想了解清楚,首先要弄清楚 interrupt 方法背后做了哪些事情!

interrupt 会做两件事:

  1. 把线程内部的标志位给设置成 true,告诉线程该终止了!

  1. 如果线程在 sleep,则会触发 sleep 的异常,把 sleep 提前唤醒!

但是 sleep 在被唤醒的时候,还会把标志位设置成 false!

扩展:像 wait,join 等类似造成线程 "阻塞挂起" 的方法,都有类似清除标志位的设定。

这样一来,就 interrupt 就白忙活了,如果没有没有 sleep,则是会正常终止上述线程的。

那么这样有什么好处呢?

就举个简单的例子,假设张三在打游戏,张三的女朋友让张三放下游戏陪她去逛街,那么张三就有三种选择:

  • 立刻放下游戏,陪女朋友

  • 忽略女朋友,不管她,当作没听到

  • 等过一会游戏打完,再去陪女朋友

此时我们就可以修改上述的代码了:

立刻放下手机陪女朋友版本:

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("打游戏!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            break; //马上放下手机陪女朋友
        }
    }
});

忽略女朋友版本:

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("打游戏!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("接着打!");
        }
    }
});

过一会再陪女朋友版本:

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("打游戏!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 此处可以写任意代码
            System.out.println("等游戏打完!");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }
});

为什么 interrupt 不设定成立刻终止线程呢?而是让线程自己做选择呢?

因为 CPU 是随机调度线程的,所以当 interrupt 方法执行后,并不确定对应线程执行到哪里了,如果对应线程活还没干完,直接啪一下终止了,这样是很危险的行为!把是否真的终止线程的选择权交给程序猿,这才是一个很好的选择!

同时 Thread 类中还有一个 Thread.interrupted() 方法,手动清除标志位,这个了解即可。


下期预告:【多线程】认识线程的状态

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/424103.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

练习,异常,异常处理,try-catch,throws

package com.jshedu.homework_;/*** author Mr.jia* version 1.0*/ //匿名内部类 public class Homework04 {public static void main(String[] args) {Cellphone cellphone new Cellphone();//1.匿名内部类&#xff0c;同时也是一个对象/*new computer() {Overridepublic dou…

JavaClient With HDFS

序言 在使用Java创建连接HDFS的客户端时,可以设置很多参数,具体有哪些参数呢,只要是在部署HDFS服务中可以设置的参数,都是可以在连接的时候设置. 我没有去验证所有的配置是否都可以验证,只是推测cuiyaonan2000163.com 依据 创建HDFS的构造函数如下所示: 网上比较常用的是get…

gdb 跟踪调式core

自己编译的问题出现段错误: 编译:使用gdb调试core文件来查找程序中出现段错误的位置时,要注意的是可执行程序在编译的时候需要加上-g编译命令选项。 gdb调试core文件的步骤 gdb调试core文件的步骤常见的有如下几种,推荐第一种。 具体步骤一: (1)启动gdb,进入core文…

【剑指 offer】旋转数组的最小数字

✨个人主页&#xff1a;bit me&#x1f447; ✨当前专栏&#xff1a;算法训练营&#x1f447; 旋 转 数 组 的 最 小 数 字核心考点&#xff1a;数组理解&#xff0c;二分查找&#xff0c;临界条件 描述&#xff1a; 有一个长度为 n 的非降序数组&#xff0c;比如[1,2,3,4,5]…

ABAP 创建、修改、删除内部交货单(VL31N/VL32N)

一、干货 VL31N创建的BAPI&#xff1a; 1.GN_DELIVERY_CREATE 通用交货单使用的bapi&#xff0c;推荐使用 2.BAPI_DELIVERYPROCESSING_EXEC 简单&#xff0c;但是字段比较少 3.BBP_INB_DELIVERY_CREATE 听说有bug&#xff0c;我就没有使用这个了 VL32N修改/删除BAPI: BAPI_INB…

每日学术速递4.14

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Deep RL at Scale: Sorting Waste in Office Buildings with a Fleet of Mobile Manipulators 标题&#xff1a;大规模深度强化学习&#xff1a;使用移动机械手对办公楼中的垃圾进行…

VS2022编译libui库

libui是一个 C 中简单且可移植(但并非不灵活)的 GUI 库,它使用每个平台原生的GUI技术进行绘制。 官网地址:链接 本文将使用VS2022编译libui库,操作系统为Windows10。 1. 下载源代码 首先在官网下载源代码,由于此代码不依赖第三库,故只需下载源代码即可进行编译。 我下…

R730服务器环境搭建(centos7、lanproxy、docker、k8s)

文章目录前言一、centos7安装1.制作u盘启动盘2.开始装系统&#xff1a;二、环境安装&#xff08;lanproxy、docker、k8s&#xff09;1.lanproxy安装2.docker安装&#xff08;如果通过k8sOfflineSetup安装k8s可以跳过这一步&#xff0c;因为会自动安装docker&#xff09;3.安装k…

安装 KeyShot 流程

| 安装 KeyShot 流程 KeyShot 安装程序将指导您完成安装过程。 在 Windows 上&#xff0c;安装过程会要求您考虑以下事项终用户协议 为使用计算机的所有人或仅为当前用户安装 KeyShot 安装文件夹的位置 资源文件夹的位置 ——资源文件夹包含许多可以与 KeyShot 一起使用的纹…

NSSCTF doublegame题解

运行一下&#xff0c;是一个贪吃蛇游戏 先玩一玩&#xff0c;蛇的移动速度太快了&#xff0c;玩不了 查壳 64位文件&#xff0c;无壳 进入IDA分析 发现这个EXE文件是开了程序基址随机化&#xff0c;就是每次用IDA打开指令的地址不一样 我们要想使用x64dbg和IDA的时候&#…

Docker的基本操作

文章目录一、 Docker的基本操作1.1 镜像1.1.1 介绍1.1.2 镜像操作1.2 容器1.2.1 介绍1.2.2 容器操作1.3 数据卷1.3 介绍1.3.2 数据卷操作一、 Docker的基本操作 1.1 镜像 1.1.1 介绍 在 Docker 中&#xff0c;镜像&#xff08;Image&#xff09;是一种轻量级、可移植的、可扩…

营销平台一站式集成 高效实现自动化

市面上广告投放渠道渠道那么多&#xff0c;图文、动图、短视频等广告形式也越来越多&#xff0c;许多企业都会有这些疑问&#xff1a; 「腾讯广告、百度营销、巨量引擎哪个广告渠道的客户适合我们公司&#xff1f;」 「这么多广告渠道&#xff0c;哪家的点击率、转化率比较高…

1.Antlr4-简介入门

1.简介: ANTLR v4是一款功能强大的语法分析器生成器&#xff0c;可以用来读取、处理、执行和转换结构化文本或二进制文件。它被广泛应用于学术界和工业界构建各种语言、工具和框架。 2 关键字&#xff1a; import, fragment, lexer, parser, grammar, returns, locals, throw…

运维——记一次接口超时的问题与解决方法(HttpException: Read timed out)

前言&#xff1a;近期,一个线上的项目,请求出现了大量接口超时的问题,找了几个小时原因,最终发现是因为数据库服务器的磁盘满了,在此记录一下寻找的过程以及发现的问题,以备后续参考。 环境&#xff1a; 项目服务器(CentOS 64-bit 7.9) OpenJDK 1.8.0_272 数据库服务器(CentO…

打怪升级之FPGA组成原理(LE部分)

FPGA芯片逻辑单元的原理 不论你使用哪一款FPGA芯片&#xff0c;其核心可编程逻辑单元都是从一段内存种按顺序读取执行并执行的过程。具体来说&#xff0c;FOGA芯片内部包括可编程逻辑块(LAB)、可配置输入输出单元(IOE)、时钟管理模块、嵌入式RAM(BRAN&#xff0c;在Cyclone IV…

【堆的使用】【dfs构建数】二叉树遍历

二叉树遍历方法一&#xff1a;方法二&#xff1a;利用堆的性质原题链接 方法一&#xff1a; 利用dfs构建树 因为这个前序遍历给了我们空的叶节点 所以我们可以只根据叶节点 构建树 abc##de#g##f### 构建图如下 我们根据前序 abc##de#g##f### 发现 dfs左子树 和 右子树 当…

mac系统下使用clion调试redis源码

获取源代码 有两种方式&#xff0c;第一种是从官网下载 Redis 源码压缩包&#xff0c;如图 1-1 所示。 图1-1 将压缩包解压得到一个文件夹。 第二种方式&#xff0c;通过 git clone 获取源码。 从 Github 上&#xff0c;使用 git clone https://github.com/redis/redis.git…

第1章-JVM与Java体系结构

1、本系列博客&#xff0c;主要是面向Java8的虚拟机。如有特殊说明&#xff0c;会进行标注。 2、本系列博客主要参考尚硅谷的JVM视频教程&#xff0c;整理不易&#xff0c;所以图片打上了一些水印&#xff0c;还请读者见谅。后续可能会加上一些补充的东西。 3、尚硅谷的有些视频…

业务转包?行为不可控?企业外包如何保护数据安全?

出于人力、设备、开发成本等各种因素考虑&#xff0c;企业会把一部分业务外包出去&#xff0c;比如AI数据标注外包、IT外包、银行部分业务外包等。同时&#xff0c;随着企业不断向外包业务转移&#xff0c;保护数据安全和隐私已成为企业最为关注的问题之一。 来自国外的一份报告…

1.16 从0开始学习Unity游戏开发--人物控制

上一篇我们简单的做了一个玩家不动的情况下&#xff0c;如何控制准心来射击子弹&#xff0c;但是显然正常的游戏需要移动玩家本体&#xff0c;所以本篇我们需要补全这部分玩法所需的功能。 人物移动 在我们之前的篇章里面&#xff0c;讲解了如何通过物理引擎来实现物体的物理…