【JavaEE初阶】第四节.多线程基础篇 Thread类的使用、线程的几个重要操作和状态

news2024/9/21 2:37:38

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

前言

一、Thread类的常见构造方法

二、Thread 的几个常见属性

三、和线程相关的几个重要的操作 

      3.1 启动线程 - start()

      3.2 中断线程

      3.3 等待线程 - join() 

      3.4 获取到线程引用 

      3.5 休眠线程 - sleep()

四、线程的状态

      4.1 Java 线程中的基本状态

      4.2 线程之间的状态是如何转换的

总结


前言

今天我们将进入到Java线程的基础篇中有关 Thread类的使用、线程的几个重要操作和状态的学习,本节内容和上一节内容息息相关,请连贯的进行学习!!!!!


一、Thread类的常见构造方法

解析:

Thread():

默认无参构造方法,如 :Thread t1 = new Thread();


Thread(Runnable target):

使用 Runnable 创建一个任务,再把 任务 放到线程里面,

如 Thread t2 = new Thread(new MyThread());


Thread(String name):

给线程起一个名字,线程 在操作系统内核里 没有名字,只有一个身份标识;

但是 Java中 为了让程序猿 调试的时候方便理解,这个线程是谁;

就在 JVM 里给对应的 Thread对象 加了个名字(JVM中的 Thread对象 和 操作系统内核里面的线程 一一对应);

这个名字对于程序的执行没影响,但是对于程序猿调试来说还是挺有用的;

如果不手动设置,也会有默认的名字,形如 Thread-0、Thread-1;


Thread(Runnable target):

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

package thread;
 
public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },("我的线程"));
        t.start();
 
        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

程序运行,我们可以找到 jconsole.exe 去看一看:

二、Thread 的几个常见属性

 

解析:

getId():获取到的是 线程在 JVM 中的身份标识

线程中的身份标识是有好几个的:内核的 PCB 上有标识;到了用户态线程库里 也有标识(pthread,操作系统提供的线程库);到了 JVM 里又有一个标识(JVM Thread类 底层也是调用操作系统的 pthread 库) 

三个标识各不相同,但是目的都是作为身份的区分;

由于 介绍的是 Java程序,所以我们只需要知道 JVM 中的身份标识即可;


getName():

在 Thread 构造方法里,自己所起的名字


getState():

PCB 里面有状态,此处得到的状态是 JVM 里面设立的状态体系,这个状态比操作系统内置的状态要更丰富一些


getPriority():

获取到优先级


isDaemon():

daemon 称为 "守护线程",也可以理解为 "后台线程";

类似于 手机app,打开一个app,此时这个app就来到了 "前台",当按到 "回到菜单" 这样的按钮,此时app就切换到 "后台";
线程也分成 前台线程 和 后台线程(这个是可以自己来设置的),创建线程出来默认为是 前台线程,前台线程 会阻止进程结束;换句话说,进程会保证所有的前台线程都执行完了 才会退出;

当然,main 这个线程就是一个前台线程

后台线程不会阻止进程结束,所以 进程退出的时候 不关后台进程是否执行完 就退出了

package thread;
 
public class Demo7 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },("我的线程"));
        t.start();
    }
}

执行结果:

分析:

由于该线程是一个前台线程,所以需要等待 其运行结束,进程才会结束,所以会一直执行下去

再如:我们可以把他手动设置成 后台线程:

运行结果:

分析:

通过 setDaemon(true) 可以把线程设置为后台线程,等到主线程执行完,进程就结束了;

需要注意的是,先要 设置线程,然后再启动线程

示例:

package thread;
 
public class Demo8 {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"我的线程");
 
        t.start();
 
        System.out.println("线程Id:" + t.getId());
        System.out.println("线程名称:" + t.getName());
        System.out.println("线程状态:" + t.getState());
        System.out.println("线程优先级:" + t.getPriority());
        System.out.println("是否后台线程(true/false):" + t.isDaemon());
        System.out.println("是否存活:" + t.isAlive());
    }
}

 运行结果:

这些操作都是获取到的是 一瞬间的状态,而不是持续性的状态

三、和线程相关的几个重要的操作 

3.1 启动线程 - start()

创建 Thread实例,并没有真的在操作系统内核里创建出线程(仅仅只是在安排任务,"跑步时 的 '各就各位,预备' ")!!!

