深入理解java线程,并分析部分源码

目录

线程基础

线程和进程

进程

线程

进程和线程的区别

进程间通信方式

线程的同步互斥

上下文切换

内核模式和用户模式

CPU保护环

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

Java线程详解

Java线程的实现方式

Thread

Runnable

Callable

lambda

线程创建和启动的流程

Java线程的实现原理

thread.start()源码分析

Java线程 → 内核线程

协程

Java线程调度机制

协同式调度

抢占式调度

Java线程调度

Java线程生命周期

thread常用方法

sleep

yield

join

stop

Java线程中断机制

api

sleep感受中断

Java线程间通信

volatile

等待唤醒机制

管道输入输出流


线程基础

在开始研究java线程之前,我们先回想一下线程相关的知识

线程和进程

再分清楚进程和线程

进程

程序由指令+数据组成,但是这些指令要运行,数据要进行读写,必须将指令加载到CPU,数据加载到内存,指令的运行过程中,还需要用到磁盘、网络等等的设备,进程,即使用来加载指令,管理内存、管理IO的

当一个程序被运行,从磁盘加载这个程序的代码到内存,这个时候,就开启了一个进程,比如,你电脑上打开运行了一个杀毒软件,这就开启了一个进程

深入理解java线程,并分析部分源码_第1张图片

看到没,这些玩意,就是一个个应用进程

进程,可以当做是程序的一个实例,大部分程序可以同时运行多个实例进程的,比如,浏览器,记事本,but,有的就只能启动一个,比如,音乐APP

操作系统会以进程为单位,分配系统的资源,所谓资源,就是CPU时间片啊,内存啊这些的,进程是资源分配的最小单位

线程

别迷糊啊,线程和进程不是一个东西,线程,是进程中的实体,一个进程可以有多个线程,但,一个线程必须有一个父进程,也就是说,一个线程一定有它归属的进程

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

线程,也会被成为轻量级进程,是操作系统调度的最小单位,也就是说,是CPU调度执行的最小单位

别混,别混,你看,就像这玩意,就是一个个线程

进程和线程的区别

ok,单独解释了进程和线程,我们来直截了当的对比区别吧

  • 进程基本上都是互相独立的,而线程存在于进程内,是进程在子集
  • 进程拥有共享的资源,比如内存空间,供内部的线程共享
  • 进程间通信比较复杂
    • 同一台计算机的进程通信成为 IPC
    • 不同计算机的进程通信,需要通过网络,并遵守相同协议,比如 http
  • 线程通信相对简单,毕竟他们共享进程的内存,多个线程可以访问同一个共享变量
  • 线程更加轻量,线程上下文的切换成本,一般比进程上下文切换成本低

进程间通信方式

进程间有一些通信方式

  • 管道及有名管道:管道可以用于具有亲缘关系的父子进程间通信,有名管道除了具有管道所具有的的功能外,它还允许无亲缘关系进程间的通信,比如,java 访问 redis
  • 信号:信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某件事发生,一个进程收到一个信号,与处理器收到一个中断请求效果上可以说是一样的
  • 消息队列:消息队列是消息的链接表,它克服了前面两种通信方式中信号量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新信息,对消息队列有读权限的进程,可以从消息队列中读取信息,比如,常用的 MQ
  • 共享内存:这可以说是最有用的进程间通信方式,它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存的数据的更新,这种方式需要依靠某种同步操作,比如互斥锁和信号量等 比如,分布式锁
  • 信号量:主要作为进程之间及同一种进程的不同线程之前的同步和互斥手段
  • 套接字:这是一种更为一般的进程间通信机制,可以用于网络中不同机器之间的进程间通信,应用很广泛 socket

补充一下信号量和PV操作

在操作系统中,进程间经常会存在互斥(都需要共享独占性资源的时候)和同步(完成异步的两个进程的写作)两种关系,为了有效的处理这两种情况,就有了信号量和PV操作

信号量:是一种特殊的变量,表现形式是一个整型S和一个队列

