Java锁相关问题
为什么volatile保证内存的可见性
1.禁止重排序和强制刷新内存
2.常量使用volatile关键字,读的时候强制读取主内存的数据保证是最新的,写的时候需要把最新的数据回写到主内存
3.指令重排:指计算机编译器和处理器为了提高执行效率,对指令进行优化重排,目的是为了减少CPU空闲时间和提高内存使用率,进而提升程序执行顺序。
4.并发情况下无法保证数据安全,需业务进行加锁操作。
1. volatile标记值的修改操作步骤 写操作: 线程本地内存更新:线程将新值写入本地内存(高速缓存)。 刷新主内存:通过内存屏障指令(如lock前缀)强制将本地内存值刷新到主内存,确保所有线程可见。 内存屏障作用:禁止指令重排序,确保写操作前后的指令顺序。 2. 非volatile标记值的修改操作步骤 写操作: 线程本地内存更新:线程将新值写入本地内存(高速缓存)。 无强制刷新:未使用内存屏障指令,本地内存值可能延迟刷新到主内存。 指令重排序风险:编译器或处理器可能重排序指令,导致数据不可见。
1.Java中有那些锁,区别是什么
1.java中的锁主要分为乐观锁和悲观锁,其中CAS是乐观锁,synchronized、Lock是悲观锁。
2.乐观锁认为我在操作的时候不会有其余的线程竞争,如果有竞争就自旋等待(CAS)或异常结束(数据库多版本机制)
3.悲观锁认为我操作的时候一定会有其余的线程竞争,如果我未竞争到锁就结束或阻塞等待(tryLock)。
2.CAS实现原理
1.CAS是乐观锁思想实现的,主要解决线程并发问题,其原理是新值、旧值、预期值的比较与交换,当且仅当旧址和预期值相同的情况才会把旧址改为新值,JAVA中是基于unsafe类提供的方法实现的,CAS是CPU基本的原子操作 。
2.java中Atomic类的方法是基于CAS实现的,MySql数据的多版本控制也是基于CAS实现的。
3.CAS存在ABA问题。当共享变量经历A→B→A的值变化时,CAS仅检查最终值是否与预期值相同(均为A),而忽略中间被修改为B的过程,导致逻辑误判。JAVA中提供了解决ABA的方案,可以使用AtomicstampedReference<V>来解决。
4. 在100个并发线程同时对 AtomicInteger 进行累加操作时,即使初始内存值和预期值都为1,最终仍能正确累加到100,在并发操作的时候只有一个线程会CAS成功,其余的线程需要进行自旋,直到读取到的旧值和内存值一样才会进行累加。
public final int incrementAndGet() { // 调用 Unsafe 的 getAndAddInt 方法,参数:当前对象、value字段的内存偏移量、累加值1 return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } // Unsafe 类中的核心方法(底层实现) public final int getAndAddInt(Object var1, long var2, int var4) { int var5; // 自旋(循环)+ CAS 核心逻辑 do { // 1. 读取当前内存中的值(volatile 保证可见性) var5 = this.getIntVolatile(var1, var2); // 2. CAS 尝试更新:比较内存值是否等于 var5,是则更新为 var5+var4,否则返回false重试 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 3. CAS 成功后,返回更新前的旧值 return var5; }
3.synchronized实现原理和锁升级

1.synchronized是一共互斥锁,只能有个线程持有这把锁,synchronized中有个owner属性,owner存放的是持有当前锁的线程,当owner为null的时候其余线程可以通过CAS进行owner修改,如果修改成功则当前线程持有锁,如果修改失败则进行几次CAS,没有拿到就信息park挂起线程。
4.synchronized锁升级过程。
1.无锁:当前对象没有被任何锁资源占用&&在JDK1.8后会有个4S的偏向锁延迟,JVM刚刚启动的4S中,不存在偏向锁状态。
2.偏向锁:
1.撇去4S的延迟,刚创建出来的都行都是偏向锁(匿名偏向) 2.当前对象被作为锁资源占用,且指向了某一线程此时就是偏向锁(普通偏向) 3.如果一个线程执行任务后,反复的获取同一把锁,这个时候偏向锁的优势就体现出来了,无需进行CAS操作,直接比较线程指向是否相同,相同则直接获取锁。
3.轻量级锁:在偏向锁的状态下,锁资源出现了竞争,就会升级为轻量锁。轻量锁状态下会进行几次CAS操作将owner(monitor中的owner)修改为自己的线程,默认是10次,这里的CAS操作走的自适应自旋锁。
4.重量级锁:在轻量级锁状态下,轻量级锁竞争失败,就会升级为重量级锁,重量级锁也会进行几次CAS操作将owner(monitor中的owner)修改为自己的线程,如果成功就直接拿锁执行任务,如果失败则挂起线程,等起锁释放后被唤醒。
private void synchronizedTest1(){ //1.无锁状态 没有被任何锁资源占用且在JVM启动4S内 Object o = new Object(); synchronized (o){ //2.轻量级锁 } } private void synchronizedTest2(){ try { //模拟JVM启动耗时 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //3.偏向锁的匿名偏向 Object o = new Object(); synchronized (o){ //4.偏向锁的普通偏向 } } }
synchronized锁升级的作用是什么
-
性能优化
- 减少锁开销:从无锁到重量级锁的逐步升级,避免频繁的系统调用(如
lock()/unlock())。 - 适应竞争场景:根据竞争程度自动选择最优锁类型(偏向锁、轻量级锁、重量级锁)。
- 减少锁开销:从无锁到重量级锁的逐步升级,避免频繁的系统调用(如
-
提升并发性能
- 单线程优化:偏向锁(偏向当前线程)避免CAS开销,性能接近无锁。
- 低竞争场景:轻量级锁(自旋锁)减少线程阻塞,提升吞吐量。
- 高竞争场景:重量级锁(操作系统互斥量)确保线程安全,但性能下降。
4.Synchronized和ReentrantLock的区别
1.Synchronized是java内置关键字,是JVM基于内置关键字monitor实现,Synchronized是公平锁,内置自动加锁和释放锁,是可重入锁,发生异常时自动释放锁。
2.ReentrantLock是API级别的,基于AQS(AbstractQueuedSynchronized实现。ReentrantLock根据参数属性控制是公平还所非公平锁,需显示调用lock(),unLock()进行加锁和释放锁,使用ReenTrantLock锁进行线程间的通信需要依赖Condition进行,也是可重入锁,可中断和尝试获取锁,发生异常时需要在finally内释放锁。
5.AQS是什么

1.AQS(AbstractQueuedSynchronized)其实JUC下一个基础类,没有具体是实现并发功能,但是大多数JUC包下的类都继承了AQS,如;ReentrantLock,CountDownLatch、线程池等等、
2.AQS核心的是同步状态管理和线程队列机制
1.AQS中有一个被volitail修饰的state,当state = 0时表示没有线程持有锁资源,当state>0时表现有线程持有资源。 2.Node节点组成的双向队列:当出现锁竞争的时候,未竞争到锁的线程会作为node节点存放到双向队列中。 3.Node对象节点组成的单向链表:当线程持有锁的时候执行了await命令,线程会释放锁,此时当前线程会作为node节点存放到单线链表中,当接受到signal时会从单向链表转移到双向队列参与锁竞争。
6.公平锁和非公平锁的区别

1.公平锁支持先进先出,先到的线程先获取锁。
2.非公平锁允许线程执行CAS竞争,谁拿到锁谁就去执行任务,其余的就排队。
3.synchronized是公平锁的代表,ReenTrantLocak可通过属性设置公平非公平。
4.ReentrantLock中有两个方法决定实现公平和非公平之分。
lock: 非公平锁:线程直接执行CAS尝试将state从0修改为1,如果修改成功拿锁走人,修改失败调用tryAcquire。 公平锁:执行执行tryAcqurie。 tryacquire方法: 非公平锁:线程直接执行CAS尝试将state从0修改为1,如果修改成功拿锁走人,失败了就排队。 公平锁:如果state=0且有线程排队则直接给最先进来的线程。
7.系统中分布式锁是如何使用的,分布式锁有几种实现方式

1.在分布式系统/微服务系统的情况多线程并发竞争同一资源这个时候就需要使用分布式锁。
2.分布式锁有redis、redission,lua脚本、zookeeper
3.redis分布式锁可以使用setnx来进行加锁和释放锁,setnx命令保证我对某个key设置值之后其余命令无法在设置,但是加锁后执行业务逻辑时程序停止或重启了,就会导致当前key长生不老,就会导致次业务因无法获取锁而崩溃。
4.可以使用setnx+expire来做分布锁,这样锁就有过期时间了,在执行业务逻辑时程序崩溃也会释放锁,当时setnx+expire是非原子的,两者之间也存在程序崩溃后锁无法释放的问题。
5.可以使用redis的set(key,value,nx,ex,时间单位)来做分布式做,redis层面保证命令是原子的。但是代码书写时要把加锁操作放到try()cathc()finally()外,避免因当前线程加锁失败删除掉别的线程锁。
6.可以使用redission提高的命令做锁,redission有自动续费看门狗机制、集群之间容错机制,支持可重入机制。
什么是 Redisson 看门狗机制 看门狗是 Redisson 为解决分布式锁超时释放问题设计的自动续期机制。简单来说,当你获取到分布式锁后,如果业务逻辑还没执行完,锁却快到期了,看门狗会自动帮你把锁的过期时间延长,避免锁提前释放导致并发问题。 核心工作原理 初始加锁:你调用 Redisson 获取锁时,会设置一个默认的锁过期时间(默认 30 秒),同时启动看门狗线程。 续期触发:看门狗会每隔 锁过期时间/3(默认 10 秒)检查一次:如果当前线程还持有锁,且业务未执行完,就自动把锁的过期时间重置为 30 秒。 终止续期:当业务执行完成,你主动释放锁,或者线程意外终止时,看门狗会停止续期,锁到期后自动释放

8. CountDownLatch 和CyclicBarrier和Semaphore 的作用和区别
1.CountDownLatch主要作用是主线程等待所有被调用线程结束,才会执行,每次调用countDown()方法计算器进行减1,当结果减为0时,才会执行主线程await后的逻辑。CountDownLatch内部是基于CAS操作执行的,不可重置。
//主线程等待所以线程任务执行结束后才执行自己的逻辑:比如说田径比赛,需要大家都到齐了比赛才开始 public static void countDownLatch1() throws InterruptedException { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 120, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), new ThreadPoolExecutor.CallerRunsPolicy()); CountDownLatch countDownLatch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { Runnable runnable = new Runnable() { @Override public void run() { Random random = new Random(); int num = random.nextInt(3) + 3; System.out.println("随机数据num=" + num); try { Thread.sleep(num * 1000); } catch (InterruptedException e) { e.printStackTrace(); } countDownLatch.countDown(); System.out.println("此线程执行结束,线程名称"+ Thread.currentThread().getName()); } }; threadPoolExecutor.execute(runnable); } System.out.println("开始执行主线程"); countDownLatch.await(); System.out.println("主线程执行结束"); threadPoolExecutor.shutdown(); }
开始执行主线程 随机数据num=3 随机数据num=5 随机数据num=3 此线程执行结束,线程名称pool-1-thread-3 此线程执行结束,线程名称pool-1-thread-1 此线程执行结束,线程名称pool-1-thread-2 主线程执行结束
2.CyclicBarrier主要作用是线程直接互相等待,当最慢的线程结束才会一块进行后续操作。每次调用await()方法时计算器进行加1,当结果加到执行数值,所有线程才会执行await()后面的逻辑。 CyclicBarrier是基于reenTrantLock原理实现的,可重置。
//各个线程直接互相等待,最慢的一个线程结束了大家才一块结束,比如田径比赛后大家一块去吃烧烤 public static void cyclicBarrier(){ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 3, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), new ThreadPoolExecutor.CallerRunsPolicy()); CyclicBarrier cyclicBarrier = new CyclicBarrier(3); CompletableFuture<Void> voidCompletableFuture1 = cyclicBarrierThread1(threadPoolExecutor, cyclicBarrier); CompletableFuture<Void> voidCompletableFuture2 = cyclicBarrierThread2(threadPoolExecutor, cyclicBarrier); CompletableFuture<Void> voidCompletableFuture3 = cyclicBarrierThread3(threadPoolExecutor, cyclicBarrier); try { voidCompletableFuture1.get(); voidCompletableFuture2.get(); voidCompletableFuture3.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } //相当于大家一块跑步,大家都结束后一块去吃烧烤,需要等大家结束后才能进行下一步操作 private static CompletableFuture<Void> cyclicBarrierThread1(ThreadPoolExecutor threadPoolExecutor,CyclicBarrier cyclicBarrier){ CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> { System.out.println("线程cyclicBarrierThread1开始逻辑处理"); try { Thread.sleep(1000); System.out.println("线程cyclicBarrierThread1逻辑处理完毕"); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } System.out.println("cyclicBarrierThread1屏障通过后上报"); },threadPoolExecutor); return voidCompletableFuture; } private static CompletableFuture<Void> cyclicBarrierThread2(ThreadPoolExecutor threadPoolExecutor,CyclicBarrier cyclicBarrier){ CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> { System.out.println("线程cyclicBarrierThread2开始逻辑处理"); try { Thread.sleep(2000); System.out.println("线程cyclicBarrierThread2逻辑处理完毕"); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } System.out.println("cyclicBarrierThread2屏障通过后上报"); },threadPoolExecutor); return voidCompletableFuture; } private static CompletableFuture<Void> cyclicBarrierThread3(ThreadPoolExecutor threadPoolExecutor,CyclicBarrier cyclicBarrier){ CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> { System.out.println("线程cyclicBarrierThread3开始逻辑处理"); try { Thread.sleep(3000); System.out.println("线程cyclicBarrierThread3逻辑处理完毕"); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } System.out.println("cyclicBarrierThread3屏障通过后上报"); },threadPoolExecutor); return voidCompletableFuture; }
线程cyclicBarrierThread1开始逻辑处理
线程cyclicBarrierThread2开始逻辑处理
线程cyclicBarrierThread3开始逻辑处理
线程cyclicBarrierThread1逻辑处理完毕
线程cyclicBarrierThread2逻辑处理完毕
线程cyclicBarrierThread3逻辑处理完毕
cyclicBarrierThread3屏障通过后上报
cyclicBarrierThread1屏障通过后上报
cyclicBarrierThread2屏障通过后上报
3.Semaphore主要作用类似线程之间的凭证,当线程拿到屏障后才会执行,如果没有拿到就需要等待,当别人释放后自己取抢。Semaphore内部是基于CAS操作执行的 计数时可增可减、可动态控制许可数量。
示例:
CountDownLatch相当于在田径比赛时每个运动员去站对应的跑道,当跑到站满了(剩余跑道为0),活动才开始。
Semaphore相等于运动员手里的接力棒,只有下一个队员拿到接力棒后才可以跑。
CyclicBarrier相当于所有的接力运动员都跑完了,大家一块去吃烧烤。
9.锁和@Transactional一起使用导致事务失败是为什么
1.@Transactional有点类似AOP的环绕通知,当线程读取到方法的@Transactional命令会在进入方法前开启事务,当方法结束后才会进行事务提交或回滚。
2.如果@Transactional和分布式锁作用在一个方法,就会出现分布式锁释放了,但是事务没有提交,后来的线程获取到锁对数据进行查询或更新就会出现脏数据问题。
3.解决方案是将分布式操作作用在事务外部,保证事务提交后再进行锁释放。
//异常情况 @Transactional public void test(){ synchronized (obj){ //数据库更新操作 } }
//正常操作,锁的作用域应该包含事务的作用域 public void test1(){ synchronized (obj){ update(); } } @Transactional public void update(){ //数据库更新操作 } }

浙公网安备 33010602011771号