基础概念
什么是进程 , 什么是线程 ?
进程是一个服务 也就是一个程序的动态表现 线程是进程中的最小执行单元
创建线程的方式
-
从Thread类继承
-
实现 Runnable接口
线程的sleep/yeild/join
sleep : Thread#sleep() 不会释放锁 但是 Object#wait()会释放锁 睡眠结束完后 也会回到就绪队列
yeild : 本线程进入等待队列 , 让出一下CPU
join : 让本线程 跑到被调用者线程中去运行 且要在调用线程执行完后 运行
线程的interrupt
线程中断获取响应
synchronized
同步关键字
hospot 实现 : 锁的应该是对象头 一个对象头有32/64位具体看JVM的位数 其中两位 用于标志锁
锁升级 : 无锁 - > 偏向锁 - > 轻量级锁(自旋锁 默认旋10次 如果还得不到锁 就升级) -> 重量级锁(操作系统锁 进入等待队列)
什么时候用自旋锁 ? 什么时候用重量级锁 ?
线程数量少 并且锁定的业务代码运行时间短 就用自旋锁
线程数多 并且等待时间长的 就用重量级锁
可重入
程序执行出现异常时 默认情况下 锁是会被释放的
同步方法和非同步方法 是可以同时执行的
当做锁的对象一定要加final 并且不能用String作为锁对象
volatile
一致性 , 可见性 ( 保证线程可见性 )
底层使用MESI(CPU缓存一致性协议)
保证Jvm指令不会被重排序( 禁止指令重排序 )
底层使用cpu读写屏障来完成的 loadFence storeFence (CPU原语)
volatile修饰引用对象其实是没有什么意义的 引用对象内的数据改变了 其他线程是没法看见的
CPU缓存一致性协议(MESI)
现代CPU的缓存一致性是通过缓存锁 + 总线锁实现
CPU缓存一共有三层
-
L1 速度非常快 存的数据也很少 本人电脑中 只有256k
-
L2 速度慢于L1 本人电脑中 只有1M L1 和L2 是CPU独享的 在CPU内部
-
L3 在主板上 CPU共享 在本人电脑中 只有6M
缓存行
为了提高效率 读取数据的时候 会一次性读取缓存行大小的数据 一般为 64字节
伪共享
同一缓存行的两个不同数据, 被两个不同的CPU锁定 产生互相影响的伪共享问题
解决方法 : 缓存行对齐
CAS(自旋)
各种Atomic类就是使用了CAS保证线程安全的
ABA问题 如果是基础数据类型无所谓 但如果是引用类型 就可能有问题了
解决 : 加一个版本号 一起判断 就OK
所有的CAS 都是使用Unsafe类
JUC同步工具
各种类型锁
ReentrantLock 可重入锁
底层使用的是CAS
加锁时 Lock.lock() 最后需要手动解锁
锁 | synchronized | ReentrantLock |
---|---|---|
重入 | 可重入 | 可重入 |
解锁 | 可自动解锁 | 需要手动解锁 |
维护队列 | 只有一个队列 | 可有多个队列 (Condition) |
可尝试获取锁 tryLock() 不管有没有获取都立即返回 | ||
公平锁 | 只有非公平锁 | 有公平与非公平的切换 |
ReentrantLock.unlock() -> 调用 AQS.release(int args)
AQS.release() -> AQS.tryRelease(int arg)调用具体的实现类 -> ReentrantLock.Syn.tryRelease(int arg) 该方法中主要做了以下几件事
1 : int c = getState() - releases 获取state 并且减 arg
2 : 判断c == 0 ? setExclusiveOwnerThread为空 即无线程获取锁
3 : setState(c); 将state值更新
CountDownLatch 门闩
await() 线程阻塞
countDown() 减数 当门闩减到0的时候线程会继续运行
CyclicBarrier 循环栅栏
初始化时 可传入两个参数 ( 数量, 当满了时调用的Runnable)
Phaser 阶段
ReadWriteLock 读写锁
其实也就是共享锁和排他锁
Semaphore 信号灯
可用于限流
Exchanger 交换器
用于两线程之间数据交换 调用exchange() 阻塞 等待第二方调用exchange()时 都会放开阻塞
LockSupport
底层使用的是UnSafe实现的
wait/notify/notifyAll
wait()会释放锁
看Object源码得知
fail-fast or fail-safe
fail-fast(快速失败)
在使用迭代器对集合对象进行遍历的时候,如果A线程对集合进行遍历,正好B线程对集合进行修改(增加、删除、修改)则A线程会抛出ConcurrentModificationException异常。
java.util 下的集合都是快速失败的 不支持并发修改
fail-safe(安全失败)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception,例如CopyOnWriteArrayList。
java.util.concurrent 下的集合都是安全失败的 支持并发修改
同步容器
ConcurrentHashMap
ConcurrentSkipListMap
底层实现使用了跳表
CopyOnWriteArrayList
写时复制 当需要添加元素的时候 复制一份原数组 然后操作复制出来的 最后将修改后的数据 set回去
其主要用在读比较多 写比较少的情况下
写操作是线程安全的 读的时候没有加锁
CopyOnWriteArraySet
Queue
Queue | ||
---|---|---|
insert | offer 如果插入没有成功 就会返回false | add 如果插入没有成功 就会抛异常 |
remove | poll 如果队列没有元素了 就会返回null | remove 如果队列没有元素了就会抛异常 |
Examine | element 只是检索队列 不改变队列 为空返回异常 | peek 只检索队列 不改变队列 为空返回null |
BlockingQueue 阻塞队列
BlokingQueue | |
---|---|
insert | put 如果队列满了 会阻塞 最大值为Integer.MAX_VALUE |
remove | take 如果队列空了 会阻塞 |
priorityQueue 优先队列
内部实现了一个排序 内部是一颗树 二叉小顶堆实现
最小堆
是一颗完全二叉树 其中 父节点一定小于叶子节点
leftNodeNo = parentNo * 2 +1
rightNodeNo = parentNo * 2 + 2
parentNodeNo = (nodeNo - 1) /2
插入节点的时候 算出节点下标 然后逐层比较 直到>= 父节点时
链接 : https://www.cnblogs.com/CarpenterLee/p/5488070.html
DelayQueue
按时间去排序的Queue 按理说 这已经打破了queue的规范了 队列存储的顺序 并不是插入顺序 而是比较后的顺序
一般用来 按时间进行任务调度
SynchronousQueue
这个queue 容量是0的
用来两个线程间传递数据的
TransferQueue
transfer(E e) 该方法 只有被其他线程消费了 才会继续往下走 否则会一直阻塞在那
Queue 和 List的区别 :
Queue添加了很多对线程友好的api 比如 offer poll peek
BlokingQueue 在Queue的基础上 又添加了一些api 比如 put take
put take 实现了阻塞 天然的实现了生产者消费者模型
边边角角
PriorityQueue 优先队列
扩容机制 : 如果oldCapacity < 64 ? 扩容两倍 + 2: 扩容50%
初始容量 : 11
ThreadLocal
每个Thread中都维护了一个ThreadLocal->ThreadLocalMap
ThreadLocal 有用在Spring的声明式事务 比如 将数据库连接放入ThreadLocal中
只要是一个事务中的 都是同一个数据库连接
对象四种引用 强/软/弱/虚
强引用
只要引用指向对象 对象就不会被回收
软引用 SoftReference
当一个对象只有一个软引用指向的时候 当系统内存不够用时 才会被GC回收
弱引用 WeakReference
当一个对象只有一个虚引用指向时 只要发生了GC 就会被GC回收
一般用在容器里
WeakHashMap
使用了弱引用
WeakHashMap中的Entry<K,V> K 是使用了弱引用
如果发生gc WeakHashMap中的key会被干掉
每次put的时候 都会清理一遍WeakHashMap
WeakHashMap 的默认初始容量 16 负载因子 0.75f 每次扩容为原本的两倍
ThreadLocal
Thread中维护着一个ThreadLocalMap
ThreadLocalMap中有一个Entry<K,V>
Entry继承WeakReference
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
内部 : ThreadLocal 被一个弱引用指向着
外面 : 如果是直接new出来 被一个强引用指向 如果外面的强引用断开了
那么每当发生gc时 ThreadLocal就会被干掉
如果内部不用弱引用指向 那么会造成内存泄漏 ThreadLocal 永远不会被回收
但是 那也只是回收了Key value指向的对象 依旧存在
所以 使用完ThreadLocal后 必须要调用remove()方法
虚引用 PhantomReference
用于管理堆外内存的
new PhantomReference(引用 , 引用队列);
给写JVM的人用的
当与引用指向的对象被回收后 会在Queue中存放进一个对象
线程池
Callable
相当于有返回值的Runnable
Future
存储执行的将来才会产生的结果
get方法是阻塞的
FutureTask
Futrue + Runnable
Executor
主要做线程的执行接口
把线程的定义与线程的执行分开
ExecutorService
线程池的父接口
里面定义了一些生命周期方法和提交任务方法
submit方法是异步调用的
CompletableFuture
管理多个Future的结果
ThreadPoolExecutor
线程池的七个参数
corePoolSize
核心线程数
maximumPoolSize
最大线程数
keepAliveTime
当线程的数量大于内核时,这是多余的空闲线程在终止之前等待新任务的最大时间。
unit
时间单位
workQueue
任务队列
threadFactory
线程工厂
handler
拒绝策略 jdk一共提供了四种拒绝策略
1 : AbortPolicy 抛异常
2 : CallerRunsPolicy 在调用者的线程中执行任务
3 : DiscardOldestPolicy 丢弃队列头部的任务(其实也是等待最久的任务) 并且将当前任务尝试处理
4 : DiscardPolicy 不处理 抛弃掉
当执行被阻塞时使用的处理策略,因为达到了线程边界和队列容量
执行流程
加入一个任务 先看核心线程 如果和心线程没满 起核心线程
如果核心线程满了 查看任务队列 如果任务队列没满 加入任务队列
如果任务队列满了 起一个新线程去处理
如果达到了最大线程数 并且 任务队列也满了 那么启动拒绝策略
最大处理任务数
最大线程数 + 任务队列长度
ForkJoinPool
分叉出子任务执行 然后最后做汇总
分解汇合任务
用很少的线程可以执行很多的任务(子任务) ThreadPoolExecutor做不到先执行子任务
CPU密集型