当前位置: 首页 > article >正文

JavaEE-多线程基础知识

文章目录

  • 前言与回顾
  • 创建一个多线程
    • 线程的创建以及运行机制简述
    • step1: 继承Thread类
    • step2: 实现Runable接口
    • step3: 基于step1使用匿名内部类
    • step4: 基于step2使用匿名内部类
    • step5: 基于step4使用lambda表达式(推荐)
  • Thread的常见方法
    • 关于jconsole监视线程的工具
    • 构造方法解析
    • 获取 ID / 名称
    • 查看 / 设置后台线程
    • 查看线程的状态
    • 获取 / 设置线程的优先级
    • 查看线程存活
  • 中断线程以及阻塞唤醒机制
    • 使用自定义标志位终止线程
    • lambda中的变量捕获

前言与回顾

基础的一些关于线程/进程的一些基础的概念, 已经在之前的帖子中有过解释
简单复习一下

  • 线程是轻量级的进程(进程太重了, 创建销毁代价大)
  • 进程包括线程
    进程是操作系统资源分配的基本单位
    线程是操作系统调度执行的基本单位
  • 线程的调度是操作系统进行的, 在应用层无法进行干预, 也无法感知
  • 线程是操作系统级别的概念, 操作系统内核实现了这种机制并封装提供用户一些API
    Java中的Thread类可以视为是对操作系统的API的进一步封装和抽象

创建一个多线程

线程的创建以及运行机制简述

我们创建线程的最基本的方法其实是两种(后续还有)

  • 通过定义一个类继承Thread类重写run方法
  • 通过定义一个类实现Runnable接口重写run方法

run方法相当于主线程的main, 作为每一个线程的程序运行的入口, 我们通过start方法启动一个线程, 这时候操作系统的内部会真正的创建一个线程(源码层面是去调用的start0native修饰的本地方法, 自行调用cpp动态链接库), start0会调用run方法, 所以这相当于是一种回调函数的机制
但是我们提供了五种创建线程的方式, 其实本质都是基于上述两种(后续还要其他方法)
关于start的源码

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

// 本地方法
 private native void start0();

也就是真正起作用的是start0, 如果好奇native修饰的方法的具体实现, 可以去JDK官网上自行查看JVM源码

step1: 继承Thread类

实现代码如下, 我们让每一个线程都睡眠1000ms便于观察

/**
 * 第一种创建线程的方式
 * 继承Thread类重写run方法
 */

class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread!");
            // 睡眠一秒钟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new MyThread();
        // 开启一个线程
        t.start();

        // 主线程也执行
        while(true){
            System.out.println("hello main!");
            // 睡眠一秒钟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

开始运行, 可以明显的看到线程t和线程main在交替的执行, 这也说明了是我们的底层的抢占式调度模型在起作用, 多个线程不断地抢夺cpu时间片

在这里插入图片描述

step2: 实现Runable接口

每一个逻辑加上一个sleep方法休眠便于观察

/**
 * 第二种创建线程的方式
 * 定义一个类实现Runnable接口
 */
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个线程t
        Thread t = new Thread(new MyRunnable());
        // 开启一个线程t
        t.start();

        // 主线程的逻辑
        while(true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述

step3: 基于step1使用匿名内部类

/**
 * 基于第一种方法使用匿名内部类
 */
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        // 创建t线程
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        // 开启t线程
        t.start();

        // 主线程的逻辑
        while (true) {
            System.out.println("hello main!");
            Thread.sleep(1000);
        }
    }
}

step4: 基于step2使用匿名内部类

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        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
        t.start();

        // 主线程的逻辑如下
        while(true){
            System.out.println("hello main!");
            Thread.sleep(1000);
        }
    }
}

step5: 基于step4使用lambda表达式(推荐)

lambda表达式本质上就是一个匿名的函数, 最主要的用途就是作为回调函数, 很多语言都有, 只不过叫法有差异而已

  • 底层编译器会对lambda表达式进行处理, 创建了一个匿名的函数式接口的子类, 并且创建了对应的实例并且重写里面的方法

代码实例

