多线程的知识梳理
线程的创建方式
-
Lambda 最常用(推荐)
Thread thread = new Thread(() -> { System.out.println("hello thread"); }); thread.start();同时,在变量捕获中,lambda / 匿名内部类的捕获的变量中,必须是 final / 事实final 变量

但是在 C++ / Python / JS 这种语言的lambda变量捕获的时候就没有这种限制,Java的变量捕获是将变量进行“拷贝”一份,如果拷贝了导致前后的值不一致,可能就会出现bug或者安全问题,所以Java禁止这样的行为
-
继承Thread,使用匿名内部类——创建Thread的子类的实例,面向对象,重写了run
Thread thread = new Thread() { @Override public void run() { System.out.println("hello thread"); } }; thread.start(); -
创建子类,继承Thread,重写run方法
class myThread extends Thread { @Override public void run() { System.out.println("hello thread"); } } public static void main(String[] args) { Demo5 demo5 = new Demo5(); demo5.run(); } -
实现Runnable接口,重写run方法
class myThread implements Runnable{ @Override public void run() { System.out.println("hello run"); } } public static void main(String[] args) { myThread thread = new myThread(); thread.run(); } -
实现Runnable接口,使用匿名内部类
Thread thread = new Thread(new Runnable { @Override public void run() { System.out.println("Ciallo thread"); } }); thread.start();
另外Java规定,一个线程只能start一次,一个start对应一个线程
Thread.start()内部调用start0()
private native void start0();
一般的 Java写的代码一般先编译成.class文件,再通过JVM解释运行
native代表底层是由==C++==写的,在JVM内部通过C++实现的方法,调用系统的原生API在系统内部创建线程,系统内部通过PCB来描述线程,通过链表来组织线程的调度,系统通过PCB结构体来对线程进行调度执行。当需要调度的时候就添加到链表上
每个PCB有独自的ID,每一个ID代表着一个线程

Thread的几个常见方法

-
属性方法
ID——getId()
名称——getName()
状态——getState()
优先级——getPriority()
是否为后台线程——isDaemon()
是否存活——isAlive()
是否被中断——isInterrupted()
查看Thread.State的枚举类型中,规定了线程的以下状态
线程状态
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:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called {@code Object.wait()}
* on an object is waiting for another thread to call
* {@code Object.notify()} or {@code Object.notifyAll()} on
* that object. A thread that has called {@code 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:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
-
NEW
线程还未开始的状态
-
RUNNABLE
可运行的线程状态,正在JVM中运行的或者正在等待其他来自系统的资源
-
BLOCKED
阻塞状态,因为等待监视锁而被阻塞的状态
-
WAITING
等待线程的状态,等待被唤醒或者等到其他线程结束
- wait()<—> notify()/ notifyAll()
- join()
-
TIMED_WAITING
限时等待状态,超过特定的时间就会继续执行下去
-
TERMINATED
终止状态,线程结束
-
线程正常运行结束
-
线程异常终止,如果线程出现异常可能会带动其他的线程一起崩
-
终止线程 Interrupt
在Java中,终止线程不意味着“强行终止”,而是结束这个线程的入口方法
Java不提供强制终止线程的方法,这种做法并不安全
-
线程安全问题
当某个逻辑一个线程运行的时候通常不会出现问题,但出现多个线程执行可能就会出现问题了。
多个线程同时运行,共占同一份资源,当对某个变量同时被两个或多个线程修改,可能就会出现冲突,有bug
如以下例子
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0;i < 5000;i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0;i < 5000;i++) {
count++;
}
});
t1.start();
t2.start();
System.out.println();
System.out.println("count = " + count);


发现每一次的执行结果都不一样,没有得到正确的数字,这是为什么?
- 就把刚才的
count++操作来说,CPU的指令一般有三条,分别是load、add、save三部分,完成一次count++操作,如果t1线程与t2线程的count++操作没有被打扰,则可以正常进行,但如果在load、add、save的任意一部分出现“插队”的情况呢?