而是 调用 start,才是真正创建出线程("发令枪响")!!!

这个在上面和上一篇的博客也提到过,这里就不做过多演示了


3.2 中断线程

怎么让线程执行结束,其实方法很简单:只要让线程的 入口方法 执行完了,线程就随之结束了

主线程 的入口方法,就可以视为 mian方法

所谓的 "中断线程",就是让线程尽快把 入口方法执行结束


方法一:直接自己定义 标志位 来区分线程是否要结束

运行结果:

这个代码中,控制线程结束,主要是这个线程里有一个循环,这个循环执行完,就算结束了;

很多时候创建线程都是 让线程完成一些比较复杂的任务,往往都有一些循环,正是因为有循环,执行的时间才可能比较长;如果线程本身执行的很快,刷的一下就结束了,那么也就没有提前控制它的必要了

方法二:使用 Thread类 中自带的标志位

这种方法是可行的

Thread 其实内置了一个标志位,不需要咱们去手动创建标志位;

Thread.currentThread().isInterrupted()
 
--currentThread() 是 Thread类的静态方法,获取到当前线程的实例,这个方法中有一个线程会调用它
--线程1 调用这个方法,就能返回线程1 的 Thread对象;
--线程2 调用这个方法,就能返回线程2 的 Thread对象~
--类似于 JavaSE 里面的 this~
 
--isInterrupted() 为判定内置的标志,可以获取到标志位的值,为 true 表示线程要被中断~
package thread;
 
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程运行中......");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
 
        Thread.sleep(5000);
        System.out.println("控制新线程退出!");
        t.interrupt();
    }
}

运行结果:

 

说明:

调用了 interrupt,产生了一个异常;异常虽然出现了,但是线程仍然在继续运行;

注意理解 interrupt方法 的行为:

如果 t 线程 没有处在阻塞状态,此时 interrupt 就会修改内置的标志位;
如果 t 线程 正在处于阻塞状态,此时 interrupt 就让线程内部产生阻塞的方法,例如 sleep 抛出异常;
上述循环代码中,正好异常被捕获了;

而捕获之后,啥也没有干 只是打印了一个调用栈:

  

而如果 把上述代码中的  e.printStackTrace(); 给注释掉,那么就啥也不打印,运行结果调用栈也不打印了,直接忽略异常了
 

正是因为这样的捕获操作,程序员就可以自行控制线程的退出行为了:

即 在里面可以自主的微操作了

当然,除了 Thread.currentThread().isInterrupted(),Thread类还自带了一个静态方法 interrupted() 也可以访问标志位

使用 Thread.interrupted() 即可

如果当前线程处于运行的状态,就是修改标志位
如果当前线程处于阻塞的状态,则是触发一个异常,线程内部就会通过这个异常被唤醒
这里也不做过多叙述了,一般掌握一个就可以了;

3.3 等待线程 - join() 

我们已经知道,线程之间的执行顺序完全是随机的,看系统的调度;

一般情况下写代码,其实是比较讨厌这种随机性的,更需要能够让顺序给确定下来;

join() 就是一种确定线程执行顺序的 辅助手段;

如:咱们不可以确定两个线程的开始执行顺序,但是可以通过 join() 来控制两个线程的结束顺序

如:上一篇博客曾举了一个例子:

 private static void concurrency() {
        //concurrency 的意思是 "并发"
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread(() ->{
            int a = 0;
            for(long i = 0; i < count; i++) {
                a++;
            }
        });
 
        Thread t2 = new Thread(() ->{
            int a = 0;
            for(long i = 0; i < count; i++) {
                a++;
            }
        });
 
        t1.start();
        t2.start();
 
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        long end = System.currentTimeMillis();
        System.out.println("多线程并发执行的时间:" + (end-begin) + "毫秒");
 
    }

我们知道,main、t1、t2 三个线程是随机调度执行的;

但是此处的需求是,希望让 t1、t2 都执行完了之后,main再进行计时,来统计执行时间;

就是希望 t1、t2 先执行完,main 后执行完;

咱们无法干预 调度器的行为(调度器还是该咋随机咋随机),但是可以让 main 线程进行等待;

就在 main 里分别调用了 t1.join() 和 t2.join() 

t1.join() —> main阻塞,等待 t1 执行完~

t2.join() —> main阻塞,等待 t2 执行完~ 

