并发编程
2021-11-12 11:18:09 0 举报
AI智能生成
针对于Java语言设计的多线程,以及并发编程的常用方式
作者其他创作
大纲/内容
继承Thread
实现Runnable
匿名内部类
park()/unpark()
底层LockSupport
Callable和Feature
创建
新建
调用yield,放弃当前cpu时间片,重新进入到就绪,等待被CPU调用
运行(Runnable)---(非Java中可再划分为就绪和运行)
配合Synchronize使用,调用wait方法后,释放锁,该线程存放在“waitThread”集合中等待;一旦调用notify的时候,该线程会重新进入到“blockThread”集合,同时状态会变为就绪,竞争锁的资源
等待
睡眠时不释放“锁标志”
Sleep指令
wait(long time)
底层使用wait/notify指令
join(long time)
超时等待
Synchronize锁中未获取到锁的线程,会存放到一个blockthreads集合中,一旦释放锁后,该集合中的线程又重新竞争锁对象<br>
阻塞
线程不安全,清除监控器锁的信息
stop()指令
处于wait、join、sleep时可以生效
interrupt()指令
自定义volatile flag标志位
终止
状态(虚拟机中状态)-不反应任何操作系统的线程状态
用户线程
跟随主线程停止而停止
守护线程
分类
字节码
上下文切换
JMM内存模型
安全问题
线程
ThreadLoca设置值
解说
栈中的引用
ThreadLocalMap中的Entry对象对应的key引用
ThreadLocal引用(两处)
引用
软,发生GC的时候,如果内存不过用则会回收
弱,发生GC的时候,会被回收
强、软、弱、虚
类型
方法一,先调用remove,然后再调用赋值ThreadLocal==null
方法二,ThreadLocal中set的时候会遍历之前的key,如果是null的话,则直接干掉
预防
内存泄露
ThreadLocal
四种(底层采用无界队列)
防止CPU飙高问题
创建方式
核心数量
任务队列
最大线程数
超出核心线程数好后创建的线程存活时间
存活时间
线程池内部创建线程所用的工厂
线程工厂
任务无法执行时的处理器
处理器
核心参数
线程池可以接收新任务,及时对新添加的任务进行处理
RUNNING
线程池不可以接收新任务,可以对已添加的任务进行处理
SHUTDOWN
线程池不接收新任务,不处理已添加的任务,中断正在处理的任务
STOP
当所有的任务已终止,ctl记录的“任务数量”为0,线程池变为TIDYING,同时执行构造函数terminated()
TIDYING
线程池彻底终止的状态
TERMINATED
状态
核心线程数=CPU核心数
CPU密集型
核心线程数=CPU核心数*2
IO密集型
优化配置
ThreadPool(线程池)
底层采用Lock锁机制,进行元素的添加/移除,使用同一把锁
原理
默认是有界限的,采用int进行计数的
ArrayBlockQueue
底层采用Lock锁机制,进行元素的添加/移除,使用不同锁
默认是无界限的,采用AutomicInteger计数的
LinkBlockQueue
BlockQueue(队列)
L1
L2
L3
CPU高速缓存
CPU访问主内存的效率不高,因此引入了CPU高速缓存(工作内存);CPU ->CPU高速缓存->主内存
前因
CPU将数据从主内存读取到工作内存的时候,采用的是缓存行的方式,一次性读取64字节
代码
注解
填充对象的属性到64字节
伪共享问题
缓存行(加入validate关键字)
线程之间数据的的可见性
程序执行的顺序性(禁止重排序)
不保证原子性
特征
读指令前插入读屏障,高速缓存中数据无效,重新从主内存加载数据
读内存
写指令之后插入写屏障,写入缓存中的最新值刷新到主内存中
写内存
内存屏障
底层
通过汇编lock前缀指令触发底层锁的机制
过程省略了-反正就是加锁的机制
总线锁
MESI缓存一致性协议
锁的机制分类
工作内存和主内存数据同步方式
volatile
全局共享变量存放在主内存中
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量是需要从主内存总重新读取最新的值
线程解锁前,必须把共享变量的最新值刷新到主内存中
Synchronized
扩充
Java内存模型
new对象
克隆方式
反射
序列化与反序列化
反射可以破解懒汉式、饿汉式;无法破解静态代码块;序列化和反序列化可以破解静态代码块(懒汉和饿汉也可以破解)
说明
创建对象的方式
普通
安全
建议私有变量中添加volatile关键字,禁止new对象重排序
1.分配内存空间
2.初始化对象
3. 内存空间的地址赋值给对应引用
2和3有可能被处理器优化,发生重排序
对象创建过程
双重校验锁
懒汉式
可以防御反序列化生成的对象
前提添加此方法readResolve(),JDK源码重新编译则依旧防御不了(Jdk源码判断反序列化类中如果存在readResolve方法,则通过反射机制调用readResolve方法返回相同的对象)
备注
饿汉式
可以防御反射方式破解单例
提前在该类的私有方法里面写一个判断If(对象!=nulll){throw new exception}
静态代码块
反编译后,该类默认继承了Enum类,一些方法也是来源于此Enum类中的
反射newInstance方法中会判断实例化类的类型是否是枚举,报错提示
可以防御反射
序列化中获取对象的时候有个判断是否是枚举类,是的话,则直接调用枚举的valueOf方法获取枚举对象
可以防御序列化
枚举
单例模式
FutureTask
ForkJoin
Disruptor
wait/notify(配合synchronized使用,阻塞当前调用线程)
LockSupport.park/unpark(阻塞当前调用的线程)
实现方式
Callableball和FuturetureTask
悲观/乐观
公平/非公平
锁
Lock类
上层封装框架
value在该对象中的偏移量,也就是该对象中填充的value属性对应的偏移位置
valueoffset
value
unsafe
下层依赖对象
底层把预期值N赋给V的时候,从CPU硬件级别保证了安全性
赋值失败的话,则重新循环,然后再做修改
原子类-Atomic
范例
第一个线程修改值为A,第二个线程修改为B,第三个线程又修改为A
增加版本号/递增时间戳
优化
ABA问题
问题
CAS
8字节
哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
因此GC之后的年龄最大是15岁
GC分代年龄(4位)
哈希码(31位)
锁标志位(3位)
未被使用的(26位)
重点
包含内容
Mark Word
8字节(默认虚拟机开启指针压缩-占4个字节)
对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
描述
Class指针
对象头-详解
类中的属性等大小
实例数据
8的整数倍
填充
无锁-001
偏向-101
栈中锁记录的指针(64)
轻量级-000
Monitor记录了HashCode的值
monitor的指针(64)
重量级-010
空,不需要记录信息
GC标记-011
Mark World中锁的状态
对象的组成
方法内锁对象==方法加Synchronized
方法内锁类字节码==静态方法加Synchronized
方式
通过Monitor对象调用操作系统的Mutex指令,来保证互斥量;属于OS自身来进行维护
jdk<1.6时候
因为考虑到可能锁住的代码块,执行效率可能会比较高,那么没必要让其它线程直接变成阻塞状态;所以可以在自旋里面重试一段时间,实在不行的话,再进入到阻塞状态
增加了偏向锁、轻量级、重量级
Jdk>1.6时候
Owner(拥有锁的线程)
Recursions(重入次数)
获取锁的对象,释放该对象锁后进入地方;当被其它线程notify的时候,则从等待池转到锁池中
waitSet(等待池)
没有获取到锁的对象,通过链表数据结构存放
entryList(锁池)
monitor(监视器)对象-(每个同步对象都有自己的监视器锁)
尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁
monitorenter
释放monitor的所有权
异常退出时也会执行
monitorexit
底层指令
1.new object对象
2.Synchronized(对象)
先打印是无锁的状态,然后直接到了轻量级锁的状态,缺少偏向锁的升级过程
默认情况下,JVM会推迟偏向锁的显示;因为考虑到启动JVM的时候,存在内部的线程使用Sync来执行操作
原因
正常现象(没开启偏向锁)
睡眠5s(开启偏向锁的时候)
现象
偏向锁的阶段打印对象的HashCode的时候会直接把偏向锁的状态变成轻量级锁
场景
偏向锁
对象头中HashCode值和栈帧中的LockRecord对象中地址值更换
在加锁过程中将自身的地址和对象头中MarkWorld里面对应的内容做替换
lockRecord(记录地址作用)
指向当前加锁对象的一个引用
objectReference
线程执行的每个方法都会初始化成一个方法栈帧,该栈帧中存放着一个LockRecord对象
锁的重入后,则会新增一条Lockrecord作为重入次数;当退出代码块的时候,发现LockRecord地址指向的是null,则代表重入了,此时计数减一
锁的重入性
成功
1.多个线程同时竞争同一把锁,通过CAS修改对象头中锁的状态
2.当我们使用轻量级锁,释放锁时,还原MarkWord值内容
轻量级锁(用户态)
1.没有获取到锁的线程,存放在C++ Monitor对象EntryList集合中,同时当前线程阻塞,释放CPU的执行权
重量级锁
锁的升级过程
每个线程持有锁的时间尽可能短
粗化
编译器级别一种锁优化方式
消除
自旋
LockSupport
Queue
依赖机制
1-当前的线程被取消
0-表示当前节点在sync队列中,等待着获取锁
-1-释放资源后需唤醒后继节点
-2-等待Condition唤醒,存放在等待池中
-3-工作与共享锁状态,需要想后传播
waitStatus
等到锁的线程
Thread
双向链表中存放阻塞队列实体
Node节点
头结点,等待队列的头结点
Head
尾结点,正在等待的线程
Tail
如果线程重入的话,则state不断+1
锁的状态(0-无锁,1-有线程获取到锁)
state
记录当前持有锁的线程
exclusiveOwnerThread
公平和非公平就是在CAS的时候是否执行了!hasQueuedPredecessors()
细节
ReentrantLock 在默认情况下就是属于非公平锁
等待池
Condition
阻塞列表,也就是锁池
Lock
则使用exclusiveOwnerThread记录当前线程
本身AQS类中已经存在一个属性存放获取锁的线程,因此没必要在队列中再次存放
头节点不存放任何线程
注意
初始化链表,将当前线程存放在AQS类的双向链表尾部
阻塞线程使用lockSupport.park
失败
使用CAS修改AQS中状态值(从0到1)
获取锁
当前AQS的状态如果为0,则会唤醒阻塞队列中头结点的下一个节点从新进入到竞争锁的状态,成功的话,则会移除
释放锁
非公平锁
公平锁中首先会判断是否已经有等待的线程,如果没有等待的线程才开始利用CAS进行争抢
非公平锁刚开始获取锁的时候,直接使用了一次CAS尝试获取锁,不成功才会构建Node节点
区别(获取锁过程)
释放锁过程一样
公平锁
调用signal()后,将等待池中的node节点转移存放在锁池,等待竞争锁资源
调用signal()后,再调用unlock()方法后,唤醒锁池中的线程开始竞争锁资源
唤醒等待中的线程
1.初始化时给AQS类中的状态设置值
2.调用acquire()底层修改AQS中状态值-1的操作,如果修改成0之后,则当前线程阻塞,存放在AQS类中的双向链表中
3.调用release()底层对AQS类中状态+1的操作,同时唤醒AQS类中阻塞链表中首个线程
Semaphore(信号箱)
唤醒双向链表中存放的线程
是
countdownLatch
1.初始化给内部count 属性赋值
存放在等待池中
继续执行,同时唤醒等待池中所有的线程
否
2.调用await()底层的时候,进行count-1操作,判断count!=0
CyclicBarrier(线程栅栏)
AQS并发框架
AQS(抽象同步队列)
前者是关键字,后者是接口与
前者无法判断是否获取锁的状态,后者可以
前者会自动释放锁,后者需要手动的,并且注意死锁
前者非公平的,后者是公平的
Sync和Lock锁区别
并发编程
0 条评论
回复 删除
下一页