P操作:S = S - 1,若 S < 0,表示当前没有资源分配给该进程,进程暂停执行,进入等待队列

V操作:S = S + 1,若 S ≤ 0,表示阻塞队列中有等待该资源的进程,唤醒等待队列中的第一个进程

信号量与PV操作是用来解决并发问题的,信号量的初值就是表示资源的可用数,而且通常对于初始为0的信号量,会先做V操作

在资源使用之前,会先进行P操作,资源使用完成后,进行V操作

在互斥关系中,PV操作是在一个进程中成对出现的,而在同步关系中,PV操作一定是在两个进程甚至多个进程中成对出现的

互斥控制,就是为了保护共享资源,不让多个进程同时访问这个共享资源

线程的同步互斥

线程同步,指的是线程间具有的一种制约关系,一个线程的执行依赖于另一个线程的消息,当另一个线程的消息没到达时,它应该等待,直到另一个线程的消息到达  比如,一个http请求到Tomcat,再到Java程序

线程互斥,是指对于个共享的进程系统资源,在各单个线程访问时的排他性,当有多个线程需要访问同一个共享资源时,任意时刻只能有一个线程去使用,其他线程必须等待,直到占用资源的线程释放资源,线程的互斥,可以看成一种比较特殊的线程同步关系   比如,synchronized以及同步锁

有一些线程同步互斥的控制方法

-- 临界区:通过对多线程串行化来访问公共资源或者某一段代码,速度快,适合用来做控制数据访问
    (在一段时间内只允许一个线程访问的资源就被称为临界资源)

-- 互斥量:为了协调对一个共享资源的单独访问所设计

-- 信号量:为了控制一个具有有限数量用户资源而设计

-- 事件:用来通知线程有一些事情已经发生,从而可以启动后续任务

上下文切换

上下文切换,就是指CPU从一个进程或者线程,到另一个进程或者线程的切换

上下文切换可以更详细一点的描述为内核对CPU上的进程/线程执行以下活动:

  • 暂停一个进程的处理,并将该进程的CPU状态,也就是上下文,存储在内存中的某个地方
  • 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
  • 返回到程序计数器指定的位置以恢复运行,所谓程序计数器指定的位置,其实就是返回到进程中被终端执行的代码行

所谓内核,其实就是指操作系统的核心

这种分时操作,可以提高CPU的利用率

补充:上下文是CPU寄存器和程序计数器在任何时间点的内容;寄存器是CPU内部的一小部分非常快的内存,当然,这是相对于CPU外部的RAM主内存来说,它通过提供对常用值的快速访问来加快计算机程序的执行;程序计数器是一种专门的寄存器,它指示CPU在其指令序列中的位置,并保存着正在执行的指令的地址或者下一条要执行的指令的地址,这取决于具体的操作系统

我们既然提到了上下文切换,那就得注意这么几个地方

  1. 上下文切换,只能在内核模式下发生,内核模式是CPU的特权模式,其中只有内核运行,并提供对所有内存位置以及所有其他系统资源的访问,其他程序,包括应用程序,最初在用户模式下运行,但它们可以通过系统调用来运行部分内核代码
  2. 上下文切换,是多任务操作系统的一个基本特性,在多任务操作系统中,多个进程看起来似乎同时在一个CPU上执行,彼此之间互不干扰,这个并发的错觉就是通过快速连续发生的上下文切换来实现的,速度很快,可能每秒数十次或数百次,这些上下文切换发生的原因可能是进程自愿放弃他们在CPU的时间,也可能是调度器在进程耗尽了其CPU时间片时进行切换的结果
  3. 上下文切换通常是计算密集型的,就CPU时间而言,上下文切换对系统来说是一个巨大的成本,实际上,它可能是操作系统成本最高的操作,因此,操作系统设计中的一个主要焦点就是---尽可能地避免不必要的上下文切换,与其他操作系统相比,包括一些类Unix系统,Linux的众多优势之一有一点,是它的上下文切换和模式切换成本极低

