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

news2025/1/18 3:18:12

线程的创建

方式一:继承 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);

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

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

上述方式我们只是创建了 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 线程结束了,再进行打印,因为线程的调度是随机的。

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

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

相关文章

Spring Boot知识管理系统:敏捷开发实践

3系统分析 3.1可行性分析 通过对本知识管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本知识管理系统采用JAVA作为开发语言&#xff0c;Spring Boot框…

通过OpenCV实现 Lucas-Kanade 算法

目录 简介 Lucas-Kanade 光流算法 实现步骤 1. 导入所需库 2. 视频捕捉与初始化 3. 设置特征点参数 4. 创建掩模 5. 光流估计循环 6. 释放资源 结论 简介 在计算机视觉领域&#xff0c;光流估计是一种追踪物体运动的技术。它通过比较连续帧之间的像素强度变化来估计图…

开发一款陪玩系统所需成本,做线下家政服务需要多少预算?

开发陪玩系统所需的预算因多种因素而异&#xff0c;包括开发成本、功能复杂度、系统规模以及定制需求等。以下是对陪玩系统开发预算的详细分析&#xff1a; 一、开发成本 基础功能开发 如果只需要支持简单的语音或文字聊天和游戏匹配功能&#xff0c;开发成本可能相对较低。这…

倾斜摄影切片教程:快速上手,简单又高效!

倾斜摄影是一种从多个角度&#xff08;通常是垂直、斜45度&#xff09;拍摄地面或建筑物的影像技术&#xff0c;通过结合这些不同视角的照片&#xff0c;可以生成具有真实感的三维模型。倾斜摄影通常用于城市建模、地形勘测和测绘等领域&#xff0c;能够准确还原建筑物和地形的…

谷歌浏览器主题和皮肤设置教程

谷歌浏览器是一款功能强大且广受欢迎的网络浏览器。除了提供快速、安全的浏览体验外&#xff0c;谷歌浏览器还允许用户通过自定义主题和皮肤来个性化自己的浏览器界面。本文将详细介绍如何设置谷歌浏览器的主题和皮肤&#xff0c;并介绍一些相关的实用技巧。 &#xff08;本文…

【牛客刷题实战】BC120 争夺前五名

大家好&#xff0c;我是小卡皮巴拉 文章目录 目录 牛客题目&#xff1a; BC120 争夺前五名 题目描述 输入描述&#xff1a; 输出描述&#xff1a; 示例1 示例2 解题思路&#xff1a; 具体思路&#xff1a; 题目要点&#xff1a; 完整代码&#xff1a; 兄弟们共…

UE4 材质学习笔记07(叶子摇晃着色器/雨水潮湿着色器/材质函数/雨滴着色器)

一.叶子摇晃着色器 现实世界中叶子的晃动十分复杂&#xff0c;这次我们将实现每个叶子都动起来&#xff0c;来接近现实世界的效果 先让它整体动起来&#xff0c;可以用time节点&#xff0c;然后用sin节点限制移动的范围 接下来就是找到一个方法&#xff0c;把这条树枝上的单个…

MySQL中的增查操作:探索数据的奥秘,开启数据之门

本节&#xff0c;我们继续深入了解MySQL&#xff0c;本章所讲的基础操作&#xff0c;针对的是表的增删查改&#xff01; 一、Create 新增 1.1、语法 INSERT [INTO] table_name[(column [, column] ...)] VALUES(value_list) [, (value_list)] ... value_list: value, [, va…

WIFI(1)

WIFI&#xff08;1&#xff09; 目录 WIFI&#xff08;1&#xff09; 回顾 WIFI模块 -- 1、AP模式&#xff1a;&#xff08;服务器&#xff09;应用&#xff1a;主要是用来让用户设置自身的wifi密码 -- 2、STA模式&#xff1a;可以获取时间获取天气 应用&#xff1a; 代…

暴力破解案例

