深入理解Java线程

文章目录

  • 1.线程基础
    • 1.1进程和线程
      • 1.1.1进程
      • 1.1.2线程
      • 1.1.3进程和线程的区别
      • 1.1.4进程间通信的方式
    • 1.2线程的同步互斥
      • 1.2.1概念
      • 1.2.2线程同步互斥的控制方法
    • 1.3上下文切换
      • 1.3.1概念
      • 1.3.2查看CPU上下文切换情况
      • 1.3.3查看进程 / 线程的上下文切换情况
    • 1.4线程生命周期(操作系统层面)
    • 1.5查看进程线程的方法
      • 1.5.1**windows**
      • 1.5.2**linux**
      • 1.5.3**Java**
    • 1.6Linux系统中线程的实现方式
    • 1.7内核模式和用户模式 (Kernel Model / User Model)
  • 2.Java线程
    • 2.1Java线程的创建方式
      • 2.1.1利用Thread类/继承Thread类
      • 2.1.2Runnable
      • 2.1.3Callable
    • 2.2Java线程实现原理
      • 2.2.1Thread#start()源码分析
      • 2.2.2Java线程属于内核级线程
      • 2.2.3协程
    • 2.3Java线程的调度机制
      • 2.3.1**协同式线程调度**
      • 2.3.2**抢占式线程调度**
    • 2.4Java线程的生命周期
    • 2.5Thread.class常用方法
    • 2.6Java线程的中断机制
    • 2.7Java线程间通信
      • 2.7.1**volatile**
      • 2.7.2**等待唤醒(等待通知)机制**
      • 2.7.3**管道输入输出流**
      • 2.7.4**Thread.join**

1.线程基础

1.1进程和线程

1.1.1进程

程序由指令和数据组成,但指令运行和数据读写需要将指令加载到CPU,数据加载到内存,指令运行过程中还会用到磁盘、网络等设备。

进程就是用来加载指令,管理内存,管理IO的。

进程可以看作是一个程序的实例,大部分程序可以同时运行多个实例,例如记事本,浏览器,有的只能是一个实例,例如360安全卫士等。

因此,当一个程序被运行,从磁盘加载这个程序的代码到内存,实际上就是开启了一个进程。

操作系统以进程为单位来分配系统资源(CPU时间片,内存等资源),也就是说进程是资源分配的最小操作单元。

1.1.2线程

线程是进程中的实体,一个线程必须有一个父进程,一个进程可以有多个线程。

一个线程其实就是一个指令流,将指令流中的指令一条条的按照一定顺序交给CPU去执行。

线程,也被成为“轻量级进程”,它操作系统调度=的最小单元

1.1.3进程和线程的区别

  • 进程之间相互独立,而线程存在于进程之内,是进程的子集
  • 进程是拥有资源的基本单位,如内存空间等,供其内部的线程共享
  • 进程间通信较为复杂:
    • 同一台计算机的进程通信称为IPC(Inter-process communication)
    • 不同计算机之间的进程需要通过网络来进行通信,并且需要遵守相同的网络协议,例如HTTP等
  • 线程通信相对简单,因为它们共享进程内的内存,多个线程可以访问同一共享变量
  • 线程更轻量,线程上下文切换成本一般要比进程上下文切换低

1.1.4进程间通信的方式

  • 管道(pipe)及有名管道(named pipe)

    • 管道可用于具有亲缘关系的父子进程间的通信
    • 有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
  • 信号(signal)

    • 信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。
  • 消息队列(message queue)

    • 消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。
  • 共享内存(shared memory)

    • 可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
  • 信号量(semaphore)

    • 主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。
  • 套接字(socket)

    • 这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

1.2线程的同步互斥

1.2.1概念

  • 线程同步指的是线程之间存在依赖关系,一个线程的执行依赖于另一个线程的消息,当没有收到消息时会处于等待状态,直到消息到达时才会被唤醒

  • 线程互斥是指多个线程同时访问其共享的进程系统资源时的排他性

    任何时刻都只允许一个线程去访问共享资源,其他线程必须等待直至资源占用者释放资源

    因此,线程互斥亦可以理解为是特殊的线程同步。