我们可以通过命令 vmstat 1 来查看CPU每秒的上下文切换统计,其中,cs 列就是CPU的上下文切换统计, 注意! 注意!上下文切换不等价于线程切换,很多操作都会造成CPU上下文切换,比如线程/进程切换、系统调用、中断

深入理解java线程,并分析部分源码_第2张图片

(不清楚其他参数什么意思的话,直接搜一下这个命令吧~  我不是运维,这些我也接触不到,都是用什么找什么)

我们可以用命令查看某个线程/进程的切换情况

比如,我要查看 PID 59657 的这个进程,每秒的切换情况

pidstat  -w -p 59657 1

深入理解java线程,并分析部分源码_第3张图片

cswch表示主动切换,nvcswch表示被动切换

如果没有这个命令,也可以用cat命令去查看,以刚才这个为例子,我就是

 cat /proc/5598/status

深入理解java线程,并分析部分源码_第4张图片

看我选中的这两项,就是进程从启动到现在的总的上下文切换情况

上面的,代表主动切换,下面的,代表被动切换,仔细看,下面的前面多了个non

其实查看上下文还是有用的,比如一个Java程序,进行了大量的主动切换,那可能,这个程序存在大量的休眠或者释放操作

内核模式和用户模式

在现代操作系统中,CPU实际上都在两种截然不同的模式中花费时间,分别是内核模式和用户模式

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

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

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

  • 系统调用
  • 异常事件,当发生某些预先不可知的异常时,会切换到内核态以只想相关的异常事件
  • 设备中断,在使用外围设备时,如外围设备完成了用户请求,就会向CPU发送一个中断信号,此时,CPU就会暂停执行原本的下一条指令,转去处理中断事件,此时,如果原来在用户态,则自然会切为内核态

这里补充一个,CAS,compare and swap ,它涉及的是原子指令,CPU可以直接执行,不涉及内核态和用户态的切换

CPU保护环

提到了内核模式和用户模式,还有一个东西可以了解接触一下,分级保护域,可以叫做保护环,也叫作环形保护,也叫作CPU环,简称Rings,这是一种用来在发生故障时保护数据和功能,提升容错度,避免恶意操作,提升计算机安全的设计方式

工作在不同Ring中的对象对资源有不同的访问级别,Rings是从最高特权级到最低特权级排列,通常是数字越小级别越高

在大多数操作系统中,Ring0 具有最高特权,并且可以和最多的硬件直接交互,比如CPU,比如内存,同时内层Ring可以随便使用外层Ring的资源

深入理解java线程,并分析部分源码_第5张图片

Ring0-2 为管理员层级,可以做大部分事情,但是Ring1-2 不能使用特权指令

Ring3 代表用户模式

在 X86架构下,CPU提供了四个保护环,通常,只是用0环-内核,以及3环-用户

事实上,Rings的概念最早出现于x86保护模式的设计中

Ring 的设计将用户程序和服务程序对资源的利用进行隔离,正确使用 Ring 可以提升资源使用的安全性,比如,某个病毒程序作为一个 Ring3 运行的用户程序,它尝试在不通知用户的情况下打开硬件摄像头,就应该被阻止掉,因为访问硬件,需要 Ring1 甚至 Ring 0

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

操作系统层面的线程生命周期为五种:初始状态、可运行状态、运行状态、休眠状态、终止状态

深入理解java线程,并分析部分源码_第6张图片

 初始状态:指的是线程已经被创建,但是还不允许分配CPU执行,这个状态属于编程语言特有的,注意,这个所谓的被创建,指的是在编程语言层面被创建,并非在操作系统中已经创建,在操作系统层面,真正的线程还没有创建

可运行状态:指的是线程可以分配CPU执行,在这种状态下,真正的操作系统线程已经被成功撞见了,所以可以分配CPU执行

运行状态:当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就会变为运行状态

休眠状态:运行状态的线程如果调用一个阻塞 API 或者等待某个事件,那么线程的状态就会变为休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没机会获得CPU使用权,当等待的事件出现了,线程就会从休眠状态转换到可运行状态

