当前位置: 首页 > backend >正文

多线程八股文(自用)

多线程

程序:指令+数据(指令加载到CPU,数据加载到内存)

一个程序执行,磁盘加载代码到内存,开启了一个进程

一个线程就是一个指令流

一个进程分为一个到多个线程

进程:正在运行程序的实例,每个线程执行不同任务

不同进程使用不同的内存空间,一个进程的所有线程共享内存空间

线程上下文切换的代价更低

单核CPU下线程串行执行

操作系统组件任务调度器,将CPU的时间片分给不同的程序使用

微观串行,宏观并行

并发轮流使用CPU

并行每个核都调度运行线程

创建线程的方式

继承Thread类

重写run方法

主类new,start开启

实现Runnable接口

重写run方法

主类new

线程创建时参数传入,start开启

实现Callable接口

重写call方法

主类new

FutureTask创建时参数传入

创建线程时参数传入ft,start开启

ft的get方法获得返回值

线程池创建线程

实现Runable接口

主类new

创建线程池参数传入

Runnable没有返回值

Callable有返回值,配合Future,FutureTask获得异步执行结果

Callable中call方法允许抛出异常

Runnable中run方法里的异常只能内部处理不能抛出(try catch)

start启动线程,start方法只能被调用一次

直接调用run方法就和普通方法一样了,可以调用多次

线程状态

新建态:创建线程对象

start

可执行态:就绪 运行

运行 sleep(50) 计时等待态 到时间了 就绪

运行 wait() 等待态 notify() 就绪

运行 无法获得锁 阻塞态 获得锁 就绪

死亡态

如何保证三个线程按顺序执行

线程中的join()方法 等待线程运行结果

t.join调用此方法的线程进入时间等待状态,

直到该线程执行完成后,此线程继续执行

notifyAll:唤醒所有wait线程

notify:随机唤醒一个wait线程

sleep是Thread的静态方法

wait是Object的成员方法,每个对象都有

wait和sleep都可以被打断唤醒

锁特性不同(重点)

wait方法调用先获取wait对象的锁,sleep不用

wait方法执行后会立即释放锁,允许其它线程获得对象锁(放弃CPU,别的线程还可用)

sleep如果在synchronized代码块中执行,不会释放锁(放弃CPU,别的线程不可用)

停止一个正在运行的线程

使用退出标志,线程正常退出,run方法完成后线程终止

使用stop方法强行终止(方法已作废)

使用interrupt方法中断线程

打断阻塞的线程(sleep,wait,join)的线程,线程会抛出interruptedException异常

打断正常的线程,可以根据打断状态来标记是否退出线程

synchronized关键字

对象锁采用互斥方式,同一时刻至多只有一个线程持有对象锁,

其他线程获取这个对象锁会被阻塞住

java -v xx.class 查看class字节码信息

Monitor

上锁(对象锁)

解锁(对象锁)两次 防止因为异常抛出时没法释放锁

监视器 由jvm提供 c++语言实现

结构:

Owner:存储当前获取锁的线程,只能有一个线程可以获取锁

EntryList:关联没有抢到锁的线程,处于Blocked状态的线程(阻塞态)

WaitSet:关联了调用wait方法的线程,处于waiting状态的线程(等待态)

Monitor实现的锁是重量级锁,设计用户态和内核态的切换,进程的上下文切换

jdk6引入两种新型锁机制:偏向锁、轻量级锁

解决没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题

在HotSpot虚拟机,对象在内存中存储的布局分为

对象头:Mark Word对象头、Klass Word描述对象实例的具体类型

实例数据:成员变量

对齐填充:对象头+实例变量不是8的整数倍,通过对齐填充补齐(无意义)

每个java对象都可以关联一个Monitor对象,使用synchronized给对象上

重量级锁之后,对象头的Mark Word被设置为指向Monitor对象的指针

轻量级锁:java程序运行时,如果同步代码块的代码不存在竞争,

不同线程交替执行代码块中的代码,重量级锁没有必要

加锁流程:

1.线程栈中创建一个Lock Record,将其obj字段指向锁对象

通过CAS指令将Lock Record的地址存储在对象头的mark word中

如果对象处于无锁状态修改成功,代表该线程获得轻量级锁

如果当前线程已经持有该锁了,代表这是一次锁重入

设置Lock Record第一部分为null,起到一个重入计数器的作用

如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁

解锁流程:

遍历线程栈找到所有obj字段等于当前锁对象的Lock Record

如果Lock Record的Mark Word为null,代表是一次重入,将obj设置为null后continue

如果Lock Record的Mark Word不为null,

利用CAS指令将对象头的Mark Word恢复为无锁状态,如果失败膨胀为重量级锁

偏向锁

轻量级锁在没有竞争(就自己这个线程),每次重入任然需要CAS操作

java6引入偏向锁:只有第一次使用CAS将线程id设置到对象的Mark Word头,

之后发现线程id是自己的就表示没有竞争,不用重新CAS,只要不发生竞争,

这个对象就归该线程所有

一旦锁发生了竞争,都会升级为重量级锁

JMM java内存模型