1.2.2线程同步互斥的控制方法

  • 临界区
    • 通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(在一段时间内只允许一个线程访问的资源就称为临界资源)。
  • 互斥量
    • 为协调共同对一个共享资源的单独访问而设计的。
  • 信号量
    • 为控制一个具有有限数量用户资源而设计。
  • 事件
    • 用来通知线程有一些事件已发生,从而启动后继任务的开始。

1.3上下文切换

1.3.1概念

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0s6ACGDJ-1648567490219)(E:\file\学习\课后总结\并发编程\assert\进程上下文)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kwtx85af-1648567490221)(E:\file\学习\课后总结\并发编程\assert\线程上下文)]

进程是程序的一个执行实例。在Linux中,线程是轻量级进程,可以并行运行,并与父进程共享一个地址空间和其他资源。

上下文是CPU寄存器和程序计数器在任何时间点的内容

寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。

程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。

上下文切换指的是CPU从一个进程(或线程)到另一个进程(或线程)的切换。

深入到内核层面就是 暂停一个进程,并将该进程的上下文存储在内存的某个地方;再从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它,并返回到程序计数器所指示的位置,也就是上一次进程被中断的指令行,从而恢复进程;

上下文切换只能发生在内核模式,内核模式是CPU的特权模式,并提供对所有内存位置以及其他资源的访问。应用程序最初都在用户模式下运行,它们也可以通过系统调用来运行部分内核代码。

上下文切换是多任务操作系统的一个基本特性。在多任务操作系统中,多个进程似乎同时在一个CPU上执行,彼此之间互不干扰。这种并发的错觉是通过快速连续发生的上下文切换(每秒数十次或数百次)来实现的。这些上下文切换发生的原因是进程自愿放弃它们在CPU中的时间,或者是调度器在进程耗尽其CPU时间片时进行切换的结果

上下文切换通常是计算密集型。就CPU时间而言,上下文切换对系统来说是一个巨大的成本,实际上,它可能是操作系统上成本最高的操作。因此,操作系统设计中的一个主要焦点是尽可能地避免不必要的上下文切换。与其他操作系统(包括一些其他类unix系统)相比,Linux的众多优势之一是它的上下文切换和模式切换成本极低。

1.3.2查看CPU上下文切换情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fEGfwUQ5-1648567490222)(E:\file\学习\课后总结\并发编程\assert\vmstat)]

vmstat l

其中cs列就是CPU上下文切换的统计。当然,CPU上下文切换不等价于线程切换,很多操作会造成CPU上下文切换:

  • 线程、进程切换
  • 系统调用
  • 中断

1.3.3查看进程 / 线程的上下文切换情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rL8qDcUT-1648567490223)(E:\file\学习\课后总结\并发编程\assert\pidstat)]

  • 使用pidstat命令
常用的参数:
-u 默认参数,显示各个进程的 CPU 统计信息
-r 显示各个进程的内存使用情况
-d 显示各个进程的 IO 使用
-w 显示各个进程的上下文切换
-p PID 指定 PID

其中cswch表示主动切换,nvcswch表示被动切换。从统计数据中看到,该进程每秒主动切换次数达到将近500次,因此代码中存在大量的 睡眠\唤醒 操作。

  • 从进程的状态信息中查看

通过命令 cat /proc/5598/status 查看进程的状态信息

  • voluntary_ctxt_switches: 40469351
  • nonvoluntary_ctxt_switches: 2268

这2项就是该进程从启动到当前总的上下文切换情况。

1.4线程生命周期(操作系统层面)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2VEIJPd-1648567490224)(E:\file\学习\课后总结\并发编程\assert\OS-Thread-lifeCycle)]

操作系统的线程生命周期主要有:

  • 初始状态
  • 可运行状态
  • 运行状态
  • 休眠状态
  • 终止状态
  1. 初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建
  2. 可运行状态,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
  3. 当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
  4. 运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,当等待的事件出现了,线程就会从休眠状态转换到可运行状态
  5. 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了

这五种状态在不同编程语言里会有简化合并。

  • C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了
  • Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了

1.5查看进程线程的方法

1.5.1windows

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

1.5.2linux

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

1.5.3Java

  • jps 命令查看所有 Java 进程
  • jstack 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

1.6Linux系统中线程的实现方式

  • LinuxThreads linux/glibc包在2.3.2之前只实现了LinuxThreads
  • NPTL(Native POSIX Thread Library)

