Start 方法源码深究——模板方法设计模式

2023-09-21 20:30:03

权限管理

一. 🦁 前言

先来了解一下线程启动到生命结束的大致过程:
在这里插入图片描述

1.1 New状态

当我们用关键字 new 创建一个 Thread 对象时 ,此时它并不处于执行状态 ,因为没有调用 start 方法启动该线程 ,此时线程的状态为 new 状态 ,它只是 Thread 对象的状态(相当于new 了一个Thread对象),因为在没有 start() 之前 ,该线程根本不存在 ,new 状态时通过 start 方法进入 Runnable 状态。

1.2 Runnable

线程对象进入 Runnable 状态必须调用 start 方法 ,此时才真正地在 JVM 进程中创建 了一个线程。
线程一经启动就会立即得到执行吗?
答案是否定的 ,线程的运行与否和进程一 样都要听令于 CPU 的调度 ,所以我们把这个中间状态称为可执行状态(Runnable) ,也就是 说它具备执行的资格 ,但是并没有真正的执行起来而是在等待 CPU 的调度
由于存在 Running 状态 ,所以不会直接进入 Blocked 状态和 Terminated 状态 ,即使是在线程的执行逻辑中调用 wait、 sleep 或者其他block 的 IO 操作等 ,也必须先获得 CPU 的调度执行权才可以 ,严格来讲 ,Runnable 的线程只能意外终止或者进入 Running 状态。

1.3 Runing

一旦 CPU 通过轮询或其他方式从任务可执行队列中选中了线程 ,那么此时它才能真正 地执行 run 方法里的逻辑代码。在该状态中 ,线程的状态可以发生如下的状态转换。

  • 直接进入 Terminated 状态 ,比如调用 JDK 已经不推荐使用的 stop 方法或者意外死亡;
  • 进入 Blocked 状态 ,比如调用 sleep 或者 wait 方法而加入了 waitSet 中;
  • 进行某个阻塞的 IO 操作 ,比如因网络数据的读写而进入了 Blocked ;
  • 获取某个锁资源 ,从而加入到该锁的阻塞队列中而进入了 Blocked ;
  • 由于 CPU 的调度器轮询使该线程放弃执行 ,进入 Runnable 状态;
  • 线程主动调用 yield 方法 ,放弃 CPU 执行权 ,进入 Runnable;

1.4 Block状态

上面已经介绍了线程进入 Blocked 状态的原因 ,这里不在赘述。线程在 Blocked 状态 中可以切换至如下几个状态。

  • 直接进入 Terminated 状态 ,比如调用 JDK 已经不推荐使用的 stop 方法或者意外死 亡;
  • 线程阻塞的操作结束 ,比如读取了想要的数据字节进入到 Runnable;
  • 线程完成了指定时间的休眠 ,进入到了 Runnable ;
  • wait 中的线程被其他线程 notify/notifyall 唤醒 ,进入 Runnable ;
  • 线程获取到了某个锁资源 ,进入 Runnable;
  • 线程在阻塞过程中被打断 ,比如其他线程调用了 interrupt 方法 ,进入 Runnable。

1.5 Terminated状态

Terminated 是一个线程的最终状态 ,在该状态中线程将不会切换到其他任何状态 ,线程进入 Terminated ,意味着该线程的生命周期都结束了 ,下面这些情况会使线程进入 Terminated 状态。

  • 线程运行正常结束 ,结束生命周期;
  • 线程运行出错 ,意外结束;
  • JVM Crash,导致所有的线程都结束。

二. 🦁 线程 start 方法源码剖析

    /**
     * 2.1 Causes this thread to begin execution; the Java Virtual Machine
     * calls the {@code run} method of this thread.
     * <p> 
     * 2.2 The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * {@code start} method) and the other thread (which executes its
     * {@code run} method).
     * <p>
     * 2.3 It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @throws     IllegalThreadStateException  if the thread was already started.
     * @see        #run()
     * @see        #stop()
     */
    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();

所谓源码之下无秘密,源码的注释里面已经跟我们解释了start()方法的作用以及相关要点,我们根据上面标注的序号,一个一个来看:

2.1 虚拟机调用run方法执行线程

当调用start()后,JVM虚拟机则会调用run()方法,线程开始进入可执行状态。

2.2 最少有两个线程在执行

在main方法中,新起的线程其实都是主线程下开辟的子线程,子线程在执行时,主线程一定也会执行,他俩交替并行执行。则此时则一共有两条线程在执行(只有一条子线程的情况下)。

2. 3 不可以重复执行

子线程在调用过程中,不允许被重复执行两次,即不能重复调用两次start()。
即使子线程结束后,也是不能重新被调用的,否则会抛出线程不合法异常(IllegalThreadStateException)。

2.4 start方法体

