文章目录
深入学习java线程
线程的状态/生命周期
线程优先级
java中的线程有优先级的概念,1~10,默认优先级是5,在线程构建的时候可以通过setPriority(int)
方法来修改优先级。优先级高的线程获得cup时间片数量多于优先级低的线程
针对频繁阻塞/休眠/IO操作的线程设置较高优先级,对应侧重于CPU计算的线程设置较低的优先级
Java中的线程优先级是通过映射到操作系统的原生线程上实现的,所以线程的调度最终取决于操作系统,操作系统中线程的优先级有时并不能和Java中的一一对应,所以Java优先级并不是特别靠谱。
线程的调度
线程的调度是至操作系统为线程分配cup使用权的过程,线程的调度分为:
-
协同式线程调度(Cooperative Threads-Scheduling)
线程的执行时间由线程自己来控制,一个线程执行完后再通知操作系统去切换执行下一个线程。
好处是实现简单、避免频繁的线程上下文切换
缺点是如果线程出了问题,程序就会一直阻塞
-
抢占式线程调度(Preemptive Threads-Scheduling)
线程的执行时间以及是否切换由操作系统来控制,对于线程来说是不可控的,也就没有了【一个线程导致整个进程阻塞】的问题出现。
java是抢占式线程调度
线程的实现模式
线程是操作系统层面的实体,那么java中的线程是怎么和操作系统的线程对应起来的嘞?
其实对于任何语言实现线程有是三种方式:内核线程实现、用户线程实现、混合实现
内核线程实现
内核线程就是直接由操作系统内核支持的线程, 这种线程由内核来完成线程切换, 内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
每一个语言层面线程都直接映射一个内核线程,是1:1的对应关系
局限性:首先,由于是基于内核线程实现的,所以各种线程操作,如创建、 析构及同步,都需要进行系统调用。而系统调用的代价相对较高, 需要在用户态和内核态中来回切换。其次,每个语言层面的线程都需要有一个内核线程的支持,因此要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持的线程数量是有限的。
用户线程实现
严格意义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。
用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。 如果程序实现得当, 这种线程不需要切换到内核态, 因此操作可以是非常快速且低消耗的, 也能够支持规模更大的线程数量。
用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援
所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难。
混合实现
线程除了依赖内核线程实现和完全由用户程序自己实现之外, 还有一种将内核线程与用户线程一起使用的实现方式, 被称为N:M实现。 在这种混合实现下, 既存在用户线程, 也存在内核线程。
用户线程还是完全建立在用户空间中, 因此用户线程的创建、 切换、 析构等操作依然廉价, 并且可以支持大规模的用户线程并发。
同样又可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过内核线程来完成。
Java语言对线程的实现
Java线程在早期的Classic虚拟机上(JDK 1.2以前),是用户线程实现的, 但从JDK 1.3起, 主流商用Java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用内核线程模型。
以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构, 所以HotSpot自己是不会去干涉线程调度的,全权交给底下的操作系统去处理。
所以,这就是我们说Java线程调度是抢占式调度的原因。因为线程的执行由操作系统负责
协程
JDK19推出虚拟线程的概念,但是还不能直接应用在生产环境,如果一定要是使用协程来解决项目中的问题可以考虑使用Quasar
协程出现的原因
目前微服务架构下,要求各个微服务处理请求的耗时非常短,并且各个微服务能够同时处理更多数量的请求。
内核线程实现方式它的缺陷就是切换、调度成本高,系统能容纳的线程数量也有限。在微服务架构下,各个微服务对一次的请求响应耗时变得很短,那么就可能会出现业务线程切换的开销可能会接近与计算本身的开销。
对于java来说,用户线程实现重新引入成为了解决上述问题一个非常可行的方案
协程简介
内核线程切换开销是来自于保护和恢复线程成本(上下文切换)。如果采用用户线程实现,这些工作也不能省,但是把保护、恢复和调度的工作从操作系统转移到了程序员手上,则有很多手段来减少这些开销
用户线程实现又称为协程,这是因为最初多数用户线程的实现都采用的协同式线程调度,所以就被称之为协程了。完整的做调用栈的保护、恢复工作,所以目前也称为有栈协程
协程的主要优势是轻量, 无论是有栈协程还是无栈协程, 都要比传统内核线程要轻量得多。默认64位操作系统 一个内核线程栈占1MB,而协程的线程栈几百字节到几KB
协程的劣势:需要应用层面实现的内容特别多,比如线程调度等等。同时协程基本上是采用协同式线程调度,所以协同式线程调度的缺陷它也存在
总的来说,协程适用于被阻塞,且大量并发的场景,不适合大量计算的场景。因为协程提高更高的吞吐量,而不是更低的延迟
纤程-Java中的协程
在JVM的实现上,以HotSpot为例,协程的实现会有些额外的限制,Java调用栈跟本地调用栈是做在一起的。 如果在协程中调用了本地方法, 还能否正常切换协程而不影响整个线程? 另外, 如果协程中遇传统的线程同步措施会怎样? 譬如Kotlin提供的协程实现, 一旦遭遇synchronize关键字, 那挂起来的仍将是整个线程。
所以Java开发组就Java中协程的实现也做了很多努力,OpenJDK在2018年创建了Loom项目,这是Java的官方解决方案, 并用了“纤程(Fiber)”这个名字。
Loom项目背后的意图是重新提供对用户线程的支持, 但这些新功能不是为了取代当前基于操作系统的线程实现, 而是会有两个并发编程模型在Java虚拟机中并存, 可以在程序中同时使用。 新模型有意地保持了与目前线程模型相似的API设计, 它们甚至可以拥有一个共同的基类, 这样现有的代码就不需要为了使用纤程而进行过多改动, 甚至不需要知道背后采用了哪个并发编程模型。
根据Loom团队在2018年公布的他们对Jetty基于纤程改造后的测试结果, 同样在5000QPS的压力下, 以容量为400的线程池的传统模式和每个请求配以一个纤程的新并发处理模式进行对比, 前者的请求响应延迟在10000至20000毫秒之间, 而后者的延迟普遍在200毫秒以下,
目前Java中比较出名的协程库是Quasar[ˈkweɪzɑː®](Loom项目的Leader就是Quasar的作者Ron Pressler), Quasar的实现原理是字节码注入,在字节码层面对当前被调用函数中的所有局部变量进行保存和恢复。这种不依赖Java虚拟机的现场保护虽然能够工作,但影响性能。
Quasar实战
引入maven依赖
<!-- https://mvnrepository.com/artifact/co.paralleluniverse/quasar-core -->
<dependency>
<groupId>co.paralleluniverse</groupId>
<artifactId>quasar-core</artifactId>
<version>0.7.9</version>
</dependency>
在执行Quasar的代码前,还需要配置VM参数(Quasar的实现原理是字节码注入,所以,在运行应用前,需要配置好quasar-core的java agent地址)
-javaagent:D:\softwareDev\maven\myMaven\repository\co\paralleluniverse\quasar-core\0.7.9\quasar-core-0.7.9.jar
在具体的业务场景上,我们模拟调用某个远程的服务,假设远程服务处理耗时需要1S,使用休眠1S来代替。为了比较,用多线程和协程分别调用这个服务10000次,来看看两者所需的耗时。
下面两段代码主要区别就是一个是使用Fiber
类开启一个线程,另一个是使用线程池
Quasar的:
package cn.tulingxueyuan;
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.strands.Strand;
import org.springframework.util.StopWatch;
import java.util.concurrent.CountDownLatch;
import java.util.stream.IntStream;
public class FiberExample {
public static void main(String[] args) throws Exception{
CountDownLatch count = new CountDownLatch(10000);
// StopWatch 一个秒表 计时工具类
StopWatch stopWatch = new StopWatch();
stopWatch.start();
IntStream.range(0,10000).forEach(i-> new Fiber() {
@Override
protected String run() throws SuspendExecution, InterruptedException {
//Quasar中Thread和Fiber都被称为Strand,Fiber不能调用Thread.sleep休眠
Strand.sleep(1000);
count.countDown();
return "aa";
}
}.start());
count.await();
stopWatch.stop();
System.out.println("结束了: " + stopWatch.prettyPrint());
}
}
线程的:
package cn.tulingxueyuan;
import org.springframework.util.StopWatch;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class Standard {
public static void main(String[] args) throws Exception{
CountDownLatch count = new CountDownLatch(10000);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 随便创建一个线程池,这里就用了一个无限的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
IntStream.range(0,10000).forEach(i-> executorService.submit(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException ex) {
}
count.countDown();
}));
count.await();
stopWatch.stop();
System.out.println("结束了: " + stopWatch.prettyPrint());
executorService.shutdownNow();
}
}
其中的Fiber
就是Quasar为我们提供的协程相关的类,可以类比为Java中的Thread类。
StopWatch
是Spring的一个工具类,一个简单的秒表工具,可以计时指定代码段的运行时间以及汇总这个运行时间。
看看执行的结果:使用Quasar方式总共才耗时1.6s
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调 度以及支持性工作。
当java虚拟机中如果没有非守护线程执行的时候,java虚拟机就直接退出了,不管守护线程释放正在工作中都直接退出。
线程间的通信、协调、协作
多个线程一起工作,协作完成某项工作,这就离不开线程间的通信和协调、协作。
管道输入输出流
我们已经知道,进程间有好几种通信机制,其中包括了管道,其实Java的线程里也有类似的管道机制,用于线程之间的数据传输,而传输的媒介为内存。
Java中的管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
join()方法
面试题:现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2执行完后执行?
答:用 Thread#join 方法即可,在 T3 中调用 T2.join,在 T2 中调用 T1.join。
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B剩下的代码。
Synchronized内置锁
多个线程对一个共享资源做更新操作,那么就产生并发安全问题。那么就可以在更新共享资源的方法上或者是同步代码块中加上synchronized关键字保证同一时间只有一个线程去进行更新操作。
对象锁和类锁的区别
对象锁就是指锁的是一个实例对象,或者是synchronized关键字作用于成员方法上。而类锁是指synchronized关键字作用于类的静态方法上,锁对象是类的class对象。
咱们在使用synchronized的时候就需要注意锁对象需要使用同一个,否则加锁是毫无用处的。
volatile关键字
- 保证内存可见性
- 禁止指令重排
volatile它不能保证多个线程对共享资源更新操作的并发安全,它不是锁机制。它是一个轻量级是线程通信机制。
适用场景:一个线程写,多个线程读
volatile是轻量级的,synchronized是重量级的,synchronized它也是也保证了内存可见性。
public class VolatileCase {
private volatile static boolean ready;
private static int number;
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
// //无限循环
while(!ready){
//System.out.println("lll");
};
// 上面只给ready变量加了volatile关键字,但是这里number也是输出main线程修改后的值
// 原因是:volatile它不是针对某一个变量刷新的,它是插入了一个内存屏障,而内存屏障会把这个线程之前发生了改变的变量一起刷新。
System.out.println("number = "+number);
}
}
public static void main(String[] args) throws InterruptedException {
new PrintThread().start();
TimeUnit.SECONDS.sleep(1);
number = 51;
ready = true;
TimeUnit.SECONDS.sleep(3);
System.out.println("main is ended!");
}
}
等待/通知机制
适用场景是:线程A做的事情做到了一半,发现某些条件不满足了,需要等线程B执行相应代码让线程A的条件满足,这个时候线程A条件满足了,然后再继续往下执行。
Object
对象中的wait() notify() notifyAll()
方法
尽量不用notify()
,因为 notify()
只会唤醒一个线程,我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程
等待和通知的标准范式
等待方
- 获取对象锁
- 条件判断,如果不满足则调用
wait()
方法阻塞,被通知后仍要检查条件 - 条件满足则执行对应逻辑
synchronized(对象){
where(条件不满足){
对象.wait();
}
对应业务逻辑代码;
}
通知方
- 获得对象锁
- 改变条件
- 通知
synchronized(对象){
执行业务逻辑,改变条件,使条件满足;
对象.notifyAll();
}
注意:wait() notify() notifyAll()
方法必须用在同步方法或同步代码块中。否则会报异常。
进入wait()
方法后当前线程就会释放锁,等其他线程调用notifyAll()
方法,并执行退出synchronized代码段后,等待方就会去竞争锁,然后继续从wait()
方法位置往下执行
synchronized的锁对象和调用wait()和notifyAll()
的对象尽量使用同一个。
等待超时模式实现一个连接池
每一次往连接池中放连接对象时调用notifyAll()
方法唤醒其他正在等待连接对象的线程
获取连接的时候我们也要支持没有超时时间,无限等待的情况
如果有等待超时时间,我们首先算出具体超时的时间戳,等待方法中传最大超时时间wait(time)
,每一次被唤醒后都更新一次time时间。
public class DBPool {
/*容器,存放连接*/
private static LinkedList<Connection> pool = new LinkedList<Connection>