​ 可以通过以下命令查看系统是使用哪种线程实现

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C0k1MLAQ-1648567490226)(E:\file\学习\课后总结\并发编程\assert\linux-thread-impl)]

1.7内核模式和用户模式 (Kernel Model / User Model)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ug0GyM8L-1648567490227)(E:\file\学习\课后总结\并发编程\assert\CPU保护环.png)]

在内核模式下,执行代码时可以完全不受限制的访问底层硬件,执行任意CPU指令和引用任何内存地址。内核模式通常是操作系统最低级别、最受信任的功能保留,内核模式下的崩溃对于计算机而言是灾难性的。

在用户模式下,执行代码不能直接访问硬件或引用内存,必须委托系统API来访问硬件或内存。正是由于这种隔离提供的保护,才能使用户模式下的崩溃变得可恢复。

CPU保护模式

x86 CPU提供了四个保护环:0,1,2,3。通常情况下只使用0(内核)和3(用户)。

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

  • 系统调用
  • 异常事件(当发生某些预先不可知的异常时,就会切换到内核态,去执行相关的异常事件)
  • 设备中断(在使用外围设备时,如果外围设备完成了用户需求,就会向CPU发送一个中断信号,此时CPU就会去执行中断事件,此时,如果原来处于用户态,自然就要切换到内核态)

Microsoft解释:https://docs.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/user-mode-and-kernel-mode

2.Java线程

2.1Java线程的创建方式

2.1.1利用Thread类/继承Thread类

// 创建线程对象
Thread t = new Thread() {
    public void run() {
    // 要执行的任务
    }
};
// 启动线程

2.1.2Runnable

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

2.1.3Callable

class CallableTask implements Callable {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//提交任务,并用 Future提交返回结果
//线程池源码
static class DefaultThreadFactory implements ThreadFactory {
  private static final AtomicInteger poolNumber = new AtomicInteger(1);
  private final ThreadGroup group;
  private final AtomicInteger threadNumber = new AtomicInteger(1);
  private final String namePrefix;

  DefaultThreadFactory() {
    SecurityManager s = System.getSecurityManager();
    group = (s != null) ? s.getThreadGroup() :
    Thread.currentThread().getThreadGroup();
    namePrefix = "pool-" +
      poolNumber.getAndIncrement() +
      "-thread-";
  }

  public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
                          namePrefix + threadNumber.getAndIncrement(),
                          0);
    if (t.isDaemon())
      t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
      t.setPriority(Thread.NORM_PRIORITY);
    return t;
  }
}

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

2.2Java线程实现原理

2.2.1Thread#start()源码分析

首先我们来思考,为什么Thread执行start方法之后要调用run方法,而不是直接调用run方法?

  • 首先在Thread初始化之前会调用Thread#registerNatives()这个注册方法,用于完成native和真正方法的绑定;

        /* Make sure registerNatives is the first thing  does. */
        private static native void registerNatives();
        static {
            registerNatives();
        }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zrxLlOFJ-1648567490228)(E:\file\学习\课后总结\并发编程\assert\JNINativeMethod)]

  • Thread#start()是Java线程的启动入口,Thread#start()会调用Thread#start0(),这个方法是个native方法;JNINativeMethod中建立了JNI的映射关系,因此调用Thread#start0()实际上调用了JVM中的Thread.c#JVM_StartThread来进行线程的创建;

    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();
    
  • 利用new JavaThread(&thread_entry,sz)进行线程的创建,并根据不同的操作系统平台调用对应的os::create_Thread方法进行线程的创建,create_Thread方法最终调用的是pthread_create,相当于是操作系统进行线程的创建,属于系统调用,相当于是从用户态到内核态的切换;

  • 创建完之后,线程会有一个初始化状态initialized,并调用sync->wait()方法进行阻塞;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WO2LDhX5-1648567490233)(E:\file\学习\课后总结\并发编程\assert\thread#init)]

  • 通过native_thread->prepare(jthread)来进行Java的Thread和JVM的Thread之间一对一的绑定;

  • 通过调用Thread::start(native_thread)来启动线程,此时设置Java线程状态为runnable,接着调用os::start_thread(thread)来根据不同操作系统选择不同的线程启动方式;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmXkBMh8-1648567490237)(E:\file\学习\课后总结\并发编程\assert\thread#run)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PTXvOZgF-1648567490238)(E:\file\学习\课后总结\并发编程\assert\setRun)]

  • 此时线程状态为runnable,唤醒线程,并执行thread.cpp#JavaThread::run()方法;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OKSe01Qp-1648567490242)(E:\file\学习\课后总结\并发编程\assert\wakeup)]

  • 然后通过JavaCall::call的方式去回调new Thread()时重写的run方法;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-62OgHH5N-1648567490243)(E:\file\学习\课后总结\并发编程\assert\innner)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bm8l2ZVj-1648567490244)(E:\file\学习\课后总结\并发编程\assert\javaCall)]

