)
1. 常见锁策略用锁前需了解锁1.1 乐观锁vs悲观锁悲观锁总是假设最坏的情况每次去拿数据的时候都认为别⼈会修改所以每次在拿数据的时候都会上锁 这样别⼈想拿这个数据就会阻塞直到它拿到锁。其他线程想要访问就阻塞等待从根源上防止冲突。如synchronized、ReentrantLock等。乐观锁假设数据⼀般情况下不会产⽣并发冲突所以在数据进⾏提交更新的时候才会正式对数据是否产⽣ 并发冲突进⾏检测如果发现并发冲突了则让返回⽤⼾错误的信息让用户决定如何去做。如CAS、数据库版本号机制。1.2 重量级锁vs轻量级锁重量级锁依赖操作系统互斥锁(mutex)竞争失败线程直接阻塞。轻量级锁下线程自旋多次抢不到锁就会升级为重量级锁。轻量级锁加锁机制尽可能不使⽤mutex而是尽量在用户态代码完成。实在搞不定了再使⽤mutex。偏向锁发生竞争锁还没有释放 → 撤销偏向锁升级为轻量级锁。轻量级锁重量级锁底层机制CAS 自旋用户态操作系统内核互斥锁竞争失败行为线程自旋空转线程阻塞挂起CPU 开销自旋消耗 CPU几乎不耗 CPU开销来源CPU 空循环线程上下文切换适用场景竞争少锁持有时间短竞争激烈锁持有时间长通俗比喻轻量级锁原地转圈等。别人拿着锁你不睡觉不停尝试抢如果对方很快用完你直接拿到如果对方迟迟不放你一直转圈很累耗 CPU。重量级锁坐下睡觉等。抢不到就直接睡觉放弃 CPU别人用完锁再叫醒你睡觉不费力气但是叫醒、睡觉这个动作本身代价很高上下文切换。背诵短句轻量级锁CAS 自旋不阻塞消耗 CPU适合竞争少、锁时间短重量级锁操作系统阻塞线程有上下文切换开销适合竞争激烈、锁时间长。1.3 自旋锁自旋锁线程获取锁失败时不把自己阻塞挂起循环不停重试抢锁直到拿到锁。线程不会放弃 CPU一直在 CPU 上跑循环。特点抢不到锁不阻塞原地循环 CAS。没有线程阻塞、没有操作系统上下文切换。会占用 CPU一直空转消耗 CPU 资源。伪代码while (抢锁(lock) 失败) {}优点线程不会放弃 CPU不发生线程阻塞、操作系统调度切换。锁一旦释放线程可以立刻获取锁响应快。缺点若锁被占用时间较长线程持续空转会大量消耗 CPU而阻塞挂起的线程几乎不消耗 CPU。1.4 公平锁vs非公平锁公平锁线程按照申请锁的先后顺序排队先到先得不允许插队。非公平锁允许新来的线程直接尝试抢锁可以插队不一定按排队顺序获取。注意由于操作系统内部的线程调度是随机的正常情况可以视为非公平锁。1.5 可重入锁vs不可重入锁可重入锁允许一个线程多次获取同一把锁不会阻塞自己也叫递归锁如reentranlock和synchronized。不可重入锁简单来说就是把自己锁死如果一个线程没有释放锁反而再次申请锁就会锁死自己操作系统的mutex就是不可重入锁。1.6 读写锁1读与读之间不会有线程安全问题直接并发读取。2读与写、写与写之间存在线程安全问题需要进行互斥。Java标准库提供ReentrantReadWriteLock类来使用读写锁ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁ReentrantReadWriteLock.WriteLock 类表示⼀个写锁这两个对象都提供了lock和unlock进行加锁解锁。代码package practice.thread; import java.util.concurrent.locks.ReentrantReadWriteLock; class RW{ // 共享数据 public static int data 0; // 读写锁对象 private static ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); // 读锁 private static ReentrantReadWriteLock.ReadLock readLock rwLock.readLock(); // 写锁 private static ReentrantReadWriteLock.WriteLock writeLock rwLock.writeLock(); public RW(){ } //读操作 public void read(){ readLock.lock(); try { // 读数据 System.out.println(当前线程Thread.currentThread().getName()执行读操作,data为: data); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } finally { // 需要手动解锁(注意) readLock.unlock(); } } //写操作 public void write(int val){ writeLock.lock(); try { data val; System.out.println(当前线程Thread.currentThread().getName()已执行写操作,data为:data); Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); }finally{ writeLock.unlock(); } } } public class Demo25 { public static void main(String[] args) throws Exception{ RW rwnew RW(); new Thread(()-{ rw.read(); },读1).start(); new Thread(() - { rw.read(); }, 读2).start(); new Thread(()-{ rw.write(10); },写1).start(); new Thread(() - { rw.write(20); }, 写2).start(); } }结果2. CAS2.1 CAS是什么CAS 全称 Compare And Swap比较并交换是硬件 CPU 提供的原子指令是乐观锁的底层实现基础。CAS 包含 3 个操作数V内存中变量实际值A预期旧值B准备写入的新值执行逻辑比较内存真实值V与预期值A是否相等如果相等就把V更新为新值B返回成功如果不相等说明已经被其他线程修改直接返回失败不做修改伪代码boolean CAS(address, expectValue, swapValue){ if(getMemory(address) expectValue){ setMemory(address,swapValue); return true; } return false; }注意比较 交换这整套动作是一条 CPU 原子指令不会被线程打断。Java 通过Unsafe类封装 CAS 操作。2.2 CAS 的应用1. 实现原子类java.util.concurrent.atomic包下面的原子类AtomicInteger底层基于 CAS 实现不需要加锁保证原子性。getAndIncrement()自增伪代码public int getAndIncrement(){ int oldValue value; while(!CAS(value,oldValue,oldValue1)){ oldValue value; } return oldValue; }循环执行 CAS直到修改成功。2. 实现自旋锁循环 CAS 尝试抢占锁抢不到就不断重试。2.3 CAS 存在的问题 — ABA 问题1. 什么是 ABA线程 1 读取变量值为 A准备执行 CAS 修改在线程 1 执行 CAS 之前线程 2 把变量 A 修改为 B又改回 A。此时内存值依旧是 ACAS 校验会判定没有发生修改执行更新但数据中间已经被篡改过产生逻辑错误。举例账户余额 100线程 A 要扣款 50中途线程 B 把余额改成 50又转账改回 100此时 A 的 CAS 依旧执行扣款业务出错。2. ABA 解决方案引入版本号时间戳每次修改版本号自增CAS 同时比较值 版本号。Java 提供AtomicStampedReference内部维护数据 版本戳专门解决 ABA 问题。2.4 CAS 其他缺点竞争激烈时循环自旋大量消耗 CPU。只能保证单个变量的原子操作不能同时操作多个共享变量。3. synchronized原理synchronized 是 JVM 实现的锁锁只能升级不能降级。锁状态流转无锁 → 偏向锁 → 轻量级锁 → 重量级锁。3.1 synchronized 整体特性锁竞争少使用乐观策略冲突频繁切换悲观锁锁状态逐级升级轻量级锁使用自适应自旋非公平锁、可重入锁、不是读写锁。3.2 偏向锁偏向锁的目的单线程无竞争场景尽可能消除 CAS 加锁开销。第一个线程获取锁对象头 Mark Word 记录该线程 ID标记偏向锁。同一个线程再次进入同步块直接判断线程 ID不需要 CAS 操作。释放锁的时候不会清除偏向标记。标记存在不等于锁被占用。当出现其他线程来竞争锁立刻撤销偏向锁升级为轻量级锁。注意偏向锁只是对象头的标记不是线程状态。同一个对象偏向锁只能记录一个线程 ID。3.3 轻量级锁触发时机偏向锁发生竞争锁还没有被释放撤销偏向锁升级轻量级锁。底层基于 CAS 自适应自旋锁工作在用户态尽量不调用操作系统互斥锁 mutex。竞争锁的线程不阻塞循环 CAS 自旋尝试抢锁。JVM 自适应调整自旋次数如果自旋多次依旧获取不到锁升级重量级锁。适用场景竞争不激烈锁持有时间很短。优点没有内核态切换缺点自旋消耗 CPU。3.4 重量级锁触发时机轻量级锁自旋多次抢锁失败膨胀为重量级锁。依赖操作系统内核提供的mutex互斥锁获取锁失败的线程停止自旋被操作系统挂起进入阻塞队列锁释放后操作系统唤醒等待的线程重新竞争锁。存在用户态↔内核态切换线程上下文切换开销大适合锁持有时间长、竞争激烈。3.5 JVM 锁优化synchronized 额外优化锁消除JVM 编译器检测到锁对象不会发生多线程竞争直接把加锁解锁代码消除。例如单线程环境下StringBuffer的synchronized方法锁直接消除减少开销。锁粗化如果一段代码频繁多次加锁、释放锁JVM 会把多次加解锁合并成一次大粒度锁减少频繁锁操作开销。锁的粒度加锁和解锁范围代码越多粒度越粗返回越细// t1粗粒度锁把整个for循环全部包进synchronized Thread t1 new Thread(() - { synchronized (locker) { for (int i 0; i 50000; i) { count; } } }); // t2细粒度锁只把count这一行包进synchronized循环在锁外面 Thread t2 new Thread(() - { for (int i 0; i 50000; i) { synchronized (locker) { count; } } });4. 面试背诵小结CASCPU 硬件原子指令比较并交换乐观锁实现存在 ABA 问题使用版本号AtomicStampedReference解决。synchronized 锁升级无锁→偏向锁单线程无竞争→轻量级锁CAS 自旋→重量级锁操作系统阻塞锁只能升级不能降级。自旋锁抢锁失败循环重试耗 CPU无上下文切换适合锁持有时间短。悲观锁上来就上锁乐观锁不加锁更新校验冲突。