线程池
线程池的好处
线程池适用的场合:
增减线程的时机:
增减线程的特点:
线程回收:
通常情况下,不用自己写线程工厂,源码其实就是new Thead 传入不同参数即可
使用情况,以及优缺点
手动创建线程池还是自动创建线程池:
SynchronousQueue其实内部不存储,不用队列中转,直接去执行
这个了解就可以:
如何停止线程池
开始结束
开始结束查看
真正结束查看
线程等待一段时间,返回结果,这个只是个检测方法
当使用awaitTermination时,主线程会处于一种等待的状态,等待线程池中所有的线程都运行完毕后才继续运行
对于正在运行的线程,设置interupt中断状态位,队列中的直接返回,返回一个列表(中断线程)List List.,可以用新的线程池继续执行。
拒绝策略
拒绝时机:
CallerRunsPolicy 进行负反馈,线程池会腾出空闲,给一些缓冲
高级用法:可暂停的线程池
用的时候再看
原码分析
管理器就是用来创建,删除线程的
工作线程就是用来执行任务的线程(Work对象)
Executor只有一个方法
ExecutorService extends Executor
Executors 创建线程静态工厂
线程池是如何实现任务复用的原理
这个线程不是普通的线程,在这个线程的run 方法里面 会不停检测有没有新的任务进来,有的话就会调用这个新的任务的run方法。把这一些列的任务串联起来执行
线程池 让我们 不同的 从队列中 取任务出来并执行
executor源码分析:
Worker就是工作线程,再runworker之中直接调用task的run方法,task是个runnable实现,getTask()就是从阻塞队列中取出来
线程池使用注意点
线程泄露就是线程结束不了,一般是写的业务逻辑有问题
ThreadLocal
使用场景:
ThreadLocal的两个作用:
如何向ThreadLocal中存放数据,怎么选择
ThreadLocal的好处:
TheadLocal原理:
ThreadLocal就是个工具类
ThreadLocalMap是Thread的成员变量
一个线程拥有多个threadLocal
initialValue 源码解析
Get方法源码解析:
this就是TheadLocal对象
Set方法源码解析:
remove 源码解析:
总结:
不是拉链方法:
内存泄漏问题:
构造函数不一样,利用了弱引用了,
空指针异常:
小心包装类型,要有拆箱,装箱,在没有set的情况下,get就会返回null
共享对象问题:
Spring 中ThreadLocal的应用:
保存时间上下文
保存请求信息,里面有请求参数,请求头
锁加强
为什么sync不够用?
所以Lock接口就出来了:
可见性保证
happens-before原则,JMM靠这个解决可见性问题
锁的分类
这些分类会导致角度不同,分类角度不同
乐观锁与悲观锁
乐观锁 为什么产生:
优先级低的线程获取锁,并且不能释放,高优先级就悲剧了
git就是乐观锁
git就不适合悲观锁,公司会倒闭
重入锁与不可重入锁:
电影院买票就是需要锁的场景,锁定三分钟
其他方法介绍:
公平锁与非公平锁:
有非公平锁,是避免有空档期
A用完,去唤醒挂起的B,这时候C来了并且会很快结束,C直接用吧插队,用完,B也唤醒了,就接着B,这种差别会导致吞吐量的提升,唤醒过程开销是比较大的。
红框先判断有没有队列里的内容
共享锁于排它锁:
另一种理解方式:
读锁和写锁的交互方式
就比如男女共同上厕所,男生可以插队嘛?
循允许降级不许升级,不允许读锁插队
公平方式源码:
写锁,永远永远不用看人脸色,直接插队
看第一个是不是想获取写锁的线程。
这些方法都在tryAcquired方法里面被调用。
比如有些任务开始写一些日志,后面开始读取一些文件,这样在持有写锁时候,完成读
先获取写锁,再获取读锁,在持有的时候获取读锁,降级的好处可以提高效率
升级锁会导致线程阻塞? 就是因为读锁和写锁不能同时存在。
自旋锁与阻塞锁
怎么选?
Atomic包下的基本都是自旋锁实现的,基本都是CAS
自己写一个简单的自旋锁:
需要一个原子操作,利用CAS将值换上去
使用场景:
可中断锁与不可中断锁:
Java对虚拟机对锁的优化:
JRET编译器会动态检测
写代码时候如何提高锁并发性能:
写日志框架,打印日志需要加锁,用消息队列,减少了锁的次数
Atomic包加强
6类原子类:
a++ 本身分为三步操作
在类不是自己写的时候,需要这种操作 or 有些情况下才需要并发,其余时间不需要
内部是反射原理,所以可见范围是public的,static修饰会爆出异常
Java8 才引入 Adder 累加器
sum源码
sum过程中没有锁,cell有些变化,但求和已经结束了,求和不是很准确
Accumulator的使用
CAS
就是完成了不能被打断的数据交换操作
A是我之前读取过来的,B是我算出来的。
最终是要使用CPU的特殊指令的保证原子操作
应用场景
1 乐观锁
2 并发容器有很多CAS 体现,比如ConcurrentHashMap
3 原子类
原子类是如何实现CAS源码分析
静态代码块,拿到了value的地址
这个是unsafe类
compareANdSwapeInt是个native方法
Unsafe 类![在这里插入图片描述](https://img-blog.csdnimg.cn/20210124160250790.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTAxMjI2MDQ=,size_16,color_FFFFFF,t_70)
所以直接操作的是地址,真正的实现是一个cpp
CAS的缺点:
不变性实现
不可变性是并发编程中,重要的环节
在编译期间 就知道准确值了,而且永远不会变化,所以会当做编译时候的常量来使用,就直接使用常量,不用再运行时候再确定,
c可以确定为wukong2 那么就不会再建立一个常量 wukong2,就指向同一个地址
编译器不会提前知道 d的值,所以e的值在运行时候才能确定,所以e会在堆上生成wukong2
就看在生成字符串的时候,能不能确定值,还是运算时候确定
并发 集合加强
这三个重点了解
这个不常用
Map对应关系:
HashMap 死循环,CPU100% 问题
核心原因,多个线程同时扩容的时候会导致链表的死循环,你指向我,我指向你 ,考的话,就是看你背没背到题。
hashMap 作者,他们能知道吗,他们会同样给你一个这个建议
jdk 1.7 ConcurentHashMap
jdk1.8 ConcurrentHashMap 实现和分析
完全借鉴了1.8HashMap的思想 ,每个绿点是一个node
1.7 源码分析 put get
知道结构图就可以了
1.8 源码分析put get
put 参考之前笔记
get方法
java8 每个节点都是独立的
java7是由segment
为什么选择8转为红黑树?
链表节点的存储空间,小于红黑树的存储节点
源码里面有 注解里面,泊松分布概率计算,8次之后发送碰撞概率极小,为了有更好的效率,就变为红黑树
如果有超过8次,就说明哈西算法有问题。毕竟概率千万份之一,实际过程中,基本不会树化
为什么COncurrentHashMap错误使用,就会是线程不安全?
只保证了 get put 是线程安全,但是运算不是线程安全的。 本身组合操作是不安全的。 只保证同时put 内部数据不错乱
解决方式就是 sync 保护起来
要么就是 使用replace cas 方式,进行组合操作,取出来换回去
putIfAbsent 表示同样的原子组合操作
手写红黑树,很没有意义
CopyOnWriteArrayList
迭代时候会数据过期
初始化方法:
add方法:
get方法,没有什么特别的动作
并发队列
只列出最主要的队列,掌握这些就够了
ConcurrentLinkedQueue 非阻塞队列
Deque 是双端队列,除了头尾都可以操作,其余与Queue一样
阻塞队列看下 有无边界
三组方法
第一组阻塞
第二组抛出异常
第三组,返回false
take put 源码分析:
put方法源码分析,里面的count是原子操作。
prioritBlockingQueue,无界的原因是 可以扩容
因为无线可以开线程,所以就不存储。
如何选择适合自己的队列?
吞吐量就看锁的力度
AQS
HR 就是AQS
比如AQS里面 这种方法 使用unsafe操作
head是拿到锁的线程,后面的是等待拿锁的线程
这个获取方法,和status变量有关
同样依赖于status,但释放方法不会让线程阻塞
协作工具类需要自己去实现获取or释放等重要方法
CountDownLatch 源码分析
构造方法
这里面就设置了 AQS里面的statue
里面其实就是返回AQS里面的status
await方法就是等待,直到倒数结束
doAcquireSharedInterruptibly 这个方法就是让当前线程进入等待队列并且会让他阻塞,大于等于0就不需要等待了,直接放行
addWaiter 会把一个个线程包装成Node节点
parkANdCHeckInteruot 就是让线程阻塞,调用了LockSupport.park方法进行阻塞,其实就是调用了unsafe方法的unpark方法(native方法)进行阻塞
countDown方法
做自旋,主要作用就是status减少1, return nextc==0 说明这个是最关键的倒数,是最后一次倒数,就调用了doReleaseShared()将之前阻塞的方法全部唤醒,就是唤醒队列中的其他线程。
Semaphore 源码分析
这个tryAcquireShared 根据公平核非公平有两种实现,小于0进入排队等待,大于0就代表这次获取成功
先看非公平情况
这个方法不断自选获取许可证的数量 ,所以叫做available,acquires 代表传入的值,代表想获取几个许可证
小于0成立,直接返回remaining ,返回负数,所以就会进行等待队列
如果大于等于0,说明获取许可成功,就自旋更新许可证
ReentranLock
先判断是否是持有锁的线程,
判断冲入次数,c等于0 free状态,将锁的线程持有者,设置为null
如果 tryRelease方法返回的是true,代表锁已经完全释放,则会从队列中唤醒线程去获取锁unparkSuccessor就是去唤醒
根据公平于不公平有两种实现
公平情况下:
线程独有,设置锁的拥有者,如果不成功调用acquire方法
看是否是锁的持有者重入情况,如果是则 setState,return true
要么锁已经被其他线程持有 获取不成功 return false,就包装node 添加入队列。