系列文章目录
文章目录
- 系列文章目录
- 前言
- 一、锁策略
- 1.乐观锁&悲观锁
- 2. 重量级锁&轻量级锁
- 3. 挂起等待锁&自旋锁
- 4.公平锁&非公平锁
- 5. 可重入锁&不可重入锁
- 6.读写锁
- 二、其他优化操作
- 1.锁升级
- 2.锁消除
- 3.锁粗化
- 三、CAS
- 1.CAS是什么
- 2.CAS应用场景
- (1.原子类
- (2. 自旋锁
- 3. CAS中的ABA问题是什么样的
- 四、JUC"面试中"常见类
- 1.callable接口
- 2.reentrantLock
- (1. 区别
- (2.ReentrantLock支持公平锁
- (3. 等待通知机制
- 3.信号量Semaphore
- 4.countdownlatch
- 5.线程安全集合类
- (1.多线程使用哈希表 ,面试常考
- 总结
前言
一、锁策略
1.乐观锁&悲观锁
加锁的时候预测 , 出现锁竞争的概率是大还是小
大概率的话做法更加复杂 ,叫悲观锁
概率小做法简单一点 , 叫乐观锁
2. 重量级锁&轻量级锁
枷锁开销很大 ,重量级锁
加锁开销不大 , 轻量级锁
3. 挂起等待锁&自旋锁
挂起等待锁 : 遇到冲突 , 就把线程阻塞掉 , 等到未来某个时间唤醒 , 涉及到系统内部的线程调度 , 开销非常大 , 重量级锁的典型表现
自旋锁 : 遇到锁冲突 , 先不着急阻塞 , 而是进行重试 , 重视两下 , 所就释放了 , 用户天操作 , 不涉及到内核 , 也不涉及到线程调度 , 轻量级锁典型实现
4.公平锁&非公平锁
先来后到为公平
5. 可重入锁&不可重入锁
一个线程 , 一把锁 , 连续加两次 , 不死锁为可重入锁 , 反之为不可重入锁
可重入锁也称为"递归锁’
6.读写锁
相对为普通互斥锁 , 有两个步骤 , 枷锁和解锁 ,
读写锁有三个步骤 , 加读写锁 , 加写锁 , 解锁
是乐观,也是悲观是重量级,也是轻量级是挂起等待,也是自旋可重入锁非公平锁不是读写锁
二、其他优化操作
1.锁升级
感知不知道 , 干预不了
无锁 , 偏向锁 , 自旋锁 , 重量级锁
无锁 , 没有进入代码块
偏向锁 , 没有真正加锁 , 只是标记 , 如果有其他线程进行竞争 , 被锁升级为自旋锁
自旋锁 , 达到一定条件 , 进入轻量级锁
重量级锁 , 达到一定条件 , 进入重量级锁
2.锁消除
编译器识别到这个逻辑中不需要使用锁 , 但是代码中加锁了 , 尝试把锁去掉
3.锁粗化
实际开发过程中, 使⽤细粒度锁, 是期望释放锁的时候其他线程能使⽤锁.
但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会⾃动把锁粗化, 避免频繁申请释放
锁.
三、CAS
1.CAS是什么
CAS是解决线程安全的另一种思路
compare and swap比较一个内存和一个寄存器的值 , 内容相同 , 把内存和另一个寄存器的值进行交换
自旋锁内部实现通常就是基于CAS实现的
原子类也是
CAS实际上是CPU的一个指令 , 我们并不一定会直接使用到CAS , 但是JAVA提供了一些其它类来封装这个CAS , 很多时候间接用到了
2.CAS应用场景
(1.原子类
底层使用CAS的典型情况 , 标准库提供的一组类
有代码举例
java里, 不支持运算符重载(让各种运算符能够支持对某个类的对象来使用) , 所以不能直接++ , 因此用一个方法来完成后置++
这是CAS典型应用场景 , 除此之外 , 还有自旋锁
(2. 自旋锁
synchronized内部"自旋"就是靠CAS , 使用CAS , 就不必加锁 , 这里理解为不必加一个重量级锁 , 基于CAS实现的是"轻量级锁"
3. CAS中的ABA问题是什么样的
CAS循环检测 , 判定是否出现其他线程穿插了 , 判断依据是检测的值有没有改变 , 这样是不严谨的
ABA问题
恰好在t1出现线程切换的时候 , 切换到t2进行扣款500 , 又恰好在t2扣款后 , 进入t3转账 , 哟恰好t3的转账金额为500 , 切回t1 , 判定内存和寄存器的值相等 , 扣款 , 共两次
为解决这一问题 , 可以引入版本号
四、JUC"面试中"常见类
1.callable接口
类似于Runnable , void run表示任务的 , 没有返回值
但是Collable提供call这样的方法 , 并且带返回值 , 一个泛型参数T这样的类型
代码演示
Runnable需要用成员变量来解决线程间数据交互的问题 , 此处成员变量可以被各种线程获取到 , 使用Callable可以很好的避免上述的成员变量
Thread无法直接接受Callable对象作为参数
此处要搭配FutureTask
2.reentrantLock
可重入锁 , 现在主要用sychronized , 现在讲一下使用和区别
lock和unlock之间可能有return或者throws , 所以用try finally
(1. 区别
ReentrantLock支持tryLock
lock的行为 , 加锁不成就阻塞等待 ,
tryLock的行为加锁不成 , 可以直接返回 , 也支持指定超时时间
(2.ReentrantLock支持公平锁
在new的时候括号后面加一个ture , 此时所内部有维护队列 , 记录哥各个线程加锁的顺序 , 就会按照顺序从队列中取 , 确保拿到的锁的线程就是最早尝试什么申请的线程
默认情况下是非公开状态
(3. 等待通知机制
sychronized搭配wait , notify唤醒的时候只能随机唤醒一个
ReentrantLock搭配Condition类 , 唤醒功能丰富 , 指定唤醒
代码微信
3.信号量Semaphore
信号量就是一个"计数器`" , 描述了一个"可用资源"的个数
就像是停车场 xx个停车位, 车开进去计数-1 , 开出来+1 ,
车开进去叫做申请资源 p操作
车开出来叫做释放资源 V操作
最多减少到0 , 在信号0中 , 如果计数值已经是0 , 继续p操作会发生阻塞
PV操作本质上是操作计数器的值 , 是原子的过程 , 线程安全
再多线程中使用
代码中多次执行结果仍是正确
线程安全问题通过semaphore处理掉了
以上可以看出 , 解决线程安全问题存在很多种方式
- 调整代码结果 , 避免所个线程修改同一个变量
- synchronized
- ReentrantLock
- Swmaphore
- CAS/原子类
4.countdownlatch
线程中可以用到的方法 , 大的任务拆成小的的任务 , 所有小的任务整个任务才算结束
5.线程安全集合类
标准库中的大部分集合类是线程不安全的
(1.多线程使用哈希表 ,面试常考
HashMap线程不安全
Hashable 线程安全 , 不推荐使用 , 给方法加锁 , 也就是整个哈希表共用一把锁 , 针对不同链表进行处理的时候 , 会遇到阻塞 , 而我们需要的是针对同一个链表进行操作时阻塞
ConcurrentHashMap 线程安全 , 推荐使用 ,
优化点 :1. 在每个链表的头结点作为锁对象 , 多个线程竞争不同的锁 , 不会有冲突 , 这种改进方式叫做锁桶
2. size描述键值对个数 , 使用CAS的方式 , 对size进行修改 , 避免加锁
3. 扩容场景 , 原本创建跟大的数组 , 原来hash表的每一个元素都插入到新的哈希表数组中 , 会花费大量时间
不是一次性完成所有元素搬运 , 是分成多次 , 是达到扩容标准之后 ,每一次操作旧数组中的一部分才会搬到新数组里面是 ,也就是他会维护新老两个数据