终止状态:线程执行完或者出现异常就会进入到终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态,也就意味着线程对生命周期结束了

这五种状态并非在编程语言里都有,在某些编程语言里,可能会进行状态的简化合并,比如在C语言,就把初始状态和可运行状态合并了;在Java里,把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,但jvm层面并不关心这两个状态,因为jvm把线程调度交给操作系统处理了        

Java线程详解

ok,现在我们来看Java线程相关的知识

Java线程的实现方式

首先,就是Java中线程的实现方式,这里不过多介绍,简单说一下就行

Thread

直接使用 Thread 类即可,或者继承 Thread 类

Runnable

实现 Runnable 接口,实现后,再配合 Thread 即可使用

Callable

实现 Callable 接口,与 Runnable 不同,Callable 接口有返回值

lambda

直接new Thread 去运行

线程创建和启动的流程

其实本质上只有一种,那就是通过 new Thread() 来创建线程

1、使用 new Thread() 创建一个线程,然后调用 .start() 方法进行 Java 层面的线程启动

2、调用本地方法 start0() ,去调用JVM中的JVM_StartThread方法进行线程的创建和启动

深入理解java线程,并分析部分源码_第7张图片

3、调用 new JavaThread(&thread_entry, sz) 进行线程的创建,并根据不同的操作系统平台,调用对应的OS::create_thread方法进行线程创建

4、新创建的线程状态为 initialized,调用了sync → wait() 的方法进行等待,等到被唤醒才继续执行 thread → run()

5、调用 Thread::start(native_thread)方法进行线程启动,此时将线程状态设置为 RUNNABLE,接着调用OS::start_thread(thread),根据不同的操作系统选择不同的线程启动方式

6、线程启动之后,状态设置为RUNNABLE,并唤醒第四步中等待的线程,接着执行thread → run()的方法

7、JavaThread::run()方法会回调第一步new Thread()中复写的run()方法

Java thread → JVM thread → OS thread

会发生用户态和内核态切换,系统调用,所以说在Java中,创建一个线程是重量级操作

Java线程的实现原理

谈到实现原理,我们不得不对一个问题的答案有认知,那就是为什么调用.start()方法,而不是.run()方法

thread.start()源码分析

    public synchronized void start() {
 
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

       
        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 */
            }
        }
    }

可以看到,上来先判断了线程状态,初始的时候为0,如果不是0,那就直接抛异常

后面最重要的,其实就是调用了 start0()这个方法

深入理解java线程,并分析部分源码_第8张图片

哦吼,本地方法,没招了,撸源码吧,看JVM吧

深入理解java线程,并分析部分源码_第9张图片

OK,可以看到,调用了 JVM_StartThread,这个东西在哪儿呢?就在这儿

src/java.base/share/native/libjava/Thread.c

可能你会好奇,为什么是这儿,其实是因为 Thread 类你一创建,就会执行一个地方

深入理解java线程,并分析部分源码_第10张图片

看,static修饰,这也是个本地方法 registerNatives,这个方法会调用注册方法,完成相关方法和JVM方法的映射绑定,也就是上面那一大堆玩意

在 JVM_StartThread 中,有一个地方, 在创建 Thread ,位置在这儿

src/hotspot/share/prims/jvm.cpp

深入理解java线程,并分析部分源码_第11张图片

而这个 JavaThread,也有对应的实现

src/hotspot/share/runtime/thread.cpp

深入理解java线程,并分析部分源码_第12张图片

 可以看到,这块开始调用操作系统去创建Java线程对应的内核线程,真正去创建一个线程

既然调用了os的,那我们试试看,有没有对应的实现,搜索一下,发现有一些

深入理解java线程,并分析部分源码_第13张图片

我们就看看Linux的吧

深入理解java线程,并分析部分源码_第14张图片

可以看到,这里面就是Linux的实现(恕我菜看不懂,而且主要整理Java,C和C++就不搞太多了)