2.2.2Java线程属于内核级线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f6mCJ1RG-1648567490250)(E:\file\学习\课后总结\并发编程\assert\内核级线程)]

内核级线程:它们都是依赖于内核的,也就是说无论是系统进程还是用户进程,其创建、切换、撤销都需要由内核来完成

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

JDK1.2基于操作系统原生线程模型实现Java线程。Sun JDK,其Windows版本和Linux版本都是用一对一的线程模型来实现的,一条Java线程映射到一条轻量级进程。

2.2.3协程

协程(Coroutines),是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

  • 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。

  • 线程的默认stack大小是1M,而协程更轻量,接近1k。因此可以在相同的内存中开启更多的协程。

  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

注意: 协程适用于被阻塞的,且需要大量并发的场景(网络io)。不适合大量计算的场景

java中是否存在协程?

java中协程框架: kilim quasar

2.3Java线程的调度机制

线程调度是指系统为线程分配处理器使用权的过程,主要分为协同式调度和抢占式调度。

2.3.1协同式线程调度

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

2.3.2抢占式线程调度

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

Java线程调度就是抢占式调度

希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统

public class SellTicketDemo implements Runnable {
    private int ticket;

    public SellTicketDemo() {
        this.ticket = 100;
    }

    @Override
    public void run() {
        while (ticket > 0) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":正在执行操作,余票:" + ticket--);
                }
            }
            Thread.yield();
        }
    }

    public static void main(String[] args) {
        SellTicketDemo demo = new SellTicketDemo();

        Thread thread1 = new Thread(demo,"thread1");
        Thread thread2 = new Thread(demo,"thread2");
        Thread thread3 = new Thread(demo,"thread3");
        Thread thread4 = new Thread(demo,"thread4");

        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);
        thread3.setPriority(Thread.MIN_PRIORITY);
        thread4.setPriority(Thread.MIN_PRIORITY);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

2.4Java线程的生命周期

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

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

在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eqbTog9v-1648567490253)(\assert\Java-Thread-lifeCycle)]

public enum State {
  /**
         * Thread state for a thread which has not yet started.
         */
  NEW,

  /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
  RUNNABLE,

  /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
  BLOCKED,