共享内存中多线程程序读写操作的行为规范

JMM把内存分为两块:私有线程的工作区(工作内存),所有线程的共享区域(主内存)

线程和线程之间相互隔离,线程跟线程交互需要主内存

CAS

比较再交换 乐观锁的思想 保证线程操作数据的原子性

juc(java.util.concurrent):

AQS框架

AtomicXXX类

CAS底层依赖一个Unsafe类来直接调用操作系统底层的CAS指令

CAS乐观锁 操作共享变量的时候使用的自旋锁

synchronized悲观锁

volatile

一个共享变量(类的成员、类的静态成员变量)

保证线程间的可见性:

1.防止编译器等优化发生:

JVM虚拟机的JIT(即时编译器)给代码做了优化

在程序运行时加入vm参数 -Xint表示禁用即时编译器,不推荐,因为其他程序还用

用volatile修饰

2.一个线程对共享变量的修改对另一个线程可见

禁止进行指令重排

写操作加的屏障是阻止上方其他写操作越过屏障排到volatile变量写之下

读操作架的屏障是阻止下方其他读操作越过屏障排到volatile变量读之上

使用技巧

写变量,让volatile修饰的变量在代码最后位置

读变量,让volatile修饰的变量在代码最开始位置

@Actor保证方法内的代码在同一个线程下执行

AQS

抽象队列同步器 本质就是锁

synchronized

关键字,c++语言实现

悲观锁,自动释放锁

锁竞争激烈都是重量级锁,性能差

AQS

java语言实现

悲观锁,手动开启和关闭

锁竞争激烈的情况,提供了多种解决方案

AQS常见的实现类

ReentrantLock 阻塞式锁

Semaphore 信号量

CountDownLatch 倒计时锁

AQS基本工作机制

一个被volatile修饰的变量(state)作为锁

0表示无锁,1表示有锁

FIFO队列 头指针 尾指针

cas设置state状态,保证操作的原子性

新线程与队列中的线程共同来抢资源,是非公平锁

新线程到队列中等待,让队列中的head线程获取锁,是公平锁

ReentrantLock 可重入锁 手动释放锁

相比synchronized

可中断

可以设置超时时间

可以设置公平锁

支持多个条件变量

相同点都支持重入

ReentrantLock利用CAS+AQS队列实现,支持公平锁/非公平锁

构造方法接受可选的公平参数(默认非公平锁)

非公平锁的效率更高

线程用cas抢锁,设置state,让exclusiveOwnerThread属性指向当前线程

修改状态失败,进入双向队列中等待

head双向队列头,tail双向队列尾

exclusiveOwnerThread为null,唤醒双向队列中等待的线程

公平锁按照先后顺序获取锁

非公平锁不在排队的线程也可以抢锁

synchronized

关键字,源码在jvm中,用c++实现

退出同步代码块锁会自动释放

Lock

接口,源码由jdk提供,用java实现

使用Lock,手动调佣unLock方法解锁

都是悲观锁,互斥,同步,锁重入

Lock具有更多场景:公平锁,可打断,可超时,多条件变量

有多个实现:ReentrantLock,ReentrantReadWriteLock

竞争激烈的时候Lock优势更明显

死锁条件

一个线程需要同时获得多把锁

死锁诊断

jdk自带工具:jsp,jstack

jsp:输出JVM中运行的进程状态信息

jstack:查看java进程内线程的堆栈信息

可视化工具

jconsole java安装目录bin下

VisualVM故障处理工具 java安装目录bin下

ConcurrentHashMap

线程安全

java7分段的数组+链表

segment数组长度为8,不可扩展

再hash引出数组,可扩容

java8跟HashMap一样,数组+链表/红黑树

采用CAS+synchronized保证并发安全

CAS控制数组节点的添加

synchronized只锁定当前链表或者红黑二叉树的首节点

只要hash不冲突,不会产生并发问题

java并发编程

原子性:

1.synchronized:同步加锁

2.JUC:Lock加锁

可见性(内存)

1.synchronized

2.volatile

3.Lock

有序性

volatile

线程池

核心参数

核心线程数

最大线程数:核心线程数+救急线程最大数

生存时间:救急线程的生存时间,生存时间内没有新任务,此线程资源被释放

时间单位:救急线程的生存时间单位,如秒,毫秒

阻塞队列:没有核心线程,新任务会加入此队列,队列满会创建救急线程

线程工厂:定制线程的创建,设置线程名字,是否为守护线程

拒绝策略:所有线程都繁忙,阻塞队列也满了,触发拒绝策略

1.直接抛出异常,默认策略

2.主线程执行任务

3.丢弃阻塞队列中最靠前的任务

4.直接丢弃任务

常见阻塞队列

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。必须指定容量

2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。可以不指定容量

3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,执行时间越早优先级越高,可以设置任务什么时候执行

4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

LinkedBlockingQueue

默认无界,支持有界

底层是链表

懒惰的,创建节点时添加数据

入队会生成新的Node

两把锁:头节点上锁,尾结点上锁

ArrayBlockingQueue

强制有界

底层是数组