刚才的线程安全问题,是跟系统的线程调度有关的~~,但我们无法干预系统的调度,它是随机的,所以故引出了
原因
-
[ 根本原因 ] 系统的随机调度
-
多个线程同时对同一个变量进行修改
注意区分修改 <=> 读取
- 一个线程针对一个变量修改 🆗
- 多个线程针对不同变量修改 🆗
- 多个线程针对同一个变量**读取** 🆗
-
修改操作不是原子的
引入了锁的概念
-
本质还是由编译器优化导致的bug
-
本质还是由编译器优化导致的bug
wait & notify / notifyAll
wait 就是与notify搭配的,而且要在synchronized内使用(locker.wait() <=> locker.notify() & notifyAl )
由于线程的随即调度的特性,所以wait与notify最大的特点是
- 控制各个线程的逻辑执行顺序:T1线程执行逻辑1后,需要等待T2线程执行逻辑2,再继续下面的操作

可以理解为wait是让线程打了个小瞌睡😪💤
注意唤醒的问题


✅ 总结
- 必须用
while,保证唤醒后再次判断条件,防止虚假唤醒和错误唤醒。 -
if只能防一次,while是防到位
notify是唤醒随机一个等待的线程,而notifyAll是唤醒所有的
wait与sleep的区别
-
wait需要在锁里面操作,而sleep不需要
-
wait一定要搭配notify唤醒,sleep过了规定的时间就会继续往下执行下去,而wait需要等待通知
wait的设计初衷就是被notify,超时时间只是“后手”
-
wait与sleep一样都能起到阻塞效果
sleep是按照一定的时间阻塞
wait更偏向于执行逻辑的阻塞
-
wait会暂时地释放锁,然后再获取锁,而sleep放在锁内部,执行睡眠时不会释放锁
-
–另外sleep(0)并不是阻塞等待0ms,而是触发一次线程调度,主动让出一次线程,而具体的调度要看CPU
但interrupt又不同于sleep与wait,它是可能终止程序的~~
Thread类中run与start的区别
-
run方法是线程的执行入口,且Thread类本身
继承实现了Runnable抽象接口,故创建线程时候需要重写run方法 -
start方法是线程启用的入口,一个线程对应一个start且只能start一次,start执行的时候会自动调用run方法
-
创建线程后如果调用线程的run方法而不是start方法,并没有真正地创建线程,只是在main线程中执行了run中的重写的代码
-
start方法会真正地创建线程并实现重写的run方法
更加完善…
-
从调度上看
start方法由JVM调度,使得新线程处于就绪状态,并执行run方法
run只是普通方法,不会触发线程调度,完全由当前线程顺序执行

-
可重复调度上
start方法只能被调用一次,一个线程有且仅有一次start,调用多次会抛出异常
run方法可以被当前线程重复调用
-
线程状态
start方法使得线程处于new-runnable-waiting等一系列状态,具体要看CPU的调度
run方法只是一个主线程调用的方法,不会有状态变化
单例模式
Java实例化需要消耗资源,本质上有些逻辑与场景只需要存在一个实例,如果随意创建多个就会导致资源开销的浪费、状态混乱甚至功能出错
饿汉模式
class mySingleton {
private volatile static mySingletonLazy instance = new mySingletonLazy();
public mySingletonLazy getInstance() {
return instance;
}
private mySingletonLazy() {
}
}
🎗️懒汉模式
这是普通的懒汉模式
class mySingletonLazy {
private static mySingleton instance;
private mySingleton() {
}
public mySingleton getInstance() {
if (instance == null) {
instance = new mySingleton();
}
return instance;
}
}
这是加锁优化后的懒汉模式
class mySingletonLazy {
private volatile static mySingleton instance;
private mySingleton() {
}
public mySingleton getInstance() {
//减少加锁开销
if (instance == null) {
synchronized (this) {
//保证实例化过程原子性
if (instance == null) {
instance = new mySingleton();
}
}
}
return instance;
}
}
通常来说,我们都认为懒汉的行为是褒义,饿汉是贬义——因为饿汉开始就消耗资源了,不管使不使用,而懒汉是想要就创建,不想要就暂时搁置。
但对于线程安全的角度来说,懒汉的单例创建模式还是好的吗?
饿汉 vs 懒汉 线程安全
先下结论,饿汉是线程安全的,而懒汉是线程不安全的,但懒汉的不安全只出现在实例化之前,
-
饿汉模式是天然的线程安全的,它的方法都是实例的读操作,当多个线程对同一变量进行读操作的时候是没有问题的
-
懒汉模式的修改操作,原理上是原子的,但由于JVM优化的指令重排序问题,可能会使得懒汉模式的创建实例化的时候会出现线程安全问题
-
来看普通的懒汉模式是如何触发线程安全问题的( ̄︶ ̄)↗

