1.乐观锁 vs 悲观锁 (站在锁预估冲突角度)
悲观锁:
乐观锁:
乐观锁一般情况下都不会出现冲突,如果出现锁竞争较多的情况还是使用悲观锁。
2.重量级锁 vs 轻量级锁(站在开锁开销角度)
⼤量的内核态⽤⼾态切换很容易引发线程的调度
轻量级锁: 加锁机制尽可能不使⽤ mutex, ⽽是尽量在⽤⼾态代码完成. 实在搞不定了, 再使⽤ mutex.
少量的内核态⽤⼾态切换.不太容易引发线程调度.
理解⽤⼾态 vs 内核态
3.⾃旋锁(Spin Lock)VS挂起等待锁
⾃旋锁获取不到锁就会一直等待索德释放而得到锁。挂起等待锁获取不到锁,就会等待系统释放锁,等待系统安排锁给他
优点: 没有放弃 CPU, 不涉及线程阻塞和调度, ⼀旦锁被释放, 就能第⼀时间获取到锁.缺点: 如果锁被其他线程持有的时间⽐较久, 那么就会持续的消耗 CPU 资源. (⽽挂起等待的时候是不消耗 CPU 的).
4. 公平锁 vs ⾮公平锁
公平锁: 遵守 "先来后到". B ⽐ C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
⾮公平锁: 不遵守 "先来后到".A释放锁之后, B 和 C 都有可能获取到锁.
注意:
操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是⾮公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.公平锁和⾮公平锁没有好坏之分, 关键还是看适⽤场景.
synchronized 是⾮公平锁.
5. 可重⼊锁 vs 不可重⼊锁
可重⼊锁的字⾯意思是“可以重新进⼊的锁”,即允许同⼀个线程多次获取同⼀把锁。
理解 "把⾃⼰锁死"⼀个线程没有释放锁, 然后⼜尝试再次加锁.
⽽ Linux 系统提供的 mutex 是不可重⼊锁。
synchronized 是可重⼊锁。
6.读写锁
⼀个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
读写锁的应用场景:
- 两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可.
- 两个线程都要写⼀个数据, 有线程安全问题.
- ⼀个线程读另外⼀个线程写, 也有线程安全问题.
ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁. 这个对象提供了 lock / unlock ⽅法进⾏加锁解锁.ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁. 这个对象也提供了 lock / unlock⽅法进⾏加锁解锁.
Synchronized 不是读写锁.
7. 前面相关⾯试题
1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁.乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据.在访问的同时识别当前的数据是否出现访问冲突.悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待. 乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突. (实现细节参考上⾯的图).
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试 会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
8 .CAS
CAS: 全称Compare and swap,字⾯意思:”⽐较并交换“,⼀个 CAS 涉及到以下操作:
1. ⽐较 A 与 V 是否相等。(⽐较)2. 如果⽐较相等,将 B 写⼊ V。(交换)3. 返回操作是否成功。
CAS 伪代码
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
java 的 CAS 利⽤的的是 unsafe 这个类提供的 CAS 操作;unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;Atomic::cmpxchg 的实现使⽤了汇编的 CAS 操作,并使⽤ cpu 硬件提供的 lock 机制保证其原⼦性。
8.1CAS 应⽤
常见的原子类:
以AtomicInteger为例
addAndGet(int delta); i += delta;decrementAndGet(); --i;getAndDecrement(); i--;incrementAndGet(); ++i;getAndIncrement(); i++;
1) 实现原⼦类
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
atomicInteger.getAndIncrement();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
atomicInteger.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicInteger.get());
}
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作
伪代码实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
2. 线程1 先执⾏ CAS 操作. 由于 oldValue 和 value 的值相同, 直接进⾏对 value 赋值.
CAS 是直接读写内存的, ⽽不是操作寄存器.CAS 的读内存, ⽐较, 写内存操作是⼀条硬件指令, 是原⼦的.
4. 线程2 接下来第⼆次执⾏ CAS, 此时 oldValue 和 value 相同, 于是直接执⾏赋值操作。
5. 线程1 和 线程2 返回各⾃的 oldValue 的值即可
8.2CAS 的 ABA 问题
什么是 ABA 问题
正常的过程
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.如果当前版本号⾼于读到的版本号. 就操作失败(认为数据已经被修改过了).
对⽐理解上⾯的转账例⼦
相关⾯试题
1. ABA问题怎么解决?
9.synchronized 原理的总结:
1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.2. 开始是轻量级锁实现, 如果锁被持有的时间较⻓, 就转换成重量级锁.3. 实现轻量级锁的时候⼤概率⽤到的⾃旋锁策略4. 是⼀种不公平锁5. 是⼀种可重⼊锁6. 不是读写锁
加锁⼯作过程
1) 偏向锁
假设男主是⼀个锁, ⼥主是⼀个线程. 如果只有这⼀个线程来使⽤这个锁, 那么男主⼥主即使不领证结 婚(避免了⾼成本操作), 也可以⼀直幸福的⽣活下去.但是⼥配出现了, 也尝试竞争男主, 此时不管领证结婚这个操作成本多⾼, ⼥主也势必要把这个动作完成了, 让⼥配死⼼.
2) 轻量级锁
通过 CAS 检查并更新⼀块内存 (⽐如 null => 该线程引⽤)如果更新成功, 则认为加锁成功如果更新失败, 则认为锁被占⽤, 继续⾃旋式的等待(并不放弃 CPU).
3) 重量级锁
执⾏加锁操作, 先进⼊内核态.在内核态判定当前锁是否已经被占⽤如果该锁没有占⽤, 则加锁成功, 并切换回⽤⼾态.如果该锁被占⽤, 则加锁失败. 此时线程进⼊锁的等待队列, 挂起. 等待被操作系统唤醒.经历了⼀系列的沧海桑⽥, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁.
其他的优化操作
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
10. JUC(java.util.concurrent) 的常⻅类
Callable 接⼝
代码⽰例: 创建线程计算 1 + 2 + 3 + ... + 1000, 使⽤ Callable 版本
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result);
}
解释:
输出结果:
11. ReentrantLock (相识于Thread)
lock(): 加锁, 如果获取不到锁就死等.trylock(超时时间): 加锁, 如果获取不到锁, 等待⼀定的时间之后就放弃加锁.unlock(): 解锁
11.1ReentrantLock 和 synchronized 的区别:
// ReentrantLock 的构造⽅法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
如何选择使⽤哪个锁?
锁竞争不激烈的时候, 使⽤ synchronized, 效率更⾼, ⾃动释放更⽅便.锁竞争激烈的时候, 使⽤ ReentrantLock, 搭配 trylock 更灵活控制加锁的⾏为, ⽽不是死等.如果需要使⽤公平锁, 使⽤ ReentrantLock。