在这儿,线程创建完了,但是,别忽略一个问题,我们从Java线程到JVM线程到OS线程,最后落在了OS线程,但是OS线程不是我们的Java线程,所以需要进行绑定,并进行相应的状态变更(这儿我真看不懂除了Java以外的语言,就不乱分析了,有兴趣有能力的可以看看JDK源码)

Java线程 → 内核线程

从上面我们就可以看到,Java线程属于内核级别的线程,基于操作系统的原生线程模型来实现,一个Java线程就映射到一个轻量级进程之中

补充一下内核级线程和用户级线程

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

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

协程

我们提到了用户级线程和内核级线程,就会涉及到一个玩意,协程

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

给大家画一幅图

深入理解java线程,并分析部分源码_第15张图片

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

  1. 线程的切换由操作系统调度,协程由用户自己调度,因此减少了上下文切换,提高了效率
  2. 线程的默认stack是1M,而协程更加轻量,接近1K,因此可以在相同到内存中开启更多协程
  3. 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就可以了,所以执行效率比多线程高很多

但是,协程适用于被阻塞的,且需要大量并发的场景,比如网络IO,不适合大量计算的场景

Java线程调度机制

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

协同式调度

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

抢占式调度

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定,线程执行时间系统可控,不会有一个线程导致整个进程阻塞

Java线程调度

Java的线程调度,属于抢占式调度,Thread.yield()可以让出执行时间,但无法获取执行时间

如果希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成,Java一共有10个线程优先级,Thread.MIN_PRIORITY至Thread.MAX_PRIORITY,在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行

但是,优先级不是一定靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程的调度最终还是得取决于操作系统

Java线程生命周期

OK,上面说了Java线程的东西,我们来说Java线程的最后一点,生命周期

Java线程有六种状态

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

我们也可以从 Thread 类看到

深入理解java线程,并分析部分源码_第16张图片

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

上图

深入理解java线程,并分析部分源码_第17张图片

thread常用方法

Thread类的常用方法,大家一定都用过,我们不讲怎么用,只补充一些其他相关的信息

sleep

调用sleep会让当前线程从Running进入到TIMED_WAITING状态,但,不会释放锁对象

其他线程可以使用 interrupt 方法打断正在休眠的线程,这时 sleep 方法会抛出中断异常 InterruptedException,并会清除中断标志

睡眠结束后,线程未必会立刻得到执行

sleep当传入的参数为0时,效果与yield相同

yield

释放CPU资源,让当前线程从Running进入Runnable状态,让优先级等于或高于自己的线程获得执行机会,但,不会释放锁对象

假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为,没有比它优先级更高的线程了

具体的实现依赖于操作系统的任务调度器

join

这个没什么补充的,其实就是等待调用join的线程结束之后,程序再继续执行,适合需要等待异步执行结果的场景

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

呐,还有一件事,join的实现是基于等待通知机制的

stop

已经被JDK废弃,太过暴力,强行把执行到一半的线程终止,会释放锁对象

Java线程中断机制

Java中,没有提供一种安全的,可以直接停止某个线程的方法,而是提供了中断机制,它是一种协作机制,也就是说,不能直接中断某个线程,而是由被中断的线程自己处理,被中断的线程有完全的自主权,可以选择任何时候停止,也可以选择,不停止

api

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

注意这三种的区别

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

sleep感受中断

我们已经说过好几次,sleep中断会清除标志

我们来做个测试,加深一下印象

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
          int count = 0;
          while (!Thread.currentThread().isInterrupted() && count <= 10000) {
              System.out.println("count = " + count++);
              try {
                  Thread.sleep(500);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }

            System.out.println("thread is end......");
        });
        thread.start();
        Thread.sleep(500);
        thread.interrupt();

        System.out.println("main is running........");
    }

深入理解java线程,并分析部分源码_第18张图片

可以看到,它在sleep的时候,的确感受到了,但是,它把标志位又清除了,又继续了