/**
 * 使用lambda表达式进行问题的描述
 */
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("hello t!!!");
            }
        }, "t");

        // 开启t线程
        t.start();

        // 主线程中的逻辑
        while(true){
            Thread.sleep(1000);
            System.out.println("hello main!!!");
        }
    }
}

在这里插入图片描述
这里可以看到, 我们的两个线程正在交替的执行逻辑
我们推荐这种方式进行平时的线程的测试, 因为比较的轻量级

Thread的常见方法

关于jconsole监视线程的工具

构造方法解析

是我们的JDK中自带的一个检测当前线程的工具, 在我们的JDK安装路径中的bin目录下, 可以在这个目录下找到这个检测的工具
在这里插入图片描述
我们开启一个多线程的逻辑, 我们这里的测试代码是上面的step5创建线程的方法, 可以先去看一下…
打开之后是下面的窗口
在这里插入图片描述
这里我们查看 t线程和main线程

在这里插入图片描述
在这里插入图片描述
这里我们可以看到线程的状态
比如现在的两个线程都位于TIMED_WAITING状态(sleep(1000))
还可以查看当前线程的堆栈跟踪状态是什么


这里其实还说明了一些问题, 我们的后台其实存在多个线程(本质上是后台线程, 比如跟网络通讯相关的线程, 还有跟垃圾回收 GC 相关的一些线程) 这些线程维持我们 Java 程序的正常执行


在这里插入图片描述
这张图是JDK的帮助文档的截图, 我们逐一解释一下里面的一些主要的方法(关于线程组创建线程的方法我们暂时略过, 后期回来继续说, 其实就是把线程进行分组)

方法签名描述
Thread()直接创建线程对象
Thread(Runnable target)使用实现了Runnable接口的对象来创建线程
Thread(String name)创建线程并传入一个名字(便于使用jconsole进行监视)
Thread(Runnable target, String name)前两种的结合
Thread(ThreadGroup group, Runnable target)使用线程组来创建线程(现阶段了解)

获取 ID / 名称

对于每一个线程来说, 都有一个独一无二的 ID (标识码, 类似PID)
对于每个线程来说, 程序开发者可以选择指定一个名字(或者系统自动分配)

  • getID: 获取当前线程的标识码
  • getName: 获取当前线程的名称
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException{
        // 创建一个t线程
        Thread t = new Thread(() -> {
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getId() + " " + Thread.currentThread().getName());
            }
        }, "t线程");

        // 开启t线程
        t.start();

        // 主线程中的逻辑
        while(true){
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getId() + " " + Thread.currentThread().getName());
        }
    }
}

在这里插入图片描述
可以观测到此时的随机调度的线程的 ID 以及 name

查看 / 设置后台线程

后台线程其实就是守护线程, 其实就是进程的"幕后人员"

  • setDaemon(boolean) : 传入一个true把当前线程设置为守护线程(start前)
  • isDaemon(): 查看当前线程是否是守护线程

守护线程是当所有的用户线程全部结束之后, 自动就结束了, 但是如果守护线程自己选择结束了, 那也算是结束了, 所以我们的守护线程一般用于链接网络, 维护数据库日志等等场景

class ThreadTest01 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个t线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("守护线程执行ing");
            }
        }, "t线程");

        // 设置t线程为守护线程
        t.setDaemon(true);
        // 开启t线程
        t.start();

        while (true) {
            Thread.sleep(1000);
            System.out.println("main线程执行ing");
        }
    }
}

在这里插入图片描述
通过jconsole检测也可以发现两个线程的情况, 可以看到两个位置都是TIMED-WAITING状态

查看线程的状态

  • getState(): 查看当前线程的状态(有六种状态)
class ThreadTest01 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个t线程
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("守护线程执行ing " + Thread.currentThread().getState());
            }
        }, "t线程");

        // 设置t线程为守护线程
        t.setDaemon(true);
        // 开启t线程
        t.start();

        while (true) {
            Thread.sleep(1000);
            System.out.println("main线程执行ing " + Thread.currentThread().getState());
        }
    }
}

在这里插入图片描述
实质上有多种状态描述, 我们此时的状态是可运行状态

获取 / 设置线程的优先级

