JAVA多线程
2022-09-20 18:35:13 177 举报
AI智能生成
java多线程知识点总结
作者其他创作
大纲/内容
线程
五种状态
新建(New)
使用new创建一个线程后
就绪(Runnable)
线程执行start方法后
创建方法调用栈和程序计数器
处于线程就绪队列
等待分配CPU时间片
运行(Running)
线程获得CPU时间片,开始执行run方法
可能变为阻塞状态、就绪状态和死亡状态
阻塞(Blocked)
运行状态的线程让出CPU资源
sleep()方法(不会释放锁),时间到后等待分配CPU
调用一个阻塞式IO
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
线程的suspend方法将线程挂起
调用wait方法,等待notify/notifyAll唤醒时(会释放持有的对象锁);
阻塞状态线程无法直接转为运行态,需先转为就绪态
等待 waitting
一个线程进入了锁,但是需要等待其他线程执行某些操作。时间不确定
当wait,join,park方法调用时,进入waiting状态。前提是这个线程已经拥有锁了。
当wait,join,park方法调用时,进入waiting状态。前提是这个线程已经拥有锁了。
TIMED_WAITING
一个线程进入了锁,但是需要等待其他线程执行某些操作。时间确定
通过sleep或wait timeout方法进入的限期等待的状态)
通过sleep或wait timeout方法进入的限期等待的状态)
死亡(Dead)
run()或call()方法执行完成
线程抛出一个未捕获的Exception或Error
状态转换说明
优先级
Thread.setPriority
从1到10,低到高
优先级高的线程在抢占CPU时间片时更有优势,取决于系统
饥饿与公平
饥饿
高优先级线程吞噬所有低优先级进程的CPU时间片,造成低优先级进程无法执行
线程被永久阻塞在一个等待进入同步块的状态
等待的线程永久不被唤醒
避免饥饿问题
尽量不要让线程优先级太低
使用锁代替Synchronized
volatile关键字
可见性
被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见
底层实现 CAS
禁止之后语句重排序
CPU的乱序执行
as-if-serial语义
在将多个指令发送给CPU后,CPU可能不会按顺序执行指令,但保证最终结果同顺序执行一致
编译器、runtime和处理器都必须遵守as-if-serial语义。
哪些语句可以重排序?
根据Happens-before原则 (JVM规定重排序必须遵守的规则),共8种情况
实现
JSR内存屏障,共4种
LoadLoad屏障
如Load1 - LoadLoad - Load2, 读取Load2前要保证Load1读取完毕
StoreStore屏障
LoadStore屏障
StoreLoad屏障
Volatile的内存屏障
volatile写
StoreStoreBarrier - volatile写 - StoreLoadBarrier
在volatile写前,所有的写入操作都要完成,所有Load操作要等待volatile写入完成
volatile读
volatile读 - LoadLoadBarrier
volatile读后才允许读取操作
volatile读后才允许读取操作
volatile读 - LoadStoreBarrier
volatile读后才允许写入操作
volatile读后才允许写入操作
汇编层实现
LOCK指令,所有操作都需要刷新到主存,只有的操作才能进行
问题
为什么DCL双重检查单例要加Volatile
在最终赋值语句如 INSTANCE = new Singleton() 可以理解为三步,执行顺序有可能变为132
(1)memory=allocate();// 分配内存
(2)ctorInstanc(memory) //初始化对象
(3)INSTANCE=memory //设置s指向刚分配的地址
(2)ctorInstanc(memory) //初始化对象
(3)INSTANCE=memory //设置s指向刚分配的地址
为什么不保证原子性
如i++自增操作分三步,读i值,i+1, 写回内存
volatile只保证读i值是最新值,此时i有可能被多个线程读取到
volatile只保证读i值是最新值,此时i有可能被多个线程读取到
AQS AbstractQueuedSynchronizer
队列同步器
队列同步器
它是构建锁的基础框架,JUC并发包中的核心基础组件
Unsafe 魔法类,绕过虚拟机,直接操作内存
CAS
compareAndSwapObject
compareAndSwapInt
compareAndSwapLong
挂起与恢复
LockSupport.park调用unsafe.park
特性
阻塞等待队列
共享/独占
公平/非公平
可重入
允许中断
阻塞等待队列
实现
自旋
LockSupport
CAS
基础
wait,notify(all)
调用时需要获得对象锁,也就是必须配合synchronized
属于objct方法(因为需要获取对象锁,所以需要对象调用)
阻塞的是调用线程
作用
wait
线程释放持有的对象锁,并等待notify
notify
随机唤醒一个等待当前对象锁的线程,并让被唤醒的线程拿到对象锁
notifyAll
唤醒所有等待当前对象的线程,但是被唤醒的线程会再次去竞争对象锁。
因为一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。
因为一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。
唤醒所有等待当前对象的线程,但是被唤醒的线程会再次去竞争对象锁。
因为一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。
因为一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。
生产者消费者模式
共同竞争一个缓冲队列,无法消费/生产时调用wait等待
join方法
join是在synchronized中调用的
实现:wait方法阻塞调用线程,并在jvm中退出方法时调用了notifyAll
ThreadLocal
Thread类中有成员变量ThreadLocalMap
ThreadLocalMap为ThreadLocal的内部类
是一个修改版的Map实现
key为ThreadLocal的弱引用, value为Object
set方法
如已有threadLocal实例,线程A在set操作时,将new出来一个 ThreadLocalMap对象,
Entry中key为threadLocal实例,value为自定义值。将此ThreadLocalMap对象赋值给线程A的threadLocals属性
Entry中key为threadLocal实例,value为自定义值。将此ThreadLocalMap对象赋值给线程A的threadLocals属性
内存泄漏问题
key是弱引用,在GC时被回收,导致Thread类hreadLocal中出现key为null, value不为null的Entry,如果线程一直没回收,可能会造成内存泄漏
解决方案
将ThreadLocal定义为private static, 作为key的threadLocal就一直有强引用,保证可以通过key访问到value并清除
每次使用完threadLocal都调用remove方法
InheritableThreadLocal
父子线程共享的ThreadLocal, 继承ThreadLocal
实现
创建子线程时,子线程会复制一份父线程持有的InheritableThreadLocal
重写了getMap, createMap, 使用InheritableThreadLocal实例调用时会返回子map对象
CAS
compare and swap 比较并交换, 先取出原值,在设置新值时比较旧值是否被修改
ABA问题
如原值为1,另一线程修改为2,再改为1
解决方法,加版本号,值每次修改版本+1
java Unsafe类
CAS方法
native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较。如果相等替换为x
读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较。如果相等替换为x
如AtomicInteger利用Unsafe的CAS实现
原子性原理
基于汇编指令 lock cmpxchg
两种实现方式
一:锁内存总线
lock cmpxchg lock指令 多核CPU通过内存总线访问主内存,lock指令将内存总线锁住,禁止其他核心访问
二:缓存一致性协议
现代CPU都是用这种方式
缓存一致性协议
硬件知识扩展
硬件层
CPU具有三级缓存,L1,L2,L3,速度递减,容量递增
其中L1,L2集成在核心中,L3为多核心共享
缓存行
CPU一次读取关键数据与其相邻的部分数据分别加载如L3,L2,L1
公认的一次性取64字节
问题:缓存不一致
因为每个核心都有L1,L2缓存,一份数据就产生多个副本,可能会发生不同核心修改了同一个数据,造成缓存不一致
解决方案
Intel为MESI
CPU为每个缓存行标记4种状态Modified,Exclusive, Shared, Invalid
某一CPU核心在修改某一缓存行后将其置为Modified,同时通知其余核心,其余核心将持有的缓存行置为Invalid,
核心将修改后的值写回主存(在适当时机,如被读取),其余核心需要从主存重新读取一遍Invalid状态的缓存行
核心将修改后的值写回主存(在适当时机,如被读取),其余核心需要从主存重新读取一遍Invalid状态的缓存行
锁
锁的概念
互斥性:即在同一时间只允许一个线程持有某个对象锁
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的
可重入:jvm有一个计数器,每重入一次+1,为0时其他线程才能进入
内存模型
对象头Markword第一字节
synchronized
为什么在java1.0时效率慢
synchronized运行在ring3用户态,其锁申请过程需要经历从ring0申请锁的过程
升级过程
当只有一个线程获取,加偏向锁,101
对象被第二个线程获取,加轻量级锁 00
当竞争激烈时会直接跳过,加重量级锁
当竞争激烈时会直接跳过,加重量级锁
因为1.6后的自适应自旋,JVM控制升级为重量级锁 10
锁的范围
加在普通方法, 锁定当前对象
加在静态方法,锁的是类字节码
同步代码块,锁定括号内的对象
锁的种类
轻量级锁(自旋)
当有第二个线程请求共享资源时
不需经过操作系统,即不用经历从ring3-ring0转换
竞争激烈时不适合用,会让大量线程占用CPU空转
自旋阈值
JDK1.6后引入适应性自旋锁,自动控制阈值
重量级锁(互斥锁)
向操作系统申请互斥锁
为什么开销大?
挂起、唤醒线程都需要依赖操作系统线程管理切换上下文(主要),经历从用户态到内核态转换
偏向锁
偏向于第一个访问的线程,只有一个线程获取锁时使用,锁状态为101
延迟4s启用,JVM启动时会发生大量锁竞争,所以延迟启用
启用后new出来的对象是匿名偏向,101
重入锁
同步方法或同步块内部可以调用锁定对象的其他同步方法, 不需要重新获取锁.
用于继承,如重写的同步方法调用父类同步方法
实现
上一次锁在线程栈中生成一个LR(Lock Record),解锁一次则弹出一个LR
共享锁
独占锁
排它锁
读写锁
公平锁
非公平锁
死锁
线程间彼此依赖对方的资源又不释放自己持有的资源,造成永久性阻塞
活锁
活锁应该是一系列进程在轮询地等待某个不可能为真的条件为真。活锁的时候进程是不会blocked,这会导致耗尽CPU资源
锁优化
减少锁的粒度
如ConcurrentHashMap,将一个锁置换为多个锁,增加并行度
LinkedBlockingQueue
在队列头入队,在队列尾出队,入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高;
锁粗化
如循环内的加锁过程,应把加锁过程放在循环外,否则每一次循环都要经过一次临界区
使用读写锁
ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;
使用CAS
JMM内存模型-JSR133规范
java内存模型是抽象的概念,描述的是程序间变量的访问规则
JMM规定每个线程都有工作内存,无法直接操作主存,工作内存持有主存中共享变量的副本
JMM 8大数据原子操作
lock锁定
作用于主内存的变量,把一个变量标记为一个线程独占状态
unlock解锁
作用于主内存变量,把一个处于锁定状态的变量释放出来,使其可以被其他线程锁定
read读取
把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load载入
它把read操作从主存中获取的变量值放入工作内存的变量副本中
use使用
把工作内存中一个变量值传递给执行引擎
assign赋值
把一个从执行引擎接收到的值赋给工作内存的变量
store存储
把工作内存中一个变量的值传递给主存,一遍随后的write操作
write写入
把store操作获取的值传递到主存中的变量中
0 条评论
下一页