Java多线程知识图谱
串行、并发与并行的概念
由图片上可以较好的理解串行、并发和并行概念。
并行其实就是一种更为严格、理想的并发,即并行可以被看作并发的一个特列。并发往往是带有部分串行的并发,并发的极致就是并行。
从软件的角度来说,并发就是在一段时间内以交替的方式去完成多个任务,而并行就是以齐头并进的方式去完成多个任务。
从硬件的角度来说,在一个处理器一次只能够运行一个线程的情况下,由于处理器可以使用时间片分配的技术来实现在同一段时间内运行多个线程,因此一个处理器就可以实现并发而并行则需要靠多个处理器在同一时刻各自运行一个线程来实现。
多线程编程的实质就是将任务的处理方式由串行改为并发,即实现并发化,以发挥并发的优势。
线程安全性
在线程安全性的定义中,最核心的概念就是正确性。
正确性的含义是,某个类的行为与其规范完全一致。
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
有序性
有序性问题的原因是因为程序在执行时,可能会进行指令的重排,重排后的指令与原指令的顺序未必一致。
指令重排可以提高CPU处理性能。
重排序可以分为指令重排序和存储子系统重排序。
重排序类型 | 重排序表现 | 重排序来源(主体) |
---|---|---|
指令重排序 | 程序顺序与源代码顺序不一致 | 编译器 |
指令重排序 | 执行顺序与程序顺序不一致 | JIT编译器、处理器 |
存储子系统重排序 | 源代码顺序、程序顺序和执行顺序这三者保持一致,但是感知顺序与执行顺序不一致 | 高速缓存、写缓冲器 |
重排序的原因:
- 在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会把变量保存在寄存器中而不是内存中;
- 处理器可以采用乱序或并行等方式来执行指令;
- 缓存可能会改变将写入变量提交到主内存的次序;
- 保存在处理器本地缓存中的值,对于其他处理器是不可见的。
最小保证:即允许不同的处理器在任意时刻从同一个存储位置看到不同的值。
同步将限制编译器、运行时和硬件对内存操作重排序方式,从而在实施重排序时不会破坏JMM提供的可见性保证。
Java内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序中所有的操作定义了一个偏序关系,称之为Happens-Before。如果两个操作之间缺乏
Happens-Before关系,那么JVM可以对它们任意地重排序。
Happens-Before规则:
- 程序顺序规则。如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
- 监视器锁规则。在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
- volatile规则。对volatile变量的写入操作必须在对该变量的读操作之前执行。
- 线程启动规则。在线程上对Thread.start( )的调用必须在该线程中执行任何操作之前执行。
- 线程结束规则。线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
- 中断规则。当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行。
- 终结器(finalize( ))规则。对象的构造函数必须在启动该对象的终结器(finalize( ))之前执行完成。
- 传递性。如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。
Happens-Before排序是在内存访问级别上操作的,它是一种“并发级汇编语言”,而安全发布的运行级别更接近程序设计。
初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构造过程完成后可能改变的值,必须采用同步来确定可见性。
线程的生命周期状态
博文链接link
上下文切换
上下文切换在某种程度上可以被看作多个线程共享同一个处理器的产物,它是多线程编程中的一个重要概念。
首先了解时间片的概念:时间片决定了一个线程可以连续占用处理器运行的时间长度。
当进程中的一个线程由于其时间片用完或者其自身的原因(比如,它需要稍后再继续运行)*或者主动暂停其运行时,另外一个线程(可能是同一个进程或者其他进程中的一个线程)可以被操作系统(线程调度器)选中占用处理器开始或者继续其运行。这种一个线程被暂停,即被剥夺处理器的使用权,另外一个线程被选中开始或者继续运行的过程就叫做线程上下文切换。
一个线程被剥夺处理器的使用权而被暂停运行就被称为切出;一个线程被操作系统选中占用处理器开始或者继续其运行就被称为切入。
我们看着是连续运行的线程,实际上是以断断续续运行的方式使其任务进展的。这种方式意味着在切出和切入的时候操作系统需要保存和恢复相应线程的进度信息,即切入和切出的时候操作系统需要保存和恢复相应线程的进度信息,即切入和切出那一刻相应线程所执行的任务进行到什么程度了(如计算的中间结果以及执行到了哪条指令)。这个进度信息就被称为上下文(Context)。它一般包括通用寄存器(General Purpose Register)的内容和程序计数器(Program Counter)的内容。在切出时,操作系统需要将上下文保存到内存中,以便被切出的线程稍后占用处理器继续其运行时能够在此基础上进展。在切入时,操作系统需要从内存中加载(恢复)被选中线程的上下文,以在之前运行的基础上继续进展。
从Java应用的角度来看,一个线程的生命周期状态在RUNNABLE状态与非RUNNABLE状态(包括BLOCKED、WAITING和TIMED_WAITING中的任意一个子状态)之间切换的过程就是一个上下文切换的过程。当一个线程的生命周期状态由RUNNABLE转换为非RUNNABLE时,我们称这个线程被暂停。线程的暂停就是相应线程被切出的过程,这里操作系统会保存相应的上下文,以便该线程稍后再次进入RUNNABLE状态时能够在之前执行进度的基础上继续。而一个线程的生命周期状态由非RUNNABLE状态进入RUNNABLE状态时,我们就称这个线程被唤醒(Wakeup)。一个线程被唤醒仅代表该线程获得了一个继续运行的机会,而不代表其立刻可以占用处理器运行。因此,当被唤醒的线程被操作系统选中占用处理器继续其运行的时候,操作系统会恢复之前为该线程保存的上下文,以变在此基础上继续。
上下文切换分类:自发性上下文切换和非自发性上下文切换。
引起自发性上下文切换
- Thread.sleep( )
- Object.wait( )
- Thread.yield( )
- Thread.join( )
- LockSupport.park( )
引起非自发性上下文切换
- 被切出线程的时间片用完
- 有一个比被切出线程优先级更高的线程需要被运行
- Java虚拟机的垃圾回收(当Stop-the-world时)
上下文切换的直接开销
- 操作系统保存和恢复上下文所需的开销,这主要是处理器时间开销。
- 线程调度器进行线程调度的开销。
上下文切换的间接开销
- 处理器高速缓存重新加载的开销。一个切出的线程可能稍后在另一个处理器上被切入继续运行。所以需要加载数据。
- 上下文切换也可能导致整个一级高速缓存中的内容被冲刷,即一级高速缓存中的内容会被写入下一级高速缓存(如二级高速缓存)或者主内存(RAM)中。
显式锁
博文链接link
线程间协作
博文链接link
线程池
博文链接link