提前初始化Node数组

Node是提前创建好的

一把锁,锁的是整个数组

核心线程数

1.高并发,任务执行时间短 cpu核心数+1 减少线程上下文切换

2.并发不高,任务执行时间长

1.io密集型任务 cpu核心数*2+1

文件读写,DB读写,网络请求

2.计算密集型任务 cpu核心数+1

计算型代码,Bitmap转换,Json转换

3.并发高,业务时间长

缓存数据,增加服务器,转换成上面两种情况

线程池种类(四种常见静态方法)

java.util.concurrent.Executors

固定线程数的线程池

没有救急线程,阻塞队列Linked,为最大容量

适用于任务量已知,相对耗时的任务

单线程化的线程池

唯一的工作线程执行任务,保证所有任务按顺序执行

核心线程数和最大线程数都是1

阻塞队列Linked,为最大容量

适用于按照顺序执行的任务

可缓存线程池

核心线程数为0

最大线程数为最大数值

阻塞队列为SynchronousQueue不存储元素

适合任务比较密集,但每个任务执行时间较短

延时和周期执行的线程池

阻塞队列使用DelayedWorkQueue

可以执行延迟任务,定时以及周期性任务的执行

不建议使用上述四种方式创建线程池

尽量使用ThreadPoolExecutor创建

不使用Executor

1.固定容量线程池和单例化线程池

阻塞队列都设置最大容量,会堆积大量请求,导致OOM

2.可缓存线程池

允许创建的线程设置最大数值,会创建大量线程,导致OOM

线程池使用场景

CountDownLatch闭锁/倒计时锁

等待多个线程完成后某件事情才能执行

构造参数用来初始化等待计数值

await用来等待计数归零

countDown用来让计数减一

es数据批量导入

项目上线之前,需要把数据库中数据一次性同步到es索引库中,

一次数据读写不行(oom异常),使用线程池的方式导入,利用CountDownLatch控制

数据汇总

需要查询的数据包含多部分,在不同的微服务中实现,

调用不同接口,所有接口或部分接口没有依赖关系,

可以使用线程池+future来提升性能

异步调用

比如查询的时候保存此次的搜索记录

避免下一级方法影响上一级方法(性能考虑)

在线程池获得一个新的线程执行

使用异步线程调用下一个方法

控制某个方法允许并发访问线程的数量

Semaphore信号量 JUC包下的一个工具类,底层是AQS

限制执行的线程数量

创建Semaphore对象,可以给一个容量

Semaphore.acquire() Semaphore.release()

ThreadLocal

ThreadLocal是多线程解决线程安全的一个操作类

为每一个线程都分配一个独立的线程副本解决变量并发访问冲突的问题

ThreadLocal实现线程内的资源共享

例:JDBC操作数据库,将每一个线程的Connection放入各自的ThreadLocal,

保证线程在各自的Connection上进行数据库操作,避免A线程关闭B的连接

set(value) get remove

本质:线程内部存储类,让多个线程只操作自己内部的值,从而实现线程数据隔离

ThreadLocal内存泄漏问题

java对象四种引用类型:强引用,软引用,弱引用,虚引用

强引用:最普通的引用方式,一个对象处于有用且必须的状态,

一个对象具有强引用,GC不会回收它,即使内存不足,宁可出现OOM,也不回收

弱引用:对象处于有用但是非必须得状态,GC线程扫描内存区域,发现弱引用,

回收弱引用相关联的对象,对于弱引用的回收,无关内存区域是否足够,

一旦发现立即回收

http://www.xdnf.cn/news/6780.html

相关文章:

  • SOLIDWORKS Simulation接触定义精讲(一)
  • CVE-2017-8046 漏洞深度分析
  • 【每天一个知识点】意图传播(Intent Propagation)
  • AG 视频下载 免费分享
  • 从零开始学习three.js(19):一文详解three.js中的辅助类Helper
  • 彻底删除Docker容器中的环境变量
  • 【Kuberbetes】详谈网络(第三篇)
  • 机器学习中的特征工程:解锁模型性能的关键
  • Mysql数据库详解
  • 最小二乘法:从房价预测到损失计算
  • 从裸机开发到实时操作系统:FreeRTOS详解与实战指南
  • 质量管理工程师面试总结
  • 【AI基础设施安全检测工具】AI Infra Guard安装使用详细说明
  • 全面且深度学习c++类和对象(上)
  • 视频抽帧并保存blob
  • 第二十六天打卡
  • 数据备份与恢复方案
  • 7. 进程控制-进程替换
  • WebGIS开发智慧机场项目实战(2)
  • 前端学习(4)—— JavaScript(基础语法)
  • 循环嵌套与枚举算法
  • C41-为什么要用指针
  • 后端框架(3):Spring(1)
  • 【技术原理】ELK技术栈的历史沿革与技术演进
  • Linux——一键部署应用脚本
  • 方法区与元空间解析
  • 软件架构风格系列(2):面向对象架构
  • (网络文件系统)N
  • 本地部署Scratch在线编辑器
  • Ngrok 配置:实现 Uniapp 前后端项目内网穿透