我们现在来看一下start方法体里面的流程:

  • 状态检验
    这里会给我们来个状态检验,检验线程是否是初次执行,如果是初始执行的,则threadStatus = 0,表示NEW新建状态,否则,抛出IllegalThreadStateException();
 if (threadStatus != 0)
            throw new IllegalThreadStateException();

实践一下:启动一个start(),打断点调试:
在这里插入图片描述
在这里插入图片描述
如果再调用一次start()
在这里插入图片描述
结果如下:
在这里插入图片描述
此时threadStatus = 5,再执行则会抛出异常了。
在这里插入图片描述

  • 添加线程进线程组
group.add(this);
  • 调用c++的native方法,执行run()
 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 */
            }
        }
    }

这里调用c++的native方法,执行run(),如果启动失败,则从线程组中移除当前线程。

所以这里其实就是为了执行run(),但是我们前面写的时候是直接调用了start()方法,为什么会这样呢?

这里就涉及到一个叫 模板方法的 设计模式。

三. 🦁 模板方法设计模式

3.1 基本概念

定义了一个操作算法的框架,将一些步骤延迟到子类中执行使得子类可以不改变一
个算法的结构即可重定义该算法的某些步骤
还可以通过子类来决定父类算法中某个步骤是
否执行,实现子类对父类的反向控制

比如启动线程 start 的行为由 JVM 虚拟机来决定,但是该线程做什么事情就不由它来 决定了。对外提供了 run 方法,内部就是你自己线程的执行逻辑。

3.2 自定义一个场景实现模板方法模式

拿做西红柿炒蛋的例子来说明,同样的步骤其实不同的人做出来的饭是不一样的。就拿自己
和五星级大厨来比较吧。比如就做个西红柿鸡蛋,我们可以简单地定义一下步骤:

  • 第一步:放油
  • 第二步:放鸡蛋
  • 第三步:放西红柿

如果按照模板方法的思路去构建,我们需要剥离出两个角色:

  1. 模板方法:父类,定义一系列方法,提供骨架;
  2. 具体类:实现模板方法类提供的骨架。根据自己的个性化需求重写模板方法
/**
 * 模板方法类
 */
public abstract class Cook {
    abstract void oil();        //放油
    abstract void egg();        //打鸡蛋
    abstract void tomato();     //放西红柿
    
    // 封装具体的行为:做饭
    public final void cook(){
        oil();
        egg();
        tomato();
    }
}
  • 继承模板方法类
public class MyCooking extends Cook{
    @Override
    void oil() {
        System.out.println("我:"+"适量油!");
    }

    @Override
    void egg() {
        System.out.println("我:"+"搅拌适量鸡蛋!");
    }

    @Override
    void tomato() {
        System.out.println("我:"+"放西红柿!");
    }
}

public class Chef extends Cook{
    @Override
    void oil() {
        System.out.println("大厨:"+"适量油!");
    }

    @Override
    void egg() {
        System.out.println("大厨:"+"搅拌适量鸡蛋!");
    }

    @Override
    void tomato() {
        System.out.println("大厨:"+"放西红柿!");
    }
}
  • 实现:
public class Test {
    public static void main(String[] args) {
        new MyCooking().cook();
        new Chef().cook();
        System.out.println("我和大厨做得一样好吃!");
    }
}

在这里插入图片描述

3.3 钩子函数

这里就实现了模板设计模式,但是似乎还没做到通过子类来决定父类算法中某个步骤是 否执行,实现子类对父类的反向控制,这里我们需要通过钩子函数去实现:

  • 我们在模板方法里面添加一个钩子函数,让他决定炒菜过程中是否要放油:

/**
 * 模板方法类
 */
public abstract class Cook {    
    abstract void oil();        //放油
    abstract void egg();        //打鸡蛋
    abstract void tomato();     //放西红柿

    boolean isOil(){
        return true;
    }

    // 封装具体的行为:做饭
    public final void cook(){
        if (isOil())
            oil();
        egg();
        tomato();
    }
}

  • 在其中一个实现类中,重写isOil():
public class MyCooking extends Cook{

    private boolean isAddOilFlag = true;

    @Override
    boolean isOil() {
        return this.isAddOilFlag;
    }

    @Override
    void oil() {
        System.out.println("我:"+"适量油!");
    }

    @Override
    void egg() {
        System.out.println("我:"+"搅拌适量鸡蛋!");
    }

    @Override
    void tomato() {
        System.out.println("我:"+"放西红柿!");
        System.out.println("没放油,菜糊啦!!!");
    }
}
  • 测试结果:
    在这里插入图片描述

在这里插入图片描述

🦁 其它优质专栏推荐 🦁

🌟《Java核心系列(修炼内功,无上心法)》: 主要是JDK源码的核心讲解,几乎每篇文章都过万字,让你详细掌握每一个知识点!

🌟 《springBoot 源码剥析核心系列》一些场景的Springboot源码剥析以及常用Springboot相关知识点解读