class ThreadTest01 {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个t线程
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 查看当前线程的优先级
            System.out.println("默认的优先级: " + Thread.currentThread().getPriority());
        }, "t线程");
        t.start();
        System.out.println("最低优先级: " + Thread.MIN_PRIORITY + " 最高优先级: " + Thread.MAX_PRIORITY);
    }
}

在这里插入图片描述

  • setPriority(): 设置线程的优先级(0 - 10)

这其实是一个概率问题, 定义一个线程抢占cpu时间片的概率, 但不是绝对的

查看线程存活

  • isAlive(): 判断当前线程是否存活

请注意, 线程的存活和线程对象的生命周期一般是不一样的
比如下面的代码

class ThreadTest02{
    public static void main(String[] args) throws InterruptedException {
        // 创建一个t线程
        Thread t = new Thread(() -> {
            System.out.println("...");
        }, "t线程");
        t.start();

        while(true){
            Thread.sleep(1000);
            System.out.println(t.isAlive());
        }
    }
}

在这里插入图片描述
此时线程以及结束了(run方法执行结束), 但是线程对象并没有销毁

中断线程以及阻塞唤醒机制

使用自定义标志位终止线程

定义一个static变量作为标志位然后打标记


public class ThreadTest {

    // 自定义一个标志位打标记
    private static boolean isFinished = false;

    public static void main(String[] args) throws InterruptedException {
        // 使用lambda机制创建一个线程
        Thread t = new Thread(() -> {
            while(!isFinished){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }, "t线程");

        // 开启t线程
        t.start();

        // 三秒之后终止t线程
        Thread.sleep(3000);
        isFinished = true;
    }
}

在这里插入图片描述
可以看到 3s 之后终止 t 线程

lambda中的变量捕获

如果上面的代码的标志位是局部变量, 那么我们就触发了lambda表达式中的变量捕获语法
在这里插入图片描述
这个代码我们发现报错了

  • 因为我们的lambda是一种回调函数, 当我们的线程执行到了, 我们的main线程可能就直接销毁了, 这个变量很有可能就直接销毁了, 所以Java中针对这种语法的解决方案是, 直接拷贝一份变量的值赋值给lambda表达式中的isFinished, 所以本质上, 这两个isFinished不是一个变量, 就意味着这个变量不适合修改, 所以我们编译器就直接不允许这种变量的修改…而对于引用变量的处理逻辑是, 不可以修改引用, 但是可以修改内部的值

http://www.kler.cn/a/401606.html

相关文章:

  • 前端接入Paymax支付请求
  • 蓝桥杯每日真题 - 第16天
  • Orcad 输出有链接属性的PDF
  • 【多状态dp】买卖股票的最佳时机III
  • 机器翻译-基础与模型
  • 20241116解决在WIN11和ubuntu20.04通过samba共享时出现局域网千兆带宽拉满的情况
  • C++ ─── 哈希表(unordered_set 和unordered_map) 开散列和闭散列的模拟实现
  • 搜维尔科技:基于Touch力反馈与VR技术的虚拟气管切开术的虚拟操作软件平台
  • CentOS 环境下通过 YUM 安装软件
  • OpenAI 助力数据分析中的模式识别与趋势预测
  • 疫情期间基于Spring Boot的图书馆管理系统
  • 基于yolov8、yolov5的行人检测识别系统(含UI界面、训练好的模型、Python代码、数据集)
  • Vue所有图片预加载加上Token请求头信息、图片请求加载鉴权
  • 小米运动健康与华为运动健康在苹手机ios系统中无法识别蓝牙状态 (如何在ios系统中开启 蓝牙 相册 定位 通知 相机等功能权限,保你有用)
  • 23种设计模式-模板方法(Template Method)设计模式
  • Unix发展历程的深度探索
  • 时代变迁对传统机器人等方向课程的巨大撕裂
  • react 使用中注意事项提要
  • CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
  • Java 多线程详解
  • 掌握SEO提升网站流量的关键在于长尾关键词的有效运用
  • Pytest 学习 @allure.severity 标记用例级别的使用
  • 使用Python实现智能食品市场预测的深度学习模型
  • python语言基础-5 进阶语法-5.2 装饰器-5.2.5 装饰器使用案例(自定义装饰器实现方法重载)
  • 【青牛科技】视频监控器应用
  • CSV 文件读取