当 t1、t2 都执行完了以后,main 解除阻塞,然后才能继续往下执行,才能执行完;

注意:main 阻塞了,就不参与调度了,但是 t1、t2 仍然参与调度,它们的执行还是会 随机调度、交替执行的;

main 有点特殊,不太方便 join();

一般情况下,想让谁阻塞,谁就调用 join() 即可 ;

如:要实现让 t2 等待 t1,main 等待 t2,就可以 main 去调用 t2.join(),t2 调用 t1.join() 即可;

代码:

package thread;
 
public class Demo11 {
    private static Thread t1 = null;
    private static Thread t2 = null;
 
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main begin");
        t1 = new Thread(() -> {
            System.out.println("t1 begin");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end");
        });
        t1.start();
 
        t2 = new Thread(() -> {
            System.out.println("t2 begin");
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 end");
        });
        t2.start();
 
        t2.join();
        System.out.println("main end");
    }
}

运行结果:


如:

package thread;
//控制 main 先运行 t1,t1 执行完 再执行 t2
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main begin");
        Thread t1 = new Thread(() -> {
            System.out.println("t1 begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end");
        });
        t1.start();
 
        //等待 t1 结束
        t1.join();
 
        Thread t2 = new Thread(() -> {
            System.out.println("t2 begin");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 end");
        });
 
        t2.start();
        t2.join();
        System.out.println("main end");
    }
}

运行结果:

join() 的行为:

  1. 如果被等待的线程 还没执行完,就阻塞等待;
  2. 如果被等待的线程 已经执行完了,就直接返回;

当然,join() 还有其他的版本

在实际的开发过程中,在一般情况下,都不会使用 "死等" 的方式;

因为 "死等" 的方式有风险;

万一代码出了 bug 没控制好,就很容易让服务器 "卡死",无法继续工作;

更多的情况下是 等待的时候预期好最多等多久,超过时间了就需要做出一些措施;

3.4 获取到线程引用 

为了对线程进行操作,就需要获取到线程的引用;

这里的操作,就是指:线程等待、线程中断、获取各种线程的属性;

如果是 继承 Thread类,重写 run方法,可以直接在 run 中使用 this 即可获取到线程的实例;

但是如果是 Runnable 或者 Lambda,this 就不行了(指向的就不是 Thread实例);

更通用的方法是,使用 Thread.currentThread() ,currentThread() 是一个静态方法,其特点是 哪个线程来调用这个方法,得到的线程就是哪个实例;

3.5 休眠线程 - sleep()

sleep() 能够让线程休眠一会儿

前面已经所介绍了 sleep() 的使用方法,现在来画图介绍一下 sleep() 到底是如何使用的;

 注意:实际上应该是 双向链表 连接,不过为了简单,所以画的就是 单向链表 了;

注意:

如果写了一个 sleep(1000),那么也不一定 就会在1000ms 之后就上 CPU 运行

1000ms 之后只是把这个 PCB 放回了就绪队列!!!至于说这个线程啥时候执行,还得看调度器的心情

因此,sleep(1000) 意味着休眠时间不小于 1000ms,实际的休眠时间会略大于 1000ms,这个误差精度在 10ms 以下

 

四、线程的状态

4.1 Java 线程中的基本状态

操作系统中 进程的状态 有 阻塞状态、就绪状态、执行状态;

而在 Java/JVM里 线程中也有一些状态,更是对此做出了一些细分;

New:安排了工作,还未开始行动,创建了 Thread对象,但是还没有调用 start方法,系统内核里面没有线程;

Runnable:就绪状态,包含了两个意思:

正在 CPU上运行
还没有在 CPU 上运行,但是已经准备好;
Blocked:等待锁;

Waiting:线程中调用了 wait方法;

Time Waiting:线程中通过 sleep方法 进入的阻塞;

Terminated:工作完成了,系统里面的线程已经执行完毕,销毁了(相当于线程的 run方法 执行完了),但是 Thread对象 还在;

Blocked、Waiting、Time Waiting 三种状态都是 阻塞状态,只不过是产生阻塞状态的原因不一样,Java里面使用不同的状态来表示了

代码示例;

package thread;
 
public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        });
 
        //在 t.start() 之前获取,获取到的是 线程还未创建的状态
        System.out.println(t.getState());
 
        t.start();
        t.join();
 
        //在 t.join() 之后获取,获取到的是线程已经结束之后的状态
        System.out.println(t.getState());
    }
}