欢迎加入狮子的社区:『Lion-编程进阶之路』,日常收录优质好文

更多文章可持续关注上方🦁的博客,2023咱们顶峰相见!

更多推荐

React中组件通信02——消息订阅与发布、取消订阅以及卸载组件时取消订阅

React中组件通信02——消息订阅与发布、取消订阅以及卸载组件时取消订阅1.前言1.1使用props通信1.2关于useEffect2.安装pubsub-js3.消息订阅与发布3.1简单例子-13.2简单例子-2(完善、优化)——订阅消息+使用消息4.取消订阅4.1取消单个topic4.2取消多个或更多语法4.3卸载

LeetCode 面试题 04.09. 二叉搜索树序列

文章目录一、题目二、C#题解一、题目从左向右遍历一个数组,通过不断将其中的元素插入树中可以逐步地生成一棵二叉搜索树。给定一个由不同节点组成的二叉搜索树root,输出所有可能生成此树的数组。点击此处跳转题目。示例1:输入:root=[2,1,3]输出:[[2,1,3],[2,3,1]]解释:数组[2,1,3]、[2,3,

macOS 12 Monterey:一次全新的跨设备协作体验

macOS12Monterey是苹果公司的一次重大突破,它打破了设备间的壁垒,将不同设备无缝地连接在一起,极大地提升了用户的工作效率和娱乐体验。Monterey带来了通用控制、AirPlay、捷径等新功能,以及一些实用的新小功能。安装:macOS12Montereyv12.6.9正式版功能特点通用控制macOS12Mo

会“穿墙术”的神奇材料 ——超固体

超固体(supersolid)是一种具备超流特性的固体,也就是集“超流体+固体”特性于一身的物质。简单来说就是超固体既有晶体态中原子规则排布的特征,又可以像超流体一样无摩擦流动。在凝聚态物理学中,超固体是具有超流体特性的空间有序材料。超固体是一种晶体材料,其原子排列结构规则且重复,也能够永远流动而不损失任何动能。尽管它

SpringMVC之自定义注解

目录一、Java注解1.1注解简介1.2注解分类1.3JDK基本注解1.4JDK元注解1.5自定义注解1.5.1标记注解1.5.2元数据注解1.6如何自定义注解二、自定义注解的基本案例2.1案例一(获取类、方法以及属性上的注解)2.1.1@Ingerited的使用2.2案例二(获取类属性上的注解属性值)2.3案例三(获

全球变暖问题(floodfill 处理联通块问题)

全球变暖问题文章目录全球变暖问题前言题目描述题目分析边界问题的考虑岛屿是否被淹没判断:如何寻找联通块:代码预告前言之前我们介绍了bfs算法在二维,三维地图中的应用,现在我们接续进行拓展,解锁floodfill算法,准确的来说是用bfs算法解决联通块问题。后续还会更新bfs算法有关内容,喜欢的小伙伴可以点个关注啦。题目描

数据结构和算法之快速排序

快速排序是一种基于分治法的排序算法。它通过不断地将数组分成较小的子数组,并按照递归的方式对每个子数组进行排序,最终将整个数组排序。#mermaid-svg-Za26UnuASULzGzsM{font-family:"trebuchetms",verdana,arial,sans-serif;font-size:16px

Vue路由与nodejs环境搭建

一.路由什么是路由什么是SPA路由的思路及实现实例建立一个HTML来编写路由测试结果​编辑二.nodejs环境什么是node.jsnpm是什么node.js的下载一.路由什么是路由路由(Routing)是指根据不同的URL地址,将用户导航到不同的页面或视图的过程。在前端开发中,特别是在单页面应用(SPA)中,路由起着至

Python实现MYSQL蜜罐

1LOADDATAINFILE介绍首先开启一个Mysql,看一下mysql是如何读取主机文件的。1.1linux搭建mysql1)docker运行mysql2)启动Mysqldockerrun-itd--namemysql-p3306:3306-eMYSQL_ROOT_PASSWORD=123456mysql3)进入容

Rust常见编程概念

变量和可变性rust使用let声明变量,变量默认是不可改变的。通过在let后面加上mut,可以声明可变变量。可以在变量名后加:和类型名,来显式声明变量类型,例如:leta:u32=1;常量常量使用const声明,变量名一般约定使用大写。隐藏不同的作用域如果有重叠,且重叠区域内有同名的变量,那么更小的作用域里变量会隐藏外

CentOS 7系统安装与配置、常用100条操作命令

CentOS7是一个广泛使用的开源Linux操作系统,它是RedHatEnterpriseLinux(RHEL)的一个免费重建版本,以稳定性和安全性而著称。在CentOS7上安装虚拟机通常使用虚拟化技术,如VirtualBox或VMware等。以下是CentOS7的简要介绍以及如何安装CentOS7虚拟机的步骤。Cen

热文推荐