虽然在t1第二次创建实例后,t2的instance指向空,后续也会被GC回收,实际上已经消耗了两次实例化的资源,但如果这个对象的构造数据很大呢?那就消耗了非常多的时间
-
可以选择加锁解决这个问题
synchronized (this) { if (instance == null) { instance = new mySingleton(); } return instance; }
进一步分析~~
可以发现只要一进入方法就会加锁,而这个代码只有在实例化之前会出现线程安全问题,实例化之后就没有这个问题了
此处即使把instance实例化成功之后还是频繁的上锁解锁 => 阻塞 => 效率问题
可以给加锁增加判定的条件,减少加锁的开销
//判定是否要加锁 if (instance == null) { synchronized (this) { if (instance == null) { instance = new mySingleton(); } } } return instance;
-
但这样就真的没问题了吗?这其实还有指令重排序引起的问题
这行代码涉及的指令比较多,new mySingleton()实例化的代码分为了三个阶段
- 分配内存空间
- 针对空间进行初始化
- 分配内存首地址,赋值到引用变量中
但这个逻辑在指令重排序可能是1 => 3 => 2,这种执行顺序就会出现问题了,还是这个代码

所以此处应该禁止指令重排序,关掉编译器优化,还是用到volatile修饰~~
此处标注的饿汉式代码与考点

- 指令重排序问题
- 单例模式有且只有一份 static修饰
- 由于是在static中使用锁对象,所以锁对象也需要是static
- 锁对象最好是final / 事实final ,确保锁不会被错误引用或修改,保证线程安全不会被破坏
- 类static方法可以直接用类名直接引用,避免直接创建实例
- 单例模式两个if与上锁都是重要的,上锁保证线程安全
- 单例模式不能让你直接实例化,只能间接拿到对象
阻塞队列
基于队列“先进先出”的特点的同时加上线程安全,当队列满的时候就线程阻塞,当完成一个线程任务退出后便线程唤醒,后续阻塞的线程任务就能进入到阻塞队列中
因为阻塞队列的功能好用,所以就基于这个阻塞队列结构封装了一组服务器,这就是“消息队列”(Message Queue)
消息队列不仅可以降低两个服务器的耦合性,还能起到“削峰填谷”的作用,降低服务器的负担,让服务器始终能维持在一个合适的强度工作,是经典的**生产者消费者模型**
可以总结以下特点:
-
线程安全
-
阻塞功能
- 队列为空,尝试出队列,产生阻塞,直至队列不空
- 队列为满,尝试入队列,产生阻塞,直至队列不满
工作原理图belike

生产者消费者模型
-
降低资源的竞争
-
解耦合