运行结果:

 4.2 线程之间的状态是如何转换的

主干道是:New => Runnable => Terminated ;

在 Runnable状态 会根据特定的代码进入支线任务,这些 "支线任务" 都是 "阻塞状态";

这三种 "阻塞状态",进入的方式不一样,同时阻塞的时间也不同,被唤醒的方式也不同;

如:sleep() 等到时间到了自动唤醒,至于 wait() 和 synchronized() 是如何唤醒的以后会介绍的;


总结

今天的内容就分享到这里了,我们下一节内容再见!!!!!!!!!!!!!

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

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

相关文章

JDK 8新特性之基本发展史

目录 一&#xff1a;Java SE的发展历史 二&#xff1a;Open JDK来源 三&#xff1a;Open JDK 和 Oracle JDK的关系 四&#xff1a;Open JDK 官网介绍 小结 &#xff1a; 一&#xff1a;Java SE的发展历史 Sun公司在1991年成立了一个称为绿色计划( Green Project )的项目&a…

Ubuntu22.04 美化

一&#xff1a;安装软件 sudo apt install gnome-tweaks chrome-gnome-shell sudo apt install gtk2-engines-murrine gtk2-engines-pixbuf sudo apt install sassc optipng inkscape libcanberra-gtk-module libglib2.0-dev libxml2-utils 二&#xff1a;安装GNOME扩展插件…

Windows 卸载 Visual Studio Code、MinGW-w64、CMake

文章目录1.卸载 Visual Studio Code1.1 在控制面板中找到 Visual Studio Code 将其卸载1.2 删除之前安装过的插件1.3 删除用户信息和缓存信息2.卸载 MinGW-w642.1 删除之前解压出来的文件夹2.2 删除之前配置过的环境变量3.卸载 CMake3.1 删除之前解压出来的文件夹3.2 删除之前配…

无 Hadoop 环境部署 Kylin4

1相比于 Kylin 3.x&#xff0c;Kylin 4.0 实现了全新 spark 构建引擎和 parquet 存储&#xff0c;使 kylin 不依赖 hadoop 环境部署成为可能。无Hadoop环境也降低了运维成本&#xff0c;减少了系统资源占用。 以下操作基于centos7.6 单机版本 部署版本信息如下 JDK 1.8Hive …

【Javascript】面向对象编程,this,原型与原型链,类与实例,class,实现Map,stack,Queue ,Set

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录对象中的方法/thisthis使用bind函数原型原型链类与实例classclass语法补充Map实现Map实现stack实…

【国产GD32芯片解析中科微北斗+GPS模块经纬度数据详细教程-附完整代码工程】

国产GD32芯片解析中科微北斗GPS模块经纬度数据详细教程-附完整代码工程简介准备工作PC端需要用到的工具代码下载地址GD32F103C8T6最小系统板代码实现GD32串口引脚定义如下&#xff1a;串口的初始化串口0初始化代码&#xff1a;串口1初始化代码串口的输入串口0的输入代码如下&am…

非标准包 game.rgss3a 的打开方法 | 2023 年实测

写在前面&#xff1a;最近在玩 RPG 游戏&#xff0c;想拆一个 Game.rgss3a 包&#xff0c;在网上找了很久的拆包方法&#xff0c;感觉写的比较凌乱&#xff0c;我来给大家整理一下吧。不过我本人的技术能力也很差&#xff0c;不确定说的是不是对的&#xff0c;就当是给大家提供…

中国智造助推跨境电商企业迈向全球市场

现今&#xff0c;跨境电商行业发展的如火如荼&#xff0c;中国智造也在不断助推跨境电商企业迈向全球市场。业内人员在新常态下的思想也有了一些改变&#xff0c;现在的跨境电商都是“平台物流”&#xff0c;在物流环节&#xff0c;也需要我们的专业团队去进行整合&#xff0c;…

GD32F450寄存器和库函数

GD32F4xx用户手册 GD32F450xx数据手册 GD32F3x0固件库使用指南 一、寄存器介绍 1. 存储器映射表 GD32是一个32位的单片机&#xff0c;它的地址范围为2的32次方&#xff0c;也就是4GB的地址空间。 为了降低不同客户在相同应用时的软件复杂度&#xff0c;存储映射是按Corte…

