聊城手机网站建设费用,店铺外卖网站怎么做,wordpress站点安装,昆山便宜做网站前言
本文为 Java 面试小八股#xff0c;一句话#xff0c;理解性记忆#xff0c;不能理解就死背吧。
锁策略
悲观锁与乐观锁
悲观锁和乐观锁是锁的特性#xff0c;并不是特指某个具体的锁。
我们知道在多线程中#xff0c;锁是会被竞争的#xff0c;悲观锁就是指锁…前言
本文为 Java 面试小八股一句话理解性记忆不能理解就死背吧。
锁策略
悲观锁与乐观锁
悲观锁和乐观锁是锁的特性并不是特指某个具体的锁。
我们知道在多线程中锁是会被竞争的悲观锁就是指锁的竞争程度十分激烈很多线程都想用这把锁为了应对这个场景我们会额外做一些工作。例如一把锁此时有几十个线程都想用并且同一时刻它们都发出申请锁的请求这时候锁的竞争程度很高我们可以采取悲观锁的策略额外做一些工作。
乐观锁则相反锁的竞争程度很小就不需要做额外的工作。例如这一把锁只有两个线程在竞争并且这两个线程用锁的概率也不是很高这时候我们可以采用乐观锁策略。
重量级锁与轻量级锁
重量级锁和轻量级锁是遇到特定的场景而出现的解决方案。
上面我们就提到乐观和悲观的场景重量级锁适用于悲观的场景相应的也要付出更高的代价效率相比轻量级锁要低效。
轻量级锁适用于乐观的场景要付出的代价也要小很多效率相比重量级锁要高效。
等待挂起锁与自旋锁
挂起等待锁就是指如果一把锁已经被一个线程占用的时候发现有其他线程还想竞争这把锁操作系统就会让它们阻塞等待后续唤醒的时候需要由操作系统的内核来唤醒。
自旋锁就是指如果发现由锁竞争这时候这些线程不会阻塞等待而是以忙等的形式进行等待。
看到这里其实等待挂起锁是适用于悲观的场景下因为线程竞争激烈没必要让它们占着 CPU 资源直接让它们阻塞释放出CPU 资源减少资源的消耗。同时由于唤醒的时候是由操作系统的内核实现的所以操作系统会在内核态和用户态频繁切换效率也会比较低下。
而自旋锁则适用于乐观的场景线程以忙等的形式也就是占用着CPU但是由于锁竞争不是很激烈忙等的线程很快就可以获取到锁所以没必要阻塞等待因为操作系统的内核唤醒线程的效率要低效一些所以自旋锁的效率会比等待挂起锁的效率要高。
互斥锁与读写锁
互斥锁就是加锁之后拥有这把锁的线程才能进行操作其他线程必须等待拿到锁之后才能进行自己的操作这就是互斥。
读写锁分为读锁、写锁因为我们知道线程安全问题是因为写操作而引起的但是读操作是不会发生线程安全问题的而读写锁就是针对读操作和写操作进行加锁读锁和读锁是不会互斥的写锁与读锁是会互斥的写锁与写锁是会互斥的这里可以参考MySQL的幻读、脏读、不可重复读了
公平锁与非公平锁
公平锁是指锁的分配是按线程的等待时长来分配的举个例子假设一把锁已经被一个线程占用此时有三个线程都想竞争这把锁那这时候我们会使用额外的数据结构来保存这些线程并且记录每个线程的等待时长等到锁被释放的时候操作系统会优先把这把锁分配给等待时长最长的线程这也避免了线程饥饿。
非公平锁就是随机分配不按 “先来先得” 的规矩
可重入锁与不可重入锁
可重入锁就是当一个线程拥有这把锁的时候可以进行重复的加锁。
不可重入锁则相反即使你这个线程拥有了这把锁但是还是不能对其进行重复加锁。
synchronized 的优化
根据上面的锁策略我们来总结一下 synchronized 的特性 synchronized 具有自适应性 synchronzied 开始时是采取乐观锁策略如果锁的冲突频繁则转换为悲观锁 开始时是轻量级锁如果锁冲突频繁则转换为重量级锁 synchronized 实现轻量级锁的时候采用自旋锁策略 synchronized 是不公平锁可重入锁互斥锁不是读写锁 锁升级
JVM 会将 synchronized 的锁分为四个状态无锁、偏向锁、自旋锁、重量级锁。 当还没进入synchronized 的时候处于无锁状态一旦进入 synchronized 代码块就会变成偏向锁偏向锁并非真正加锁而是通过标记的方式以此来区分是否真正加锁了。偏向锁本质上相当于 “延迟加锁”能不加锁就不加锁避免了不必要的加锁开销这也是一种懒汉模式的体现。
一旦产生锁竞争偏向锁就会升级为自旋锁也就是轻量级锁如果竞争十分激烈进一步升级为重量级锁。 synchronized 只能进行锁升级但是不能进行锁降级 JVM 与编译器的锁优化
锁消除
JVM 会自动检测出一些没有必要加锁的操作避免这些无意义的加锁操作带来的不必要的开销JVM 会把这些锁给消除也就是说你代码加锁了但是 JVM 给删除了。
大家不用担心这个优化会产生线程安全问题因为 JVM 的锁消除是在100% 确定这个锁就是一个没必要加的锁JVM 才会进行锁消除。
锁粗化
首先介绍一个概念锁的粒度加锁与解锁之间包含的代码指令越多锁就越粗相反加锁与解锁之间包含的代码指令越少锁就越细。
public class Test {public static int sum 0;public static int count 10000;public static int total 1000;public static void main(String[] args) {Object locker new Object();Thread t new Thread(() - {for(int i 0; i 5000; i) {synchronized (locker) {sum;}synchronized (locker) {count--;}synchronized (locker) {total--;}}});}
}上面的代码就属于锁的粒度太细了频繁加锁解锁。 Thread t2 new Thread(() - {for(int i 0; i 5000; i) {synchronized (locker) {sum;count--;total--;}}});这个代码就是锁的粒度粗加锁和解锁的次数比较少。
⼀段逻辑中如果出现多次加锁解锁,编译器 和 JVM会自动进行锁的粗化。
ReentrantLock
ReentrantLock 和 synchronized 是并列关系都是用来加锁的并且都是可重入锁。
简单使用介绍ReentrantLock 使用 lock() 加锁unlock() 来解锁为了避免我们因为加锁和解锁之间有return 或者 抛出异常等等情形没能进入解锁操作所以这里使用 finally 来包含 unlock() 代码行避免忘记解锁。 ReentrantLock locker2 new ReentrantLock();Thread t3 new Thread(() - {try {locker2.lock();count;} finally {locker2.unlock();} });synchroinzed 和 ReentrantLock 的区别 synchronized 是 Java提供的关键字是 JVM 内部通过 C 实现的ReentrantLock 是Java标准库提供的类由Java代码实现 synchronized 是 通过代码块来实现加锁和解锁的ReentrantLock 通过 lock() 加锁unlock() 解锁一定要注意 unlock() 可能存在未被调用的情况。 ReentrantLock 还有一个 tryLock() 这个方法的调用不会线程产生阻塞如果加锁成功则返回 true加锁失败则返回 false接下来由调用者来根据返回值决定接下来怎么做。可以设置超时时间当等待时间达到超时时间的时候再返回true / false ReentrantLock 提供了公平锁的实现ReentrantLock locker new ReentrantLock(true);默认情况下是非公平锁。 ReentrantLock 搭配的通知等待机制是由Condition 类实现的相比于 synchronized 的 wait / notify 的功能更强大一些。 synchronized 和 ReentrantLock 都是可重入的互斥锁。 CAS CAS 全称是 Compare and swap比较并交换 CAS 在 CPU 里是一条指令具有原子性。 因此 CAS 操作是线程安全的 举个例子假设内存原始数据为 V把这个数据放入寄存器 1 和 寄存器 2 中数据的加减等操作的结果由寄存器 2 保存。CAS 会先检测原始数据 V 和寄存器 1 的数值是否一致如果一致的话可以执行修改也就是把寄存器 2 的结果放入内存中。
下面给出 CAS 的伪代码进行进一步的理解
boolean CAS(address, expectValue, swapValue) {if (address expectedValue) {address swapValue;return true;}return false;
}第一个参数是内存的数值第二个参数是寄存器 1 的数值第三个参数是寄存器 2 的数值。
首先判断内存的数值是否和寄存器的数值一致如果一致则进行寄存器 2 和内存数值的交换操作注意 这本质上在 CPU 里是一条指令具有原子性。 明确的指明if-else 和 三目运算符在 CPU 里不是一条指令和 CAS 还是由区别的。 原子类
CPU 有 CAS 指令并且给操作系统提供了 CAS 的使用接口操作系统对 CAS 进一步封装给用户提供相应的接口C 可以直接进行调用而JVM 是由 C 实现的再次对 CAS 进行封装给Java 程序员提供了 原子类。在这个 java.util.concurrent.atomic 包下就是我们的原子类了。 下面是原子类的伪代码
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue value;while ( CAS(value, oldValue, oldValue1) ! true) {oldValue value;}return oldValue;}
}oldValue 是寄存器由于Java没有寄存器的使用所以这里用 int 类型代替。
getAndIncrement() 其实就是 自增的操作首先先把内存的数值value读到寄存器 1 中CAS 指令 首先判断 value 是否和寄存器 1 中的数值 oldValue 相等如果相等就把寄存器 2 的 oldValue 1 的结果放到内存中返回 true否则返回 false 并且进入循环体再次读取内存的数值放入寄存器 1 中。 面对多线程下同时修改一个变量的时候原子类是最佳的选择。
import java.util.concurrent.atomic.AtomicInteger;public class Demo1 {private static AtomicInteger count new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() - {for (int i 0; i 50000; i) {count.getAndIncrement();}});Thread t2 new Thread(() - {for (int i 0; i 50000; i) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count count.get());}
}使用 CAS 实现自旋锁
下面是 使用 CAS 实现自旋锁的伪代码
public class SpinLock {private Thread owner null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就⾃旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner null;}
}核心代码while(!CAS(this.owner, null, Thread.currentThread())){} 当 这个锁的拥有者 为 null 的时候才能由线程Thread.currentThread() 获取这把锁的操作并返回true否则该线程以忙等的形式等待这把锁。 ABA 问题
ABA 问题是什么 我们知道 CAS 开始之前会先把内存的数值读到寄存器里在进行 CAS 的操作之前可能调度过来别的线程这个线程对这个内存的数值进行了修改操作然后又改回来了看上去这个数值没有任何变化实际上这个数据已经被动过了接着把 CAS 调度过来执行CAS 首先判定内存的数值是否和寄存器的数值一致如果一致进行交换操作这时候数值肯定是一致的所以交换操作正常被执行了。在进行内存数值和寄存器数值判定是否相等之前内存数值是否被改了又改过这就是 ABA 问题。
ABA 问题会带来什么BUG 假设一个人叫做白糖过来取500块钱假设余额有 4k这时候 ATM 机有点卡顿这时候白糖进行了多次按下取款的操作恰好这时候白糖的好朋友天王星发个信息说之前欠你的500块现在转账还你。 由于多次按下取款操作就会产生多个取款的线程来执行取款操作此时中间夹了一个还款操作的线程大家来看一下下面的流程图 取款线程 t1 把 account 修改为 3500 还款线程将 account 修改为 4000 接着又来了 取款线程 t3 由于内存4000 和寄存器的数值保留的 4000 是一致所以又将余额修改为了 3500你会发现白糖小伙就拿出了 500 块但是余额却多扣了 500 完了血亏 500可怜的白糖又要辛苦打工了。
这种事件虽然发生概率极小但是在庞大的请求数量面前还是不能忽视这个 bug 的。 如何解决 ABA 问题 因为余额是可以加又可以减的变量所以会出现上述极端的BUG但是如果我们换一个指标来作为判断标准的话就可以避免上述的BUG这里我们可以使用版本号来作为判断的指标每次修改之后版本号就 1每次进行修改操作的时候判断内存的版本号和寄存器的版本号是否相同 下面给一个伪代码 int oldVersion version;if(CAS(version, oldVersion, oldVersion 1)) {account 500;}