我们在catch中给它再改回去试试

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
          int count = 0;
          while (!Thread.currentThread().isInterrupted() && count <= 10000) {
              System.out.println("count = " + count++);
              try {
                  Thread.sleep(500);
              } catch (InterruptedException e) {
                  e.printStackTrace();
                  Thread.currentThread().interrupt();
              }
          }

            System.out.println("thread is end......");
        });
        thread.start();
        Thread.sleep(500);
        thread.interrupt();

        System.out.println("main is running........");
    }

深入理解java线程,并分析部分源码_第19张图片

可以看到,结束了,所以说,如果用了中断机制,一定要记得改掉

另外,不止sleep,wait方法也是一样的,也会清除中断标志位

Java线程间通信

我们采用多线程是为了提高效率,不可避免的,可能会需要进行线程间通信

volatile

我们之前看过volatile一点内容,其中,可见性,其实就是让线程间进行通信

等待唤醒机制

等待唤醒机制,或者说,等待通知机制,也是线程间通信的方式

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

在JDK中,我们可以使用LockSupport来实现阻塞和唤醒,线程调用park,等待‘许可’的发放,调用unpark,给指定线程发放‘许可’,它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但,连续多次唤醒和一次唤醒效果是一样的

我们先来看阻塞一次唤醒一次的效果

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {

            System.out.println("thread is park......  " + System.currentTimeMillis());
            LockSupport.park();

            System.out.println("thread is unpark......" + System.currentTimeMillis());

        });
        thread.start();

        Thread.sleep(5000);
        LockSupport.unpark(thread);

    }

深入理解java线程,并分析部分源码_第20张图片

我们再来看阻塞多次,唤醒多次的效果

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {

            System.out.println("thread is park......  " + System.currentTimeMillis());
            LockSupport.park();

            System.out.println("thread is unpark1......" + System.currentTimeMillis());
            LockSupport.park();

            System.out.println("thread is unpark2......" + System.currentTimeMillis());

        });
        thread.start();

        
        LockSupport.unpark(thread);

        Thread.sleep(1000);
        LockSupport.unpark(thread);

    }

深入理解java线程,并分析部分源码_第21张图片

注意,我们提到了,park和unpark没有顺序问题,本质就是发放许可,我们可以先发放许可

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("thread is park......  " + System.currentTimeMillis());
            LockSupport.park();

            System.out.println("thread is unpark......" + System.currentTimeMillis());

        });
        thread.start();


        LockSupport.unpark(thread);



    }

深入理解java线程,并分析部分源码_第22张图片

wait\notify机制,是moniter提供的,依赖于synchronized,也就是说,wait方法必须在synchronized加锁内部使用,notify没有绑定参数,也就是说,不一定唤醒的是哪个线程,所以一般用notifyAll,再加判断,防止虚假唤醒

注意哦,wait方法会释放锁,它要不释放锁,别的线程怎么获取锁,别的线程不获取锁,这个线程永远没机会被唤醒了

别混,千万别混,sleep不会释放锁,它只是让出了CPU而已

管道输入输出流

提到输入输出流,大部分人第一印象应该都是文件输入输出流或者网络的输入输出流,当然我自己也是这大部分人中的一个

管道输入输出流,和文件、网络的输入输出流不一样的地方在于,它主要用于线程间的数据传输,传输的媒介,就是内存

管道的输入输出流,包含四种实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种字节,后两种字符

    public static void main(String[] args) throws Exception {

        PipedReader in = new PipedReader();
        PipedWriter out = new PipedWriter();
        out.connect(in);

        Thread thread = new Thread(new RunnableTask(in));
        thread.start();

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

    }

    static class RunnableTask implements Runnable {

        private PipedReader in;

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

        @SneakyThrows
        @Override
        public void run() {
            int tag = 0;
            while ((tag = in.read()) != -1) {
                System.out.println((char) tag);
            }
        }
    }

好了,本次的分享总结到此为止,嘛,祝各位开心~

你可能感兴趣的:(并发编程,java日常分享,java,并发编程,多线程,源码分析)