python能做的100件事03-python爬虫

文章目录1. scrapy介绍2 新建爬虫项目3 新建蜘蛛文件4 运行爬虫5 爬取内容5.1分析网页结构5.2 关于Xpath解析5.3 接着解析电影数据5.4 下载缩略图5.5 完整代码6 最后说明本例基于python3和scrapy爬虫框架&#xff0c;不再介绍python的基础知识和爬虫的基本知识。1. scrapy介绍 …

制药企业的发展趋势--行业公司数据调研

制药行业是国家重点培育发展的战略产业。制药行业的发展对人民健康、医药科技和社会进步等方面都有着十分重要的作用。下面笔者将阐述近年来制药行业发展的现状及趋势&#xff0c;并对制药行业的研发、销售等多维度的信息进行展示与解读。中国制药企业现状目前&#xff0c;中国…

分享103个PHP源码,总有一款适合您

PHP源码 分享103个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 103个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1_T5IzwgcntFuyqulehbSzQ?pwdv6ds 提取码&#…

vue-router 使用与原理分析

简介 Vue Router 是Vue.js的官方路由。与Vue.js核心深度集成&#xff0c;让用Vue.js构建单页应用&#xff08;SPA&#xff09;变得更加简单。 使用 创建 1、在安装好Vue Router依赖后&#xff0c;在App.vue中引入router-view&#xff0c;它是渲染的容器 <div id"…

ARM X210开发板的软开关按键问题

一、X210 开发板的软启动电路详解 《x210bv3.pdf》 (1) 210 供电需要的电压比较稳定&#xff0c;而外部适配器的输出电压不一定那么稳定&#xff0c;因此板载了一个文稳压器件 MP1482. 这个稳压芯片的作用就是外部适配器电压在一定范围内变化时稳压芯片的输出电压都是 5V。 (2)…

Redis 主从复制

目录一、简介二、复制功能三、将服务器设置为从服务器3.1、手动设置3.2、REPLICAOF 配置选项3.3、取消复制四、查看服务器的角色4.1、查看主服务器4.2、查看从服务器五、其他配置5.1、无硬盘复制5.2、降低数据不一致情况出现的概率5.3、可写的从服务器5.4、选择性命令传播六、w…

SpringBoot统一功能处理

目录 一、统一用户的登录验证 1.1 Spring AOP 实现用户统一登录验证 1.2Spring拦截器实现统一用户的登录验证 1.3 实例演示&#xff08;通过url访问controller层的方法&#xff09; 二、统一异常处理 三、统一数据格式返回 3.1 统一数据返回格式的优点 3.2 统一数据返回…

旺季到来,跨境电商卖家年末冲刺!

又是一年年末时&#xff0c;随着新年的到来&#xff0c;在年底这段时间里&#xff0c;对于跨境电商卖家来说&#xff0c;又是一个关键节点。而现在&#xff0c;卖家们也将迎来一年一度的旺季收官&#xff0c;在此过程中卖家需要做好哪些准备做好年末冲刺呢&#xff1f; 在许多…

量化策略——准备3 数据、Backtrader回测框架与quantstats评价指标

我们一般使用AKShare这个库来获取股票数据或策略中用得到的数据&#xff1a; AKShare github主页&#xff1a;https://github.com/akfamily/akshare 使用Backtrader框架作为回测的框架&#xff1a; Backtrader github主页&#xff1a;https://github.com/mementum/backtrader …

【CTF】git源码泄露和代码审计

目录 源码获取 函数绕过 解法一&#xff1a; 解法二&#xff1a; 参考文章&#xff1a; 源码获取 这里做的web题目是buuctf中的&#xff1a;[GXYCTF2019]禁止套娃 打开页面&#xff0c;查看源码是没有可以利用的点的。开始进行目录扫描 这里使用的工具是dirsearch。直接…

前端js实现多次添加累加文件上传和选择删除(django+js)- 编辑回显文件并上传 (二)

前言 后端返回的是文件地址&#xff0c;并不是文件流或base64编码字符串&#xff0c;而修改数据的接口又只接受文件。 本篇文章主要是基于累加文件上传介绍的。 添加上传文件文章链接&#xff1a;https://blog.csdn.net/qq_43030934/article/details/128726549?spm1001.2014.…