java并发编程知识(锁)
1、synchronized用过吗经常用synchronized在JDK1.6之后进行了锁优化增加了偏向锁、轻量级锁大大提升了synchronized的性能。1.1、synchronized上锁的对象是什么①synchronized 用在普通方法上时上锁的是执行这个方法的对象。public synchronized void increment() { this.count; }②synchronized用在静态方法上时上锁的是这个类的Class对象。public static synchronized void increment() { count; }③synchronized用在代码块上时上锁的是括号中指定的对象比如说当前对象this。public void increment() { synchronized (this) { this.count; } }2、请讲一下synchronized 的实现原理synchronized依赖JVM内部的Monitor对象来实现线程同步。使用的时候不用手动去lock和 unlockJVM会自动加锁和解锁。synchronized加锁代码块时JVM会通过monitorenter、monitorexit两个指令来实现同步前者表示线程正在尝试获取lock对象的Monitor后者表示线程执行完了同步代码块正在释放锁。使用javap -c -s -v -l SynchronizedDemo.class 反编译 synchronized代码块时就能看到这两个指令。synchronized修饰普通方法时JVM会通过ACC_SYNCHRONIZED标记符来实现同步。2.1、你对Monitor了解多少Monitor是JVM内置的同步机制每个对象在内存中都有一个对象头——Mark Word用于存储锁的状态以及Monitor对象的指针。synchronized依赖对象头的Mark Word进行状态管理支持无锁、偏向锁、轻量级锁以及重量级锁。在Hotspot虚拟机中Monitor由ObjectMonitor实现ObjectMonitor() { _count 0; // 记录线程获取锁的次数 _owner NULL; // 指向持有ObjectMonitor对象的线程 _WaitSet NULL; // 处于wait状态的线程会被加入到_WaitSet _cxq NULL ; _EntryList NULL ; // 处于等待锁block状态的线程会被加入到该列表 }①_owner当前持有ObjectMonitor的线程初始值为null表示没有线程持有锁。线程成功获取锁后该值更新为线程ID释放锁后重置为null。②_count记录当前线程获取锁的次数可重入锁每次成功加锁_count 1释放锁_count - 1。③_WaitSet等待队列调用wait()方法后线程会释放锁并加入_WaitSet进入WAITING状态等待 notify() 唤醒。④_cxq阻塞队列用于存放刚进入Monitor的线程还未进入 _EntryList。⑤_EntryList竞争队列所有等待获取锁的线程BLOCKED状态会进入_EntryList等待锁释放后竞争执行权。2.2、会不会牵扯到os层面呢会synchronized升级为重量级锁时依赖于操作系统的互斥量——mutex来实现mutex用于保证任何给定时间内只有一个线程可以执行某一段特定的代码段。3、synchronized怎么保证可见性通过两步操作①加锁时线程必须从主内存读取最新数据。②释放锁时线程必须将修改的数据刷回主内存这样其他线程获取锁后就能看到最新的数据。3.1、synchronized怎么保证有序性synchronized通过JVM指令monitorenter和monitorexit来确保加锁代码块内的指令不会被重排。来解释一下比如说对于synchronized (lock) { x 1; flag true; }javap反编译后的伪代码monitorenter // 获取锁store x, 1 // 变量 x 1store flag, true // 变量 flag truemonitorexit // 释放锁实际 javap 反编译后的结果指令解释一下指令作用monitorenter获取锁进入同步代码块iconst_1将整数 1 压入操作数栈istore_1存储 1 到局部变量 xiconst_1再次将整数 1 压入操作数栈istore_2存储 1 到局部变量 flagaload 4加载 lock 对象引用monitorexit释放锁退出同步代码块3.2、synchronized怎么实现可重入的呢可重入意味着同一个线程可以多次获得同一个锁而不会被阻塞。synchronized之所以支持可重入是因为Java的对象头包含了一个Mark Word用于存储对象的状态包括锁信息。当一个线程获取对象锁时JVM会将该线程的ID写入Mark Word并将锁计数器设为1。如果一个线程尝试再次获取已经持有的锁JVM会检查Mark Word中的线程ID。如果ID匹配表示的是同一个线程锁计数器递增。当线程退出同步块时锁计数器递减。如果计数器值为零JVM将锁标记为未持有状态并清除线程ID信息。来解释一下class ReentrantExample { public synchronized void method1() { System.out.println(Method1 acquired lock); method2(); // 线程已经持有锁能继续调用 method2 } public synchronized void method2() { System.out.println(Method2 acquired lock); } public static void main(String[] args) { ReentrantExample example new ReentrantExample(); example.method1(); } }执行结果Method1 acquired lock Method2 acquired lock因为synchronized支持可重入所以method1获取锁后method2仍然可以获取锁。底层是通过Monitor对象的owner和count字段实现的owner记录持有锁的线程count记录线程获取锁的次数。4、synchronized 锁升级了解吗JDK1.6的时候为了提升synchronized的性能引入了锁升级机制从低开销的锁可以最大程度减少锁的竞争。没有线程竞争时就使用低开销的“偏向锁”此时没有额外的CAS操作轻度竞争时使用“轻量级锁”采用CAS自旋避免线程阻塞只有在重度竞争时才使用“重量级锁”由Monitor机制实现需要线程阻塞。4.1、synchronized为什么没有锁降级主要原因我认为是降级的收益不大。降级不是简单地把标志位改一下就完事。重量级锁涉及到操作系统的互斥量mutex还有等待队列、阻塞线程。要降级的话需要确保①没有线程在等待队列中②当前没有竞争发生③要安全地释放系统资源④要重新初始化轻量级锁的状态这一套检查和操作下来开销不小。而且什么时候检查每次释放锁都检查性能损耗太大了。4.2、讲一下synchronized四种锁状态吗①无锁状态对象未被锁定Mark Word存储对象的哈希码等信息。②偏向锁当线程第一次获取锁时会进入偏向模式。Mark Word会记录线程ID后续同一线程再次获取锁时可以直接进入synchronized加锁的代码无需额外加锁。③轻量级锁当多个线程在不同时段获取同一把锁即不存在锁竞争的情况时JVM会采用轻量级锁来避免线程阻塞。未持有锁的线程通过CAS自旋等待锁释放。当线程进入synchronized加锁的代码时如果对象的锁状态为偏向锁也就是锁类型为“01”偏向锁标记为“0”的状态。然后采用CAS自旋的方式尝试将对象头中的Mark Word替换为指向Lock Record的指针并将 Lock Record中的owner指针指向对象的Mark Word。如果这个替换动作成功了线程就拥有了该对象的锁对象头Mark Word的锁标志位会更新为“00”表示对象处于轻量级锁状态。④重量级锁如果自旋超过一定的次数或者一个线程持有锁一个自旋又有第三个线程进入 synchronized加锁的代码时轻量级锁就会升级为重量级锁。此时对象头的锁类型会更新为“10”Mark Word 会存储指向 Monitor 对象的指针其他等待锁的线程都会进入阻塞状态。4.3、synchronized做了哪些优化在JDK1.6之前synchronized是直接调用ObjectMonitor的enter和exit指令实现的这种锁也被称为重量级锁性能较差。随着JDK版本的更新synchronized的性能得到了极大的优化①偏向锁同一个线程可以多次获取同一把锁无需重复加锁。②轻量级锁当没有线程竞争时通过CAS自旋等待锁避免直接进入阻塞。③锁消除JIT可以在运行时进行代码分析如果发现某些锁操作不可能被多个线程同时访问就会对这些锁进行消除从而减少上锁开销。4.4、请详细说说锁升级的过程懵逼状态下的回答锁升级会从无锁升级为偏向锁再升级为轻量级锁最后升级为重量级锁。知道一点但不深入的回答①偏向锁当一个线程第一次获取锁时JVM会在对象头的Mark Word记录这个线程ID下次进入 synchronized时如果还是同一个线程可以直接执行无需额外加锁。②轻量级锁当多个线程尝试获取锁但不是同一个时段偏向锁会升级为轻量级锁等待锁的线程通过 CAS自旋避免进入阻塞状态。③重量级锁如果自旋失败锁会升级为重量级锁等待锁的线程会进入阻塞状态等待监视器Monitor进行调度。详细解释一下Ⅰ、从无锁到偏向锁当一个线程首次访问同步代码时如果此对象处于无锁状态且偏向锁未被禁用JVM会将该对象头的锁标记改为偏向锁状态并记录当前线程ID。此时对象头中的Mark Word中存储了持有偏向锁的线程ID。如果另一个线程尝试获取这个已被偏向的锁JVM会检查当前持有偏向锁的线程是否活跃。如果持有偏向锁的线程不活跃可以将锁偏向给新的线程否则撤销偏向锁升级为轻量级锁。Ⅱ、偏向锁的轻量级锁进行偏向锁撤销时会遍历堆栈的所有锁记录暂停拥有偏向锁的线程并检查锁对象。如果这个过程中发现有其他线程试图获取这个锁JVM会撤销偏向锁并将锁升级为轻量级锁。当有两个或以上线程竞争同一个偏向锁时偏向锁模式不再有效此时偏向锁会被撤销对象的锁状态会升级为轻量级锁。Ⅲ、轻量级锁到重量级锁轻量级锁通过自旋来等待锁释放。如果自旋超过预定次数自旋次数是可调的并且是自适应的失败次数多自旋次数就少表明锁竞争激烈。当自旋多次失败或者有线程在等待队列中等待相同的轻量级锁时轻量级锁会升级为重量级锁。在这种情况下JVM会在操作系统层面创建一个互斥锁——Mutex所有进一步尝试获取该锁的线程将会被阻塞直到锁被释放。5、synchronized和ReentrantLock的区别了解吗两句话回答synchronized由JVM内部的Monitor机制实现ReentrantLock基于AQS实现。synchronized可以自动加锁和解锁ReentrantLock需要手动lock()和unlock()。区别synchronizedReentrantLock锁实现机制对象头监视器模式依赖 AQS灵活性不灵活支持响应中断、超时、尝试获取锁释放锁形式自动释放锁显式调用 unlock ()支持锁类型非公平锁公平锁 非公平锁条件队列单条件队列多个条件队列可重入支持支持支持如果面试官还想知道更多可以继续回答①ReentrantLock可以实现多路选择通知绑定多个Condition而synchronized只能通过wait和notify唤醒属于单路通知ReentrantLock lock new ReentrantLock(); Condition condition lock.newCondition();②synchronized可以在方法和代码块上加锁ReentrantLock只能在代码块上加锁但可以指定是公平锁还是非公平锁。// synchronized 修饰方法 public synchronized void method() { // 业务代码 } // synchronized 修饰代码块 synchronized (this) { // 业务代码 } // ReentrantLock 加锁 ReentrantLock lock new ReentrantLock(); lock.lock(); try { // 业务代码 } finally { lock.unlock(); }③ReentrantLock提供了一种能够中断等待锁的线程机制通过lock.lockInterruptibly()来实现。ReentrantLock lock new ReentrantLock(); try { lock.lockInterruptibly(); } catch (InterruptedException e) { // 处理中断异常 }5.1、并发量大的情况下使用synchronized还是ReentrantLock我更倾向于ReentrantLock因为①ReentrantLock提供了超时和公平锁等特性可以应对更复杂的并发场景。②ReentrantLock允许更细粒度的锁控制能有效减少锁竞争。③ReentrantLock支持条件变量Condition可以实现比synchronized更友好的线程间通信机制。5.2、Lock了解吗Lock 是JUC中的一个接口最常用的实现类包括可重入锁ReentrantLock、读写锁 ReentrantReadWriteLock等。5.3、ReentrantLock的lock()方法实现逻辑了解吗lock方法的具体实现由ReentrantLock内部的Sync类来实现涉及到线程的自旋、阻塞队列、CAS、AQS等。lock方法会首先尝试通过CAS来获取锁。如果当前锁没有被持有会将锁状态设置为1表示锁已被占用。否则会将当前线程加入到AQS的等待队列中。final void lock() { if (compareAndSetState(0, 1)) // 尝试直接获取锁 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 如果获取失败进入AQS队列等待 }6、AQS了解多少AQS是一个抽象类它维护了一个共享变量state和一个线程等待队列为ReentrantLock等类提供底层支持。AQS的思想是如果被请求的共享资源处于空闲状态则当前线程成功获取锁否则将当前线程加入到等待队列中当其他线程释放锁时从等待队列中挑选一个线程把锁分配给它。6.1、AQS的源码了解吗第一状态 state由volatile变量修饰用于保证多线程之间的可见性private volatile int state;②同步队列由内部定义的Node类实现每个Node包含了等待状态、前后节点、线程的引用等是一个先进先出的双向链表。static final class Node { static final int CANCELLED 1; static final int SIGNAL -1; static final int CONDITION -2; static final int PROPAGATE -3; volatile Node prev; volatile Node next; volatile Thread thread; }AQS 支持两种同步方式①独占模式下每次只能有一个线程持有锁例如 ReentrantLock。 ②共享模式下多个线程可以同时获取锁例如 Semaphore 和 CountDownLatch。核心方法包括①acquire获取锁失败进入等待队列②release释放锁唤醒等待队列中的线程③acquireShared共享模式获取锁④releaseShared共享模式释放锁。AQS 使用一个 CLH 队列来维护等待线程CLH 是三个作者 Craig、Landin 和 Hagersten 的首字母缩写是一种基于链表的自旋锁。在CLH中当一个线程尝试获取锁失败后会被添加到队列的尾部并自旋等待前一个节点的线程释放锁。CLH的优点是假设有100个线程在等待锁锁释放之后只会通知队列中的第一个线程去竞争锁。避免同时唤醒大量线程浪费CPU资源。7、说说ReentrantLock的实现原理ReentrantLock是基于AQS实现的 可重入排他锁使用CAS尝试获取锁失败的话会进入CLH 阻塞队列支持公平锁、非公平锁可以中断、超时等待。内部通过一个计数器state来跟踪锁的状态和持有次数。当线程调用lock()方法获取锁时ReentrantLock会检查state的值如果为0通过CAS修改为1表示成功加锁。否则根据当前线程的公平性策略加入到等待队列中。线程首次获取锁时state值设为1如果同一个线程再次获取锁时state加1每释放一次锁state减1。当线程调用unlock()方法时ReentrantLock会将持有锁的state减1如果state 0则释放锁并唤醒等待队列中的线程来竞争锁。使用方式非常简单class CounterWithLock { private int count 0; private final Lock lock new ReentrantLock(); public void increment() { lock.lock(); // 获取锁 try { count; } finally { lock.unlock(); // 释放锁 } } public int getCount() { return count; } }new ReentrantLock()默认创建的是非公平锁NonfairSync。在非公平锁模式下锁可能会授予刚刚请求它的线程而不考虑等待时间。当切换到公平锁模式下锁会授予等待时间最长的线程。8、ReentrantLock怎么创建公平锁很简单创建ReentrantLock的时候传递参数true就可以了。ReentrantLock lock new ReentrantLock(true); // true 代表公平锁false 代表非公平锁 public ReentrantLock(boolean fair) { sync fair ? new FairSync() : new NonfairSync(); }1怎么创建一个非公平锁呢创建 ReentrantLock 时不传递参数或者传递参数就好了。2非公平锁和公平锁有什么不同公平锁意味着在多个线程竞争锁时获取锁的顺序与线程请求锁的顺序相同即先来先服务。非公平锁不保证线程获取锁的顺序当锁被释放时任何请求锁的线程都有机会获取锁而不是按照请求的顺序。8.1、公平锁的实现逻辑了解吗公平锁的核心逻辑在AQS的hasQueuedPredecessors()方法中该方法用于判断当前线程前面是否有等待的线程。如果队列前面有等待线程当前线程就不能抢占锁必须按照队列顺序排队。如果队列前面没有线程或者当前线程是队列头部的线程就可以获取锁。9、CAS了解多少CAS是一种乐观锁用于比较一个变量的当前值是否等于预期值如果相等则更新值否则重试。在CAS中有三个值①V要更新的变量(var)②E预期值(expected)③N新值(new)先判断V是否等于E如果等于将V的值设置为N如果不等说明已经有其它线程更新了V当前线程就放弃更新。这个比较和替换的操作需要是原子的不可中断的。Java中的CAS是由Unsafe类实现的。AtomicInteger类的compareAndSet就是一个CAS方法AtomicInteger atomicInteger new AtomicInteger(0); int expect 0; int update 1; atomicInteger.compareAndSet(expect, update);它调用的是Unsafe的compareAndSwapInt。9.1、怎么保证CAS的原子性CPU会发出一个LOCK指令进行总线锁定阻止其他处理器对内存地址进行操作直到当前指令执行完成。lock cmpxchg [esi], eax ; 比较 esi 地址中的值与 eax如果相等则替换10、CAS有什么问题CAS存在三个经典问题ABA问题、自旋开销大、只能操作一个变量等。10.1、什么是ABA问题ABA问题指的是一个值原来是A后来被改为B再后来又被改回A这时CAS会误认为这个值没有发生变化。线程 1CAS(A → B)修改变量 A → B线程 2CAS(B → A)变量又变回 A线程 3CAS(A → C)CAS 成功但实际数据已被修改过可以使用版本号/时间戳的方式来解决ABA问题。比如说每次变量更新时不仅更新变量的值还更新一个版本号。CAS操作时不仅比较变量的值还比较版本号。class OptimisticLockExample { private int version; private int value; public synchronized boolean updateValue(int newValue, int currentVersion) { if (this.version currentVersion) { this.value newValue; this.version; return true; } return false; } }Java的AtomicStampedReference就增加了版本号它会同时检查引用值和stamp是否都相等。使用示例class ABAFix { private static AtomicStampedReferenceString ref new AtomicStampedReference(100, 1); public static void main(String[] args) { new Thread(() - { int stamp ref.getStamp(); ref.compareAndSet(100, 200, stamp, stamp 1); ref.compareAndSet(200, 100, ref.getStamp(), ref.getStamp() 1); }).start(); new Thread(() - { try { Thread.sleep(100); } catch (InterruptedException e) {} int stamp ref.getStamp(); System.out.println(CAS 结果 ref.compareAndSet(100, 300, stamp, stamp 1)); }).start(); } }10.2、自旋开销大怎么解决CAS失败时会不断自旋重试如果一直不成功会给CPU带来非常大的执行开销。可以加一个自旋次数的限制超过一定次数就切换到synchronized挂起线程。int MAX_RETRIES 10; int retries 0; while (!atomicInt.compareAndSet(expect, update)) { retries; if (retries MAX_RETRIES) { synchronized (this) { // 超过次数使用 synchronized 处理 if (atomicInt.get() expect) { atomicInt.set(update); } } break; } }10.3、涉及到多个变量同时更新怎么办可以将多个变量封装为一个对象使用AtomicReference进行CAS更新。class Account { static class Balance { final int money; final int points; Balance(int money, int points) { this.money money; this.points points; } } private AtomicReferenceBalance balance new AtomicReference(new Balance(100, 10)); public void update(int newMoney, int newPoints) { Balance oldBalance, newBalance; do { oldBalance balance.get(); newBalance new Balance(newMoney, newPoints); } while (!balance.compareAndSet(oldBalance, newBalance)); } }11、Java有哪些保证原子性的方法比如说以Atomic开头的原子类synchronized关键字ReentrantLock锁等。12、原子操作类了解多少原子操作类是基于CAS volatile实现的底层依赖于Unsafe 类最常用的有AtomicInteger、AtomicLong、AtomicReference等。像AtomicIntegerArray这种以Array结尾的还可以原子更新数组里的元素。class AtomicArrayExample { public static void main(String[] args) { AtomicIntegerArray atomicArray new AtomicIntegerArray(new int[]{1, 2, 3}); atomicArray.incrementAndGet(1); // 对索引 1 进行自增 System.out.println(atomicArray.get(1)); // 输出 3 } }像AtomicStampedReference还可以通过版本号的方式解决CAS中的ABA问题。class AtomicStampedReferenceExample { public static void main(String[] args) { AtomicStampedReferenceInteger ref new AtomicStampedReference(100, 1); int stamp ref.getStamp(); // 获取版本号 ref.compareAndSet(100, 200, stamp, stamp 1); // A → B ref.compareAndSet(200, 100, ref.getStamp(), ref.getStamp() 1); // B → A } }13、AtomicInteger的源码了解吗AtomicInteger是基于volatile和CAS实现的底层依赖于Unsafe类。核心方法包括 getAndIncrement、compareAndSet等。public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }14、线程死锁了解吗死锁发生在多个线程相互等待对方释放锁时。比如说线程1持有锁R1等待锁R2线程2持有锁 R2等待锁R1。14.1、死锁发生的四个条件了解吗第一条件是互斥资源不能被多个线程共享一次只能由一个线程使用。如果一个线程已经占用了一个资源其他请求该资源的线程必须等待直到资源被释放。第二个条件是持有并等待一个线程已经持有一个资源并且在等待获取其他线程持有的资源。第三个条件是不可抢占资源不能被强制从线程中夺走必须等线程自己释放。第四个条件是循环等待存在一种线程等待链线程A等待线程B持有的资源线程B等待线程C持有的资源直到线程N又等待线程A持有的资源。14.2、该如何避免死锁呢第一所有线程都按照固定的顺序来申请资源。例如先申请R1再申请R2。第二如果线程发现无法获取某个资源可以先释放已经持有的资源重新尝试申请。15、死锁问题怎么排查呢首先从系统级别上排查比如说在Linux生产环境中可以先使用top ps等命令查看进程状态看看是否有进程占用了过多的资源。接着使用JDK自带的一些性能监控工具进行排查比如说 使用jps -l查看当前进程然后使用jstack进程号 查看当前进程的线程堆栈信息看看是否有线程在等待锁资源。也可以使用一些可视化的性能监控工具比如说JConsole、VisualVM等查看线程的运行状态、锁的竞争情况等。我们来通过实际代码说明一下class DeadLockDemo { private static final Object lock1 new Object(); private static final Object lock2 new Object(); public static void main(String[] args) { new Thread(() - { synchronized (lock1) { System.out.println(线程1获取到了锁1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println(线程1获取到了锁2); } } }).start(); new Thread(() - { synchronized (lock2) { System.out.println(线程2获取到了锁2); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println(线程2获取到了锁1); } } }).start(); } }创建两个线程每个线程都试图按照不同的顺序获取两个锁lock1和lock2。锁的获取顺序不一致很容易导致死锁。运行这段代码会发现两个线程都无法继续执行进入了死锁状态。运行jstack pid命令可以看到死锁的线程信息。编码时尽量使用tryLock()代替lock()tryLock()可以设置超时时间避免线程一直等待。同时尽量避免一个线程同时获取多个锁如果需要多个锁可以按照固定的顺序获取。16、聊聊线程同步和互斥同步意味着线程之间要密切合作按照一定的顺序来执行任务。比如说线程A先执行线程B再执行。互斥意味着线程之间要抢占资源同一时间只能有一个线程访问共享资源。比如说线程A在访问共享资源时线程B不能访问。同步关注的是线程之间的协作互斥关注的是线程之间的竞争。16.1、如何实现同步和互斥可以使用synchronized关键字或者Lock接口的实现类如ReentrantLock来给资源加锁。锁在操作系统层面的意思是Mutex某个线程进入临界区后也就是获取到锁后其他线程不能再进入临界区要阻塞等待持有锁的线程离开临界区。16.2、锁要解决哪些问题第一谁可以拿到锁可以是类对象可以是当前的this对象也可以是任何其他新建的对象。synchronized (this) { // 临界区 }第二抢占锁的规则能不能抢占多次自己能不能反复抢。第三抢不到怎么办自旋阻塞或者超时放弃第四锁被释放了还在等待锁的线程怎么办是通知所有线程一起抢或者只告诉一个线程抢16.3、说说自旋锁自旋锁是指当线程尝试获取锁时如果锁已经被占用线程不会立即阻塞而是通过自旋也就是循环等待的方式不断尝试获取锁。线程1 线程2| || 获取锁成功 | 尝试获取锁|------------|锁已被占用自旋等待| 释放锁 ||------------| 获取锁成功| |适用于锁持有时间短的场景ReentrantLock的tryLock方法就用到了自旋锁。自旋锁的优点是可以避免线程切换带来的开销缺点是如果锁被占用时间过长会导致线程空转浪费CPU资源。class SpinLock { private AtomicBoolean lock new AtomicBoolean(false); public void lock() { while (!lock.compareAndSet(false, true)) { // 自旋等待不断尝试获取锁 } } public void unlock() { lock.set(false); } public static void main(String[] args) { SpinLock spinLock new SpinLock(); Runnable task () - { spinLock.lock(); try { System.out.println(Thread.currentThread().getName() 获取到锁); } finally { spinLock.unlock(); } }; Thread t1 new Thread(task); Thread t2 new Thread(task); t1.start(); t2.start(); } }默认情况下自旋锁会一直等待直到获取到锁为止。在实际开发中需要设置自旋次数或者超时时间。如果超过阈值线程可以放弃锁或者进入阻塞状态。16.4、互斥和同步在时间上有要求吗互斥的核心是保证同一时刻只有一个线程能访问共享资源。同步强调的是线程之间的执行顺序特别是在多个线程需要依赖于彼此的执行结果时。例如在CountDownLatch中主线程会等待多个子线程的任务完成。class SyncExample { public static void main(String[] args) throws InterruptedException { CountDownLatch latch new CountDownLatch(3); // 创建3个子线程 for (int i 0; i 3; i) { new Thread(() - { try { Thread.sleep(1000); // 模拟任务 System.out.println(打完王者了.); } catch (InterruptedException e) { e.printStackTrace(); } finally { latch.countDown(); // 每个线程任务完成后计数器减1 } }).start(); } System.out.println(等打完三把王者就去睡觉...); latch.await(); // 主线程等待子线程完成 System.out.println(好王者玩完了可以睡了); } }所有子线程完成后主线程才会继续执行。17、聊聊悲观锁和乐观锁悲观锁认为每次访问共享资源时都会发生冲突所在在操作前一定要先加锁防止其他线程修改数据。乐观锁认为冲突不会总是发生所以在操作前不加锁而是在更新数据时检查是否有其他线程修改了数据。如果发现数据被修改了就会重试。17.1、乐观锁发现有线程过来修改数据怎么办可以重新读取数据然后再尝试更新直到成功为止或达到最大重试次数。读取数据 - 尝试更新 - 成功返回成功|- 失败 - 重试 - 达到最大次数 - 返回失败写个代码演示一下class CasRetryExample { private static AtomicInteger counter new AtomicInteger(0); private static final int MAX_RETRIES 5; public static void main(String[] args) { boolean success false; int retries 0; while (retries MAX_RETRIES) { int currentValue counter.get(); boolean updated counter.compareAndSet(currentValue, currentValue 1); if (updated) { System.out.println(更新成功当前值: counter.get()); success true; break; } else { retries; System.out.println(更新失败进行第 retries 次重试); } } if (!success) { System.out.println(达到最大重试次数操作失败); } } }