暴力破解 1 概述 暴力破解&#xff0c;是一种针对密码的破译方法&#xff0c;将密码进行逐个推算直到找出真正的密码为止。 2 爆破HTTP协议 第一步:实验性发送请求成功 import requests url "http://192.172.0.100:8080/woniusales/user/login"data {"use…

CUDA - nvprof 性能刨析

nvprof 是一个可执行文件&#xff0c;使用everything搜索可以看到它在&#xff1a; 执行命令&#xff1a; nvprof exe_name 如何在windows 下使用&#xff0c;可以参看: windows下使用nvcc和nvprof。 示例 #include <cuda_runtime.h> #include <device_launch_par…

YOLOv8实战水果识别【数据集+YOLOv8模型+源码+PyQt5界面】

本文采用YOLOv8作为核心算法框架&#xff0c;结合PyQt5构建用户界面&#xff0c;使用Python3进行开发。YOLOv8以其高效的实时检测能力&#xff0c;在多个目标检测任务中展现出卓越性能。本研究针对水果数据集进行训练和优化&#xff0c;该数据集包含丰富的水果图像样本&#xf…

Merlion笔记(二):单变量时间预测

1 简单示例 我们首先导入Merlion的TimeSeries类和M4数据集的数据加载器。然后&#xff0c;我们可以将该数据集中的特定时间序列划分为训练集和测试集。 from merlion.utils import TimeSeries from ts_datasets.forecast import M4time_series, metadata M4(subset"Hou…

ClickHouse入库时间与实际相差8小时问题

原因一&#xff1a;服务端未修改默认时区 解决方案&#xff1a; 1、找 ClickHouse 配置文件 config.xml&#xff0c;通常位于 /etc/clickhouse-server/ 目录。 2、编辑 config.xml 文件&#xff0c;找到 标签。如果标签不存在&#xff0c;需要手动添加。 3、修改 标签的内容为 …

docker部署dvwa靶场

一、简述前情 最近差不多又是网安专业的同学搭建靶场的时候了&#xff0c;DVWA&#xff08;Damn Vulnerable Web Application&#xff09;是安全入门者们最为熟知的练习靶场&#xff0c;它能够帮助小白对常见的安全问题进行理解&#xff0c;并且随着理解问题的深入自主调整靶场…

Redis-02 数据持久化

redis持久化即将数据从内存写入磁盘&#xff0c;Redis提供了两种持久化的方式&#xff1a;RDB和AOF。 1.RDB RDB持久化&#xff1a;Redis可以将内存中的数据定期快照保存到磁盘上的一个二进制文件中。RDB持久化是一种比较紧凑的文件格式&#xff0c;适用于备份和灾难恢复。通过…

vue3中监视 Reactive对象中的属性

watch 的第一个参数可以是不同形式的“数据源”&#xff1a;它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组 一、框架&#xff1a; <template><div class"divBox"><h2>姓名&#xff1a;{{ person.…

【深圳大学/大学物理实验2】超声探伤实验 实验前预习题答案参考

一、单选题 共 6 小题 共 31 分 1. (5分)电偶极矩的单位是&#xff08; &#xff09; 学生答案&#xff1a;B √ A. B. C. D. 2. (5分)本实验中产生超声波的方法是&#xff08; &#xff09; 学生答案&#xff1a;D √ A. 热电效应 B. 光电效应 C. 电磁效应 D. 压电效…

【专题】关系数据库标准语言SQL

1. SQL语言介绍 SQL(Structured Query Language)是结构化查询语言&#xff0c;它是一种在关系数据库中定义和操纵数据的标准语言&#xff0c;是用户与数据库之间进行交流的接口。 1.1 SQL数据库的体系结构 使用SQL关系数据库的特点&#xff1a; SQL用户可以是应用程序&#xf…

闯关leetcode——100. Same Tree

大纲 题目地址内容 解题代码地址 题目 地址 https://leetcode.com/problems/same-tree/description/ 内容 Given the roots of two binary trees p and q, write a function to check if they are the same or not. Two binary trees are considered the same if they are…