【JUC第二章下】:锁机制关键字 你好我是fengxin_rou这是我的个人主页fengxin_rou的主页❄️欢迎查看我的专栏我的专栏《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWebAI的talis学习系统》、《苍穹外卖》目录前言synchronized 修饰普通方法、静态方法、代码块锁的对象分别是谁1. synchronized 修饰 普通方法2. synchronized 修饰 静态方法3. synchronized 修饰 代码块volatile关键字的作用是什么1.保证对所有线程保持可见性2.保证volatile前后指令不会重排序为什么volatile不能用来保证原子性什么是可重入锁为什么需要可重入什么是公平锁、非公平锁优缺点非公平锁吞吐量为什么比公平锁大悲观锁和乐观锁区别、适用场景悲观锁适用场景乐观锁适用场景CAS 原理是什么自旋、Unsafe 类作用CAS 三大问题ABA、循环耗时、只能保证单个变量原子性怎么解决什么是自旋锁优缺点什么是死锁死锁的产生条件是什么什么是偏向锁撤销、重偏向批量重偏向高频考点轻量级锁自旋次数自适应什么是锁粗化、锁消除JVM 优化手段1.锁消除2.锁粗化前言前面一篇【JUC第二章上】锁机制关键字我们初步学习了JUC锁相关的知识这一章对起里面的关键字做出一些补充synchronized 修饰普通方法、静态方法、代码块锁的对象分别是谁一句话总结普通方法锁实例 静态方法锁类模板 代码块锁括号里。1. synchronized 修饰普通方法锁的是调用这个方法的对象thispublic synchronized void method() { }锁对象this场景多个线程操作同一个对象时会互斥不同对象之间互不影响2. synchronized 修饰静态方法锁的是类的 Class 对象public static synchronized void method() { }锁对象类名.class场景全局锁整个类只有一把锁不管多少个对象全部互斥3. synchronized 修饰代码块锁的是括号里的对象synchronized(锁对象) { }锁对象你自己指定this、class、自定义对象都可以最灵活推荐使用注意普通方法锁this和静态方法锁class是两把完全不同的锁它们之间不会互斥可以同时执行volatile关键字的作用是什么1.保证对所有线程保持可见性当一个变量被声明为volatile变量时这个变量就会进入主存主存是对所有线程可见的只要一修改该变量那么其他线程都可以看到2.保证volatile前后指令不会重排序volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序JMM实际定义了4种内存屏障StoreStore(写写屏障)、StoreLoad屏障(写读屏障)、LoadLoad(读读屏障)、LoadStore(读写屏障)1)在vloatile之前插入StoreStore屏障可以防止volatile之前的普通写被重排序到volatile写之后2在 volatile 写操作之后插入StoreLoad屏障防止 volatile 写与后面可能出现的 volatile 读/写发生重排序这是开销最大的屏障。3在 volatile 读操作之后插入LoadLoad屏障防止 volatile 读与后面的普通读发生重排序。4在 volatile 读操作之后插入LoadStore屏障防止 volatile 读与后面的普通写发生重排序。为什么volatile不能用来保证原子性volatile 的核心作用只有两个保证变量的可见性禁止指令重排序但是 volatile 完全不具备 “原子性保障” 的能力。像i这种操作不是一条指令而是三步操作的组合读取主内存的值执行 1 计算写回主内存这三步是可被打断的。volatile 没有任何机制能把这三步捆绑成一个不可分割的整体。所以 volatile不能保证原子性不能解决多线程下的并发安全问题。什么是可重入锁为什么需要可重入可重入锁是指同一个线程可以多次持有锁并且不会造成死锁void a() { lock(); // 第一次拿锁 → 计数1 b(); // 里面又 lock() } void b() { lock(); // 同一个线程再次拿锁 → 计数2 unlock(); }这里的计数器是属于锁的不是方法的。所以在同一个线程里能够持续计数计数器的规则是拿到锁计数1失去锁-1直到重新回到0该线程才是完全释放锁为什么需要可重入为了避免同一个线程调用嵌套加锁方法时自己把自己锁死也就是产生死锁例子 java 运行 void a() { lock(); b(); // 内部又 lock() } void b() { lock(); }如果锁不可重入线程进入 a () → 获取锁调用 b () → 再次请求锁发现锁被自己持有 → 阻塞等待自己释放自己等自己 → 死锁什么是公平锁、非公平锁优缺点公平锁是指多个线程按照申请锁的顺序来获取锁即排在第一个的线程才能获取锁优点每一个线程一段时间后都能获取锁各个线程平等缺点整体执行速度慢吞吐量小非公平锁是指多个线程同时竞争锁抢到的锁的线程才能执行抢不到的去等待队尾等待优点是整体执行效率高吞吐量大缺点可能产生线程饥饿问题如果有线程一直插队那么一个队列可能很久都不能执行非公平锁吞吐量为什么比公平锁大公平锁获取锁时先将线程自己添加到等待队列的队尾并休眠当某线程用完锁之后会去唤醒等待队列中队首的线程尝试去获取锁锁的使用顺序也就是队列中的先后顺序在整个过程中线程会从运行状态切换到休眠状态再从休眠状态恢复成运行状态但线程每次休眠和恢复都需要从用户态转换成内核态而这个状态的转换是比较慢的所以公平锁的执行速度会比较慢。非公平锁当线程获取锁时会先通过 CAS 尝试获取锁如果获取成功就直接拥有锁如果获取锁失败才会进入等待队列等待下次尝试获取锁。这样做的好处是获取锁不用遵循先到先得的规则从而避免了线程休眠和恢复的操作这样就加速了程序的执行效率悲观锁和乐观锁区别、适用场景乐观锁 对于并发间操作产生的线程安全问题持乐观状态乐观锁认为竞争不总是会发生因此它不需要持有锁将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量如果失败则表示发生冲突那么就应该有相应的重试逻辑。悲观锁 对于并发间操作产生的线程安全问题持悲观状态悲观锁认为竞争总是会发生因此每次对某资源进行操作时都会持有一个独占的锁就像 synchronized直接上了锁就操作资源了。乐观锁不上锁写入内存失败解决是重试悲观锁用上锁的方法防止写入失败悲观锁适用场景写操作多并发竞争激烈要求数据强一致性例库存扣减、交易、金融数据乐观锁适用场景读操作多高并发、冲突少追求高性能例商品详情、缓存更新、计数服务CAS 原理是什么自旋、Unsafe 类作用CAS是一种乐观锁机制内存位置的值(V)预期的值(A)和新值(B)如果内存位置的值V和预期的值A一致那么久将V替换成B这一操作是原子性的通常由硬件指令执行如在现代处理器上cmpxchg指令可以实现 CAS 操作。自旋是竞争锁失败后不进入阻塞而是进入重试CAS直到拿到锁在轻量锁时会触发作用避免线程阻塞带来的内核态 / 用户态切换开销提升性能。Unsafe类Unsafe 是 Java 底层的一个 “危险工具类”可以直接操作内存、CAS、线程挂起 / 唤醒。作用实现 CAS 原子操作AtomicInteger、ReentrantLock底层全靠它直接操作内存分配内存、释放内存、修改对象字段线程调度park()、unpark()挂起 / 唤醒线程LockSupport 底层二者联系自旋 循环重试Unsafe 提供 CAS 原子操作指令while(true) { // 自旋循环 if(Unsafe.compareAndSwap(...)) { // CAS 成功 → 拿到锁 break; } // 失败 → 继续自旋 }CAS 三大问题ABA、循环耗时、只能保证单个变量原子性怎么解决ABA:在CAS执行过程中另外一个线程把A改成B之后再改回A对于执行CAS过程的线程这个过程是无感的。就会导致操作认为未变更线程1读取变量为A准备改为C。此时线程2将变量A→B→A。线程1的CAS执行时发现仍是A但状态已丢失中间变化。解决方法Java 提供的工具类会在 CAS 操作中增加版本号Stamp或标记每次修改都更新版本号使得即使值相同也能识别变更历史。比如可以用 AtomicStampedReference 来解决 ABA 问题通过比对值和版本号识别ABA问题。AtomicStampedReferenceInteger ref new AtomicStampedReference(100, 0); // 尝试修改值并更新版本号 boolean success ref.compareAndSet(100, 200, 0, 1); // 前提当前值100 且 版本号0, 才会更新为200, 1循环耗时是指CAS自旋问题如果一直重试那么就会一直自旋空转循环浪费CPU解决方法1.jdk1.6后的自旋自适应前面有线程自旋成功了当前自旋时间就久一点前面自旋失败了当前就自旋时间短点2.锁升级机制如果自旋次数达到阈值会升级为重量锁陷入阻塞3.直接适用Lock锁锁竞争激烈时失败后直接陷入阻塞只能保证单个变量原子性CAS只能对一个变量做原子操作。比如a; b;你不能用一个 CAS 同时保证 a 和 b 都原子。这叫不能保证多行操作的原子性解决办法加sychornized锁对这一块加锁让两个操作包起来能体现整体的原子性什么是自旋锁优缺点自旋锁在争抢锁失败之后不进入阻塞状态而是进入自旋不断重试直到拿到锁优点不会进入阻塞避免了从用户态到内核态转换的开销响应速度快因为没有阻塞缺点占用CPU:如果一直没有强锁成功会一直空转自旋循环消耗CPU不适合长耗时的任务持锁线程执行时间久自旋线程一直空转性能急剧下降。什么是死锁死锁的产生条件是什么死锁两个线程互相持有对方需要的锁且不会主动释放锁互相等待卡死产生条件1.互斥条件一个锁只能由一个线程持有其他线程只能等待释放才能拿到2.不可剥夺条件锁在持有阶段不会被其他线程抢占只能自己释放3.请求与保持条件在持有一个锁的时候又去请求另一个锁且不释放锁4.循环条件A持有B的请求锁请求B里面的锁B持有A的请求锁请求A里面的锁什么是偏向锁撤销、重偏向偏向锁是 JVM 为减少无竞争场景下锁开销设计的轻量级锁默认开启适用于单线程反复获取锁的场景。触发场景当有第二个线程尝试获取已偏向的锁偏向锁就会被撤销。执行流程锁对象当前偏向线程 A线程 B 来抢锁JVM 进入安全点暂停持有偏向锁的线程 A检查线程 A 是否还在使用该锁情况 1A 已退出同步块 → 直接取消偏向锁降级为无锁B 正常获取情况 2A 仍在持有锁 →撤销偏向升级为轻量级锁两个线程开始自旋竞争。关键特点撤销会进入安全点有性能开销偏向锁一旦撤销默认不会再重新偏向旧逻辑撤销是偏向锁走向轻量级锁 / 重量级锁的第一步。重偏向Bias Rebias锁对象已经撤销过偏向当原偏向线程彻底不再使用该锁后锁可以重新偏向到新线程这个过程就叫重偏向。触发条件单对象重偏向锁曾经偏向线程 T1后因竞争被撤销T1 长时间不再访问该锁新线程 T2 多次获取这把锁JVM 判定当前无多线程竞争将锁重新偏向给 T2。批量重偏向高频考点当一个类下大量对象都发生偏向撤销阈值默认20 JVM 认为该类锁存在线程切换但竞争不激烈会把该类所有已撤销偏向的对象统一允许重偏向后续新线程获取时直接偏向新线程减少撤销开销。行为偏向锁撤销重偏向触发原因多线程竞争锁原偏向线程不再使用锁新线程独占锁状态变化偏向 → 无锁 / 轻量级锁无偏向 → 重新偏向新线程性能影响高进安全点、暂停线程低纯标记修改发生时机锁竞争瞬间竞争消失、单线程复用锁轻量级锁自旋次数自适应JDK 1.6 之后引入了自适应自旋锁。自旋次数不再固定而是由锁的历史自旋成功率决定如果该锁上之前自旋成功过JVM 会让当前线程自旋更久如果该锁上自旋很少成功JVM 会减少自旋次数甚至直接跳过自旋快速升级为重量级锁。 目的是减少无用自旋提升并发性能。什么是锁粗化、锁消除JVM 优化手段锁粗话和锁消除都是JVM种JIT(即时编译器)优化代码的手段目的是让代码的效率更高1.锁消除定义当一个代码块里不存在线程冲突时就消除锁原因JIT 会做逃逸分析 如果一个对象只在当前线程里使用没有逃逸到其他线程那就不可能产生线程竞争加锁完全多余。触发条件开启逃逸分析JDK 7 默认开启对象不逃逸出当前线程同步块无竞争可能作用完全消除无意义的锁开销性能提升巨大。2.锁粗化定义把连续多次加锁解锁合并成一次大锁。原理如果一段代码里反复对同一个对象加锁、解锁JIT 会把这些锁合并成一个更大范围的锁减少频繁加锁解锁的消耗。代码示例// 未优化前多次加锁解锁 for (int i 0; i 1000; i) { synchronized (lock) { // 每次循环都加锁 → 释放 } }锁粗化后// 优化后只加锁一次 synchronized (lock) { for (int i 0; i 1000; i) { // ... } }作用减少频繁加锁解锁带来的性能损耗。