大家好!我是未来村村长,就是那个“请你跟我这样做,我就跟你这样做!”的村长👨?🌾!
||Coding Again 3||
? 未来村村长正推出一系列【Coding Again】文章,对之前学过的和没学过的知识重新进行整理,因为现在再回顾之前的文章,写得很乱,大概是因为自己当时也没有搞太明白相关的知识点,再出发即是一次对过去的知识扬弃的过程。该系列文章以java后端学习路线为轴进行推出,如果喜欢就一键三连吧!
一、Java多线程基础
1、相关概念
(1)程序、进程、线程
- 程序:特殊的静态文本(代码)
- 进程:动态概念,程序运行时产生进程
- 线程:进程的更细一级划分
(2)多线程优势
- 更合理利用系统计算资源,特别是多核CPU资源
- 提高系统的吞吐量和执行效率
(3)线程的类别
- 守护线程:不处理用户的业务逻辑,用来守护用户线程的运行,通过Thread.setDaemon(true)方法来显式地设定。如GC垃圾回收线程就是守护线程。启动守护线程时,要将setDaemon(true)方法写在start方法前,不然会抛出异常。
- 用户线程:为了满足一定业务需求而编写的线程。
2、线程创建方式
(1)创建方式
① 继承Thread类
- 自定义线程类继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象
- 调用start()方法启动线程
public class Threadtest implements Runnable{
@Override
public void run() {
for (int i1 = 0; i1 < 2000; i1++) {
System.out.println("===="+i1);
}
}
}
public static void main(String[] args) {
Threadtest threadtest = new Threadtest();
new Thread(threadtest).start();
}
}
② 实现Runnable接口
public class Threadtest implements Runnable {
@Override
public void run() {
int i = 0;
while(i<=101) {
System.out.println(Thread.currentThread().getName() + "跑了》》》" + i++ + "米");
if(i==100){
System.out.println("冠军 是"+Thread.currentThread().getName());
System.exit(0);
}
if(Thread.currentThread().getName() == "兔兔" && i%10==0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
Threadtest threadtest = new Threadtest();
new Thread(threadtest,"兔兔").start();
new Thread(threadtest,"龟龟").start();
}
③ 实现Callable接口
- 创建一个线程类,通过implements关键字实现Callable接口
- 重写call()方法
- 在main方法中实例化该线程类的对象
- 新建和实例化FutureTask类,将上一步实例化的线程类对象传入FutureTask构造函数
- 使用Thread()构造方法,传入上一步实例化的线程类(FutureTask)对象
- 调用start()方法启动线程
(2)三种方法对比
① Thread与Runnable
? java只允许使用单继承,所以可以通过实现Runnable接口来实现线程的创建,这样就可以继承其它类。Thread的底层源码,也是通过实现了Runnable接口,在此基础上完成了线程的相关特性和操作。
② 三者的使用场景
- Thread:简单快速创建逻辑不复杂的线程类,适用于需求固定、任务单一、逻辑简单的线程。
- Runnable:解决单继承的约束,实现代码的复用,实现该接口的类还能继续被继承。
- Callable+FutureTask:能够返回一个带有指定类型的返回值的线程逻辑处理办法,线程执行完成时将返回值返回至FutureTask对象,该对象会一直等待返回值的返回,指导超时或用户取消。
二、线程生命周期
1、线程的状态
? 在Thread源码中使用enum枚举了Thread的六种状态:
- NEW(新建):线程创建,未调用start方法启动【new】
- RUNNABLE(可运行):线程处于可执行状态,已经在Java虚拟机执行,但还在等其它操作系统资源【start()】
- BLOCKED(阻塞):线程处于阻塞,线程在等待一个监控锁,特别是多线程下的场景等待另一个线程同步块的释放。【synchronized】
- WAITING(等待):线程处于等待状态,指的是该线程正在等待另一个线程执行某些特定的操作【wait()、join()】
- TIMED_WAITING(调校时间的等待):与时间相关的等待,调用了设定等待时长参数的方法,如sleep(500)或wait(500)【sleep(xx)、wait(xx)】
- TERMINATED(终止):线程执行完毕的状态
? 我们可以通过getState()方法获取线程的执行状态,或者通过isAlice()方法判断一个线程是否还存活。
2、线程的生命周期
(1)新建到可运行
? ① 一个线程类被new关键字实例化时,该线程就进入NEW(新建状态),调用start()进入RUNNABLE状态。
? ② RUNNABLE包含两种状态,一是READY就绪(当调用start()方法式,线程需要获取到CPU资源才能进入运行状态,即RUNNING),二是RUNNING运行中。
? ③ 一个正处于RUNNING的线程,若内部调用了yield()方法,则会主动让步将CPU资源让出来,从而从新处于READY状态。
(2)可运行到阻塞
? ① 在多线程中,为了保证数据操作的正确性,需要设置同步来保护数据,可以用synchronized关键字对一段代码设置同步块或设置同步方法,这样的同步块或同步方法就会加入一个🔒,只有一个线程完成操作后,其它线程才能再次对该同步块或同步方法进行操作。
? ② 当一个线程中,设置了同步块或同步方法(一般在一个线程类的方法或代码块中加synchronized),而其它线程进入了同步块或同步方法时,就会发生线程的阻塞。
(3)等待和恢复
? ① 代码中出现了wait()或join()方法时,线程会进入WAITING状态。
? ② 代码中出现了wait(xxx)、join(xxx)、wait(xxx)方法时,线程会进入TIMED_WAITING。
? ③ 通过notify或notifyAll()方法,会重新唤醒线程。
(4)线程终止和关闭
? ① 内部原因:当线程处理完所有业务逻辑后,会自动进入TERMINATED状态,该线程生命周期结束。
? ② 外部原因:人工调用了关闭线程的方法,如interrupt()。当线程一旦到了TERMINATED状态,该线程所有方法都应该停止调用。
3、线程的优先级
? 在java中,可以使用setPriotity()设置线程的优先级。
三、线程调度方式
1、调度概述
- context:上下文,在框架中经常出现,我们可以理解为该环境、内容、容器、信息是贯穿某事物生命周期各个方面的。
- 每个线程都有自己的context,我们称为线程的上下文环境。当其中一个线程,由RUNNABLE状态转为BLOCKED、WAITING、TIMED_WAITING状态时,就会发生线程间的上下文切换。
2、相关方式【睡眠、等待、让步、唤醒、插队】
(1)sleep()睡眠
? 让一个运行中的线程睡眠或休息一段时间,然后继续执行,进入READY状态。
(2)wait()等待
- RUNNABLE-TIMED_WAITING或WAITING
? 与sleep()类似,能让一个运行中的线程睡眠或休息一段时间。一般情况,wait()需要使用notify()或notifyAll()方法来重新唤醒。使用notify()方法,在多条线程处于WAITING状态下时,只是随机将其中一个处于WAITING状态或TIMED_WAITING状态的线程唤醒。若wait(xx)方法传入了毫秒数,则会在时间结束后自动唤醒。
? 与sleep()区别?:
- wait()并非线程自带的方法,是对象实例的成员方法之一,sleep()是线程自带的方法。
- wait()方法被调用时,该对象会释放监控🔒,而sleep()方法被调用后不会释放监控🔒。
(3)yield()让步
(4)notify()唤醒
(5)notifyAll()唤醒
(6)join()插队
public class Threadtest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + "号不要脸插队");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new Threadtest());
thread.start();
for (int i = 0; i < 100; i++) {
if(i==50){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(i + "号同学排队");
}
}
}
3、线程安全与线程不安全
线程安全:线程运行的结果可以预测,无论运行多少次,它的值总是能够确定。对于单线程,一般认为是线程安全的。
线程不安全:多线程的并发导致的结果不确定性,就是线程不安全。线程不安全问题存在与实例变量,方法中的私有变量不存在非线程安全问题。
四、线程组与线程池
1、线程组
? 一组线程或线程组的集合,在多线程情况下,对线程进行分组管理。直接在main方法中运行的线程或线程组,都属于main线程组,在main方法中运行的代码上一级为System线程组,其中线程的上一级为main线程组。
① 创建
ThreadGroup threadGroup01 = new ThreadGroup()
② 使用
Thread thread01 = new Thread(threadGroup01,new ThreadImplentsRunnable(),"thread-01");
Thread thread02 = new Thread(threadGroup01,new ThreadImplentsRunnable(),"thread-02")
③ 线程组的枚举
Thread[] threadList = new Thread[10];
threadGroup.enumerate(threadList);
④ main线程组的获取
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup()
2、线程池
(1)线程池相关概念
线程池:线程容器,提前创建一部分线程待用,该容器中的线程提前进行了初始化,减少了重复创建线程的等待时间。
线程池有以下优点:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
其原理如下图:【来源:水印】
线程池的状态:【来源:水印】
-
RUNNING:接受新任务并处理排队的任务。 -
SHUTDOWN:不接受新任务,但处理排队的任务。 -
STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务。 -
TIDYING:所有任务都已终止,workerCount 为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。 -
TERMINATED:terminated() 已完成。
(1)线程池的核心类ThreadPoolExecutor类
① ThreadPoolExecutor构造方法的参数
- int corePoolSize:核心线程池的大小,线程池初始化后的基本核心线程数。
- int maximumPoolSize:线程池的最大允许线程池数,线程池承载峰值。
- long keepAliveTime:线程池中线程的允许空闲时间,超过这个时间可能被终止
- TimeUnit unit:枚举类中的时间常量
- BlockingQueue<Runnable> workQueue:排队的策略,用于线程阻塞时存储等待执行的任务
- ThreadFactory threadFactory:用于创建线程池中线程的工厂类,内部通过addworker()方法新增线程
- RejectedExecutionHandler handler:拒绝的策略
② 线程池类中的重要方法
? addWorker():线程池若没有达到线程初始化时要求的核心线程数,则会调用addWorker()方法,将该任务作为新开启的线程,放入线程池充当其中一个工作线程,在次判断线程池有没有达到线程初始化时要求的核心线程数直到线程池饱和,若没有则继续创建新线程。
? execute()和submit():
- 都是向线程池提交一个任务,在submit方法中仍然是调用的execute方法进行任务的执行或进入等待队列或拒绝。
submit方法比execute方法多的只是将提交的任务(不管是runnable类型还是callable类型)包装成RunnableFuture然后传递给execute方法执行。 - submit方法和execute方法最大的不同点在于submit方法可以获取到任务返回值或任务异常信息,execute方法不能获取任务返回值和异常信息。
- RunnableFuture从名字就可以知道,他既是一个Runnable又是一个Future,所以说submit方法提交的任务被包装成RunnableFuture后,后面执行任务的时候运行的就是RunnableFuture.run()方法
(2)线程池的创建与使用
① 创建
ThreadPoolExecutor threadPool01 = new ThreadPoolExecutor(3,5,500,TimeUnit.MILLSECONDS,new ArrayBlockingQueue<Runnable>(5));
WorkRunnable wrok = new Work();
threadPool01.execute(work);
② 使用
Java多线程包提供了一些线程池创建类
- newCachedThreadPool:带缓存功能的线程池,若任务不多,线程池会自动缩小线程数量,可灵活回收空闲线程
- newFixedThreadPool:能设置最大工作线程数的线程池,一开始每当提交一个任务时,都会创建一个新的工作线程运行任务,同时该线程仍然作为工作线程在线程池中待命,一段时间没有任务,工作线程数量不会减少,会一直等待新的任务
- newScheduleThreadExecutor:设置最大线程数的线程池,它的主要任务是定时调度任务
- newSingleThreadExecutor:只有一个线程的线程池,能够保证所有任务按照指定顺序执行
五、多线程并发处理
1、多线程并发基础
? 多线程并发的情况下,要保证各个线程的安全,需要满足三大多线程的并发特性,即原子性(Atomic),内存可见性,避免指令重排序
- 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 原子性底层实现核心思想是:CAS,但是CAS中存在ABA问题。(CAS,compare and swap的缩写,判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。ABA,如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。)
- 内存可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- 避免指令重排序:即程序执行的顺序按照代码的先后顺序执行,因为JVM会根据Java代码的内部指令进行重新排序后执行,称为指令重排序。
2、多线程的同步
? 同步在多线程中更多的是等待的意思,同步可以理解为有共同的需要而按一步步顺序执行。如同接力棒比赛一般,要一位运动员跑完其自身的路程时,下一位运动员接棒后才能继续完成自己的路程。
(1)synchronized关键字
我们通常使用synchronized关键字来给一段代码或一个方法上锁,它通常有以下三种形式。
① 修饰实例方法
? 如果对一个类的普通方法加同步关键字synchronized,则这个方法的同步监控锁属于该类的实例化后的对象,同步监控锁的是每一个对象自身。即一个实例化了多个对象实例,就会产生多个同步监控锁,对应每一个对象实例。
public synchronized void instanceLock() {
}
public void blockLock() {
synchronized (this) {
}
}
② 修饰类方法(静态方法)
? 如果一个类的静态方法加同步关键字synchronized,则这个方法的同步监控锁属于这个类的,同步监控的是整个类,并非实例化的对象。即一个类无论实例化多少个对象,它们都用同一把同步监控锁。
public static synchronized void classLock() {
}
public void blockLock() {
synchronized (this.getClass()) {
}
}
③ 修饰代码块,锁为创建的Object对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
}
}
(2)锁的类型
① 乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
-
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现就是悲观锁。 -
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。
② 同步锁机制 —— 偏向锁/轻量级锁/重量级锁
-
偏向锁:指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。 -
轻量级锁:指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 -
重量级锁:指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。synchronized关键字所使用到的同步监控锁是重量级锁。
③ 死锁和活锁
死锁:两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候,死锁会让你的程序挂起无法完成任务。
死锁产生的四个条件:
- 互斥条件:进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 坏路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
死锁的解决办法,防止四个条件产生:
- 破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。
- 破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
- 破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
- 破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
活锁:同样会发生在多个相互协作的线程间,当他们为了彼此间的响应而相互礼让,使得没有一个线程能够继续前进,那么就发生了活锁。同死锁一样,发生活锁的线程无法继续执行。
解决:锁让出的时候添加随机睡眠时间
(3)volatile关键字
? volatile关键字能做到在多线程情况下,对一个多线程彼此关心的变量进行内存可见性的强调,当其中一个线程对该变量进行修改时,其它线程马上能从内存刷新获得该变量的最新值,该关键字还能避免指令重排序。
volatitle xxxObject xxx = 0;
? 该关键字无法满足多线程并发特性的原子性,所以不能替代synchronized使用。
(4)异步
在我们的业务中很可能会碰到需要执行一段时间的任务,并且如果同步的话就会造成一些无谓的等待。因此可以使用异步调用的方法,不阻塞当前其他任务的执行。
参考:
https://blog.csdn.net/v123411739/article/details/106609583?
《Java多线程与大数据处理实战》
|