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