多线程八股文(自用)
多线程
程序:指令+数据(指令加载到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线程扫描内存区域,发现弱引用,
回收弱引用相关联的对象,对于弱引用的回收,无关内存区域是否足够,
一旦发现立即回收