深入理解Java线程

1. 线程基础知识

1.1 线程和进程

  • 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的 基本单位。
  • 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个 线程共享进程的资源

1.2 上下文切换(Context switch)

上下文切换是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。

  • 上下文切换只能在内核模式下发生。

内核模式是CPU的特权模式,其中只有内核运行,并提供对 所有内存位置和所有其他系统资源的访问。其他程序(包括应用程序)最初在用户模式下运行,但 它们可以通过系统调用运行部分内核代码。

  • 上下文切换是多任务操作系统的一个基本特性。
内核模式(Kernel Mode)vs 用户模式(User Mode)

Kernel Mode(内核模式)

在内核模式下,执行代码可以完全且不受限制地访问底层硬件。它可以执行任何CPU指令和引用任何内存地址。内核模式通常为操作系统的最低级别、最受信任的功能保留。内核模式下的崩溃是灾难性的;他们会让整个电脑瘫痪。
User Mode(用户模式)

在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api来访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可恢复的。在您的计算机上运行的大多数代码将在用户模式下执行。

应用程序一般会在以下几种情况下切换到内核模式:

1. 系统调用。

2. 异常事件。当发生某些预先不可知的异常时,就会切换到内核态,以执行相关的异常事件。

3. 设备中断。在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件。此时,如果原来在用户态,则自然就会切换到内核态。

1.3 操作系统层面线程生命周期

这五态分别是:**初始状态、可运行状态、运行状态、休眠状态和终止状态 **

2. Java线程详解

2.1 Java线程的实现方式

方式1:使用 Thread类或继承Thread类

java 复制代码
//创建线程
Thread thread = new Thread() {
    @Override
    public void run() {
        //执行的任务
    }
};
//运行线程
thread.start();

方式2:实现 Runnable 接口配合Thread

java 复制代码
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        //执行的任务
    }
};
Thread thread1 = new Thread(runnable);
thread1.start();

方式3:使用有返回值的 Callable

java 复制代码
class CallableTash implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}
//创建线程池
ExecutorService execut = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
Future<Integer> result = execut.submit(new CallableTash());

方式4:使用 lambda

java 复制代码
Thread thread2 = new Thread(() -> {/**执行的任务*/}, "thread");
//运行线程
thread2.start();

本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启 动线程最终都会调用Thread#run方法

2.2 Java线程的实现原理

线程创建和启动的流程

  1. 使用new Thread0创建一个线程,然后调用start0方法进行java层面的线程启动
  2. 调用本地方法start0(),去调用jvm中的JVM_StartThread方法进行线程创建和启动;
  3. 调用new JavaThread(&thread entry, sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create thread方法进行线程创建
  4. 新创建的线程状态为lnitialized,调用了sync->wait0的方法进行等待,等到被唤醒才继续执行thread->run0;:
  5. 调用Thread::starti(native thread),方法进行线程信动,此时将线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操系统选择不同的线程启动方式,
  6. 线程启动之后状态设置为RUNNABLE, 并唤醒第4步中等待的线程,接着执行thread->run0的方法
  7. JavaThread::run0方法会回调第1步new Thread中复写的run0方法

Java线程执行为什么不能直接调用run()方法,而要调用start()方法?
直接调用run()方法仅仅简单的调用对象的方法,而调用start()方法会调用本地方法栈,从而让操作系统新建新线程,最终让新线程执行run方法。

2.3 Java线程属于内核级线程

  • 内核级线程(Kernel Level Thread ,KLT):

它们是依赖于内核的,即无论是用户进程中的线 程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。

  • 用户级线程(User Level Thread,ULT):

操作系统内核不知道应用线程的存在

2.4 Java线程的调度机制

  • 协同式线程调度

线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另 外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里。

  • 抢占式线程调度

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中, Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有 一个线程导致整个进程阻塞。

2.5 Java线程的生命周期

Java 语言中线程共有六种状态,分别是:

  1. NEW(初始化状态)
  2. RUNNABLE(可运行状态+运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

2.6 Thread常用方法

sleep方法
  • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
  • 睡眠结束后的线程未必会立刻得到执行
  • sleep当传入参数为0时,和yield相
yield方法
  • yield会释放CPU资源 ,让当前线程从 Running 进入 Runnable状态,让优先级更高 (至少是相同)的线程获得执行机会,不会释放对象锁
  • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比 它优先级更高的线程;
  • 具体的实现依赖于操作系统的任务调度器
join方法

等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之 后才能继续运行的场景。

2.7 Java线程的中断机制

断机制是一 种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理。被 中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选 择压根不停止

API的使用

  • interrupt(): 将线程的中断标志位设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志 位,重置为fasle
java 复制代码
while (!Thread.currentThread().isInterrupted() && more work to do) {
    do more work
}

例如:

java 复制代码
class StopThread implements Runnable{

    @Override
    public void run() {
        for (int i =0 ; 
             Thread.currentThread().isInterrupted() && i<1000;
             i++){
            System.out.println(i);
        }
    }
}

public static void main(String[] args) {
    Thread thread3 = new Thread(new StopThread());
    thread3.start();
    Thread.sleep(5);
    thread3.interrupt();
}

注意:
sleep可以被中断 抛出中断异常:sleep interrupted, 清除中断标志位
wait可以被中断 抛出中断异常:InterruptedException, 清除中断标志位

上述情况需要重新 线程中断状态为true

java 复制代码
try {
 	Thread.sleep(1);
} catch (InterruptedException e) {
	e.printStackTrace();
    //重新设置线程中断状态为true
    Thread.currentThread().interrupt();
}

2.8 Java线程间通信

volatile

volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程 之间进行通信。

等待唤醒(等待通知)机制
  • wait和notify

等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法, 线程将进入等待队列进行等待直到被唤醒。

  • LockSupport park/unpark

LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待"许可",调用 unpark则为指定线程提供"许可"。使用它可以在任何场合使线程阻塞,可以指定任何线程进行 唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样 的。

java 复制代码
class ParkThread implements Runnable{
    @Override
    public void run() {
        System.out.println("程序正在执行");
        System.out.println("程序将要进入等待.....");
        LockSupport.park();
    }
}

// main...
Thread parkThread = new Thread(new ParkThread());
parkThread.start();

System.out.println("唤醒parkThread");
LockSupport.unpark(parkThread);
管道输入输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程 之间的数据传输,而传输的媒介为内存。管道输入/输出流主要包括了如下4种具体实现: PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节, 而后两种面向字符。

Thread.join

join可以理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等 待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但 是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串 行的,最后join的实现其实是基于等待通知机制的

相关推荐
Abladol-aj31 分钟前
并发和并行的基础知识
java·linux·windows
清水白石00831 分钟前
从一个“支付状态不一致“的bug,看大型分布式系统的“隐藏杀机“
java·数据库·bug
JunLan~5 小时前
Rocky Linux 系统安装/部署 Docker
linux·docker·容器
吾日三省吾码6 小时前
JVM 性能调优
java
方竞6 小时前
Linux空口抓包方法
linux·空口抓包
弗拉唐7 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
海岛日记7 小时前
centos一键卸载docker脚本
linux·docker·centos
oi777 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
AttackingLin8 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python