剑指JUC原理-2.线程

创建和运行线程

直接使用Thread

java 复制代码
// 创建线程对象
Thread t = new Thread() {
 public void run() {
 // 要执行的任务
 }
};
// 启动线程
t.start();

--------------------------------------------------
    
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
 @Override
 // run 方法内实现了要执行的任务
 public void run() {
 log.debug("hello");
 }
};
t1.start();

输出:
19:19:00 [t1] c.ThreadStarter - hello

使用Runnable配合Thread

其特点是:把"线程"和"任务"(要执行的代码)分开

Thread代表线程

Runnable代表可运行的任务(线程要执行的代码)

java 复制代码
Runnable runnable = new Runnable() {
 public void run(){
 // 要执行的任务
 }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start(); 

--------------------------------------------------
    
// 创建任务对象
Runnable task2 = new Runnable() {
 @Override
 public void run() {
 log.debug("hello");
 }
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

输出:
19:19:00 [t2] c.ThreadStarter - hello

在java8以后,可以使用lambda来精简代码,快捷键是选中Runnable alt + enter

Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

之所以Runnable能使用lambda,主要是Runnable接口有@FunctionalInterface,该注解只允许存在一个接口方法。

Thread和Runnable的关系(源码)

通过定位源码发现 Runnable的实现方式,其实走的还是run方法,有Runnable的还是优先执行Runnable的run方法,而Thread本身的实现其实就是重写了run方法。

总结:

  • Thread是把线程和任务合并在了一起,Runnable是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

FutureTask配置Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

java 复制代码
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

输出:
19:22:27 [t3] c.ThreadStarter - hello
19:22:27 [main] c.ThreadStarter - 结果是:100

FutureTask代码分析

其中get方法是一个主线程阻塞同步等待task执行完毕的结果

多线程同时执行

java 复制代码
public static void main(String[] args) {
        new Thread(() -> {
            while(true) {
                log.debug("running");
            }
        },"t1").start();
        new Thread(() -> {
            while(true) {
                log.debug("running");
            }
        },"t2").start();
    }

实际的输出结果是交替执行,谁先谁后并不由我们来控制,是由底层的任务调度器来决定的。

查看进行线程的方法

windows

任务管理器可以查看进程和线程数,也可以用来杀死进程

tasklist 查看所有进程

taskkill 进程号 杀死某个进程

java

jps 命令查看所有 Java 进程

jstack 查看某个 Java 进程(PID)的所有线程状态

linux

ps -fe 查看所有进程 ps -fT -p 查看某个进程(PID)的所有线程 kill 杀死进程 top 按大写 H 切换是否显示线程 top -H -p 查看某个进程(PID)的所有线程

线程运行之原理

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈) 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟 机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

以debug的方式来介绍栈帧

java 复制代码
	public static void main(String[] args) {
        method1(10);
    }

    private static void method1(int x) {
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }

    private static Object method2() {
        Object n = new Object();
        return n;
    }

我们对method1 打上断点进行debug

首先可以看到main先进入了栈帧中,其Variables中 有其参数

当继续进行的时候,此时method1进入栈帧,同时其Variables 包含其入参

继续执行,其Variables 包含其局部变量

继续执行,此时method2进入栈帧

然后是返回n

由于栈的数据结构是先进先出的,当执行完毕后,会对method2进行释放

以上就是栈帧的整个执行流程了。

图解线程流程

最开始是执行一个类加载,将类加载到java虚拟机中,加载的位置是将字节码放置在方法区

类加载完成后,java虚拟机就会启动一个main的主线程,给主线程分配一块栈内存,此时线程就交给任务调度器调度执行

如果cpu分配给我们的主线程了

虚拟机会给main方法分配一块栈帧内存

当然每个栈内存中还存在一个程序计数器的组件,它是每个线程私有的,记录了我当前该执行哪行代码,cpu就向程序计数器要 是什么

局部变量表其实是创建时其实就已经分配好了,不是运行到某行代码时才分配内存

其中method1栈帧中 x = 10,并且相应的语句也会逐渐的到程序计数器中

然后的流程其实就是和debug一样,逐层的释放掉了

多线程运行原理

java 复制代码
public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                method1(20);
                // 断点模式 要选择 thread 不要选择 all
            }
        };
        t1.setName("t1");
        t1.start();
        method1(10);
    }

    private static void method1(int x) {
        int y = x + 1;
        Object m = method2();
        System.out.println(m);
    }

    private static Object method2() {
        Object n = new Object();
        return n;
    }

此时的断点模式要选择 Thread

其实本质上和单线程是一样的,不同点在于,每个线程都有一个自己私有的栈,每个栈里面还是个单线程一样,但是线程切换的话,会保存当前的操作。其实最本质的就是要理解,每个栈内存是相互独立的。

线程上下文切换

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能
相关推荐
金銀銅鐵1 天前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路1 天前
go-grpc设计思想
开发语言·后端·golang
system_error_x1 天前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn1 天前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈1 天前
AI 读你的提示词,其实很没耐心
后端
用户8356290780511 天前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123451 天前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒1 天前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记1 天前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598781 天前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端