-
削峰填谷
belike三峡大坝,使得服务器能够以自己的速度节奏来处理任务。不同的服务器它能承受的工作量是不同的,当请求量比较高时,就能够按照
-
自己的速度来读取速度,会花更多的时间消费队列中的内容,总体能保证自己能正常工作,不至于请求量太大卡死
弊端
像这样的任务请求,他的响应时间会受到影响,因为MQ会出现阻塞的情况,你不知道它前面还有多少个请求在排队~~
所以生产者消费者模型更适合应用在“异步”的场景,不适合“同步”的场景。像需要数据高速反馈的场景就不适合了,例如像FPS游戏的服务器
——那什么是同步 和 异步???
- 同步:A做完了事情需要跟B汇报,B通知了A,A再继续忙后续的事情
- 异步:A做完了事情跟B汇报,不管B回不回应,A都不等了,B把结果做好了再通知A
Java封装了阻塞队列
BlockingQueue


只有put 和 take 才带有阻塞效果~~
线程池
听着,所谓线程池…🍅
线程 相比于进程轻量,线程池就是提前把一些线程创建好,放到一个“池”里,想用就直接拿出来,比从用户态 —> 内核态 —> 用户态的操作系统步骤创建要快,属于是纯用户态的代码

如果我们采用线程池的方案,提前把线程通过操作系统的API创建好了,想要直接从用户态里去取就好了
优点
- 降低资源消耗:减少线程的创建和销毁带来的资源开销
- 提高响应速度:能当成任务直接调度,不用等待线程创建
- 可管理性:进行统一的分配,监控,避免因大量线程间的因互相抢占系统资源导致的线程阻塞
🎗️ThreadPoolExecutor类
Java的标准库中也提供了线程池的类
构造方法
构造方法是经典的面试题

-
线程数

核心线程数 与 最大线程数:核心线程数是不能销毁的,非核心线程数是当任务太多了系统自动创建新的线程。
最大线程数 = 核心线程数+非核心线程数
-
等待最大时间

-
任务队列

直接用阻塞队列,队列没有任务或爆满时能阻塞等待
-
线程工厂

工厂模式就是针对对象创建的一种设计模式,通过工厂类专门实现创建对象,将创建对象的过程与方法解耦合,便于扩展与维护,屏蔽内部对象的构造细节,根据输入的请求返回构造好的一个空的产品
-
拒绝策略💫

拒绝策略是当任务队列长度达到上限时,如果再尝试往里加入任务时就会触发“拒绝策略”,而阻塞就是拒绝策略的其中一种,但“死等”是不好的,设置超时时间就是典型的措施。但还有其他的拒绝策略,开发中通常认为新任务是比较重要的,其他的策略是尽力让新任务执行
一共有四种拒绝策略

- AbortPolicy:直接抛出异常
- CallerRunsPolicy:某个线程调用submit提交任务,但由于任务队列已满了,尝试让调用submit的那个线程负责执行这个任务
- DiscardOldestPolicy:把任务队列中最老的任务(最早加入的)直接抛弃,然后添加新任务进来
- DiscardPolicy:把任务队列中最新的任务(最晚加入的)直接抛弃,然后添加新任务进来
总结

Executors类
对ThreadPoolExecutor的进一步封装,更加方便

协程
是比线程更加轻量的线程——java21才引用的
ThreadLocal
是轻量线程级的变量,生命周期与线程相同
因为多个线程修改同一个变量是不安全的,而多个线程修改不同的变量是安全的。所以ThreadLocal就是给每个线程都分配了一个变量,让他们自己玩自己的,互不干扰
public static ThreadLocal<Integer> tl = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
tl.set(i);
}
System.out.println("t1: " + tl.get());
//可手动删除
tl.remove();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
tl.set(i);
}
System.out.println("t2: " + tl.get());
//不手动删除就等线程结束
});
t1.start();
t2.start();
t1.join();
t2.join();
tl.set(88);
System.out.println(tl.get());
}
- List item

当线程第一次使用tl的时候就创造了“副本”,而且修改副本不影响tl本体
希望能帮助到你们,祝身体健康,欢迎评论区指正错误
10万+

被折叠的 条评论
为什么被折叠?



