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
方法启动一个线程, 这时候操作系统的内部会真正的创建一个线程(源码层面是去调用的start0
native修饰的本地方法, 自行调用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不是一个变量, 就意味着这个变量不适合修改, 所以我们编译器就直接不允许这种变量的修改…而对于引用变量的处理逻辑是, 不可以修改引用, 但是可以修改内部的值