  /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * 
    *
  • {@link Object#wait() Object.wait} with no timeout
  • *
  • {@link #join() Thread.join} with no timeout
  • *
  • {@link LockSupport#park() LockSupport.park}
  • *
* *

A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called Object.wait() * on an object is waiting for another thread to call * Object.notify() or Object.notifyAll() on * that object. A thread that has called Thread.join() * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: *

    *
  • {@link #sleep Thread.sleep}
  • *
  • {@link Object#wait(long) Object.wait} with timeout
  • *
  • {@link #join(long) Thread.join} with timeout
  • *
  • {@link LockSupport#parkNanos LockSupport.parkNanos}
  • *
  • {@link LockSupport#parkUntil LockSupport.parkUntil}
  • *
*/ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }

从JavaThread的角度,JVM定义了一些针对Java Thread对象的状态(jvm.h)

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8NEIPKTK-1648567490254)(\assert\JVM-Thread)]

从OSThread的角度,JVM还定义了一些线程状态给外部使用,比如用jstack输出的线程堆栈信息中线程的状态(osThread.hpp)

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bb89kSrR-1648567490255)(\assert\OS-Thread)]

2.5Thread.class常用方法

  • sleep()

    • 调用 sleep 会让当前线程从 Running 进入TIMED_WAITING状态,不会释放对象锁
    • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,并且会清除中断标志
    • 睡眠结束后的线程未必会立刻得到执行
    • sleep当传入参数为0时,和yield相同
  • yield()

    • yield会释放CPU资源,让当前线程从 Running 进入 Runnable状态,让优先级更高(至少是相同)的线程获得执行机会,不会释放对象锁;
    • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程;
    • 具体的实现依赖于操作系统的任务调度器
  • join()

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

    • stop()方法已经被jdk废弃,原因就是stop()方法太过于暴力,强行把执行到一半的线程终止。

    • stop会释放对象锁,可能会造成数据不一致。

2.6Java线程的中断机制

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

所谓协作式,是通过一个中断标识位来实现的。其它线程如果想要中断线程A,就对线程A的中断标识位做一个标记,线程A自己通过**「轮询」**去检查标识位,然后自己做处理。

API的使用

  • interrupt(): 将线程的中断标志位设置为true,不会停止线程
  • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
  • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
public class ThreadInterruptTest {

    static int i = 0;

    public static void main(String[] args)  {	
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public  void run() {
                while (true) {
                    i++;
                    System.out.println(i);
                    //Thread.interrupted()  清除中断标志位
                    //Thread.currentThread().isInterrupted() 不会清除中断标志位
                    if (Thread.currentThread().isInterrupted() ) {
                        System.out.println("=========");
                    }
                    if(i==10){
                        break;
                    }

                }
            }
        });

        t1.start();
        //不会停止线程t1,只会设置一个中断标志位 flag=true
        t1.interrupt();
    }

利用中断机制优雅的停止线程

while (!Thread.currentThread().isInterrupted() && more work to do) {
    do more work
public class StopThread implements Runnable {
public class StopThread implements Runnable {

    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
        }
        System.out.println("线程停止: stop thread");
    }

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

注意:使用中断机制时一定要注意是否存在中断标志位被清除的情况

sleep 期间能否感受到中断

修改上面的代码,线程执行任务期间有休眠需求

@Override
public void run() {
    int count = 0;
    while (!Thread.currentThread().isInterrupted() && count < 1000) {
        System.out.println("count = " + count++);

        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("线程停止: stop thread");

处于休眠中的线程被中断,线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样就会导致while条件Thread.currentThread().isInterrupted()为false,程序会在不满足count < 1000这个条件时退出。如果不在catch中重新手动添加中断信号,不做任何处理,就会屏蔽中断请求,有可能导致线程无法正确停止。

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

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

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

2.7Java线程间通信

2.7.1volatile

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

public class VolatileDemo {

    private static volatile boolean flag = true;

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (flag){
                        System.out.println("trun on");
                        flag = false;
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (!flag){
                        System.out.println("trun off");
                        flag = true;
                    }
                }
            }
        }).start();
    }

2.7.2等待唤醒(等待通知)机制

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

public class WaitDemo {
    private static Object lock = new Object();
    private static  boolean flag = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    while (flag){
                        try {
                            System.out.println("wait start .......");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    System.out.println("wait end ....... ");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag){
                    synchronized (lock){
                        if (flag){
                            lock.notify();
                            System.out.println("notify .......");
                            flag = false;
                        }

                    }
                }
            }
        }).start();
    }

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

public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();

        System.out.println("唤醒parkThread");
        LockSupport.unpark(parkThread);
    }

    static class ParkThread implements Runnable{

        @Override
        public void run() {
            System.out.println("ParkThread开始执行");
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }

2.7.3管道输入输出流

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

public class Piped {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);

        Thread printThread = new Thread(new Print(in), "PrintThread");

        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
        }
    }

    static class Print implements Runnable {
        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }

2.7.4Thread.join

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

e = 0;
try {
while ((receive = System.in.read()) != -1) {
out.write(receive);
}
} finally {
out.close();
}
}

static class Print implements Runnable {
    private PipedReader in;

    public Print(PipedReader in) {
        this.in = in;
    }

    @Override
    public void run() {
        int receive = 0;
        try {
            while ((receive = in.read()) != -1) {
                System.out.print((char) receive);
            }
        } catch (IOException ex) {
        }
    }
}

### 2.7.4**Thread.join**

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















































你可能感兴趣的:(JUC,java,开发语言,后端)