您的位置 首页 java

Java锁——2022最新Java面试八股文

java 提供了各种各样的锁,每种锁有不同的特性,所以使用场景也不一样。本文从Java锁的底层AQS开始讲起,大概如下:

  • AQS
  • 锁状态变更(升级、消除等)
  • 各种类型锁介绍
  • volatile
  • synchronized
  • Reentrant lock 特性
  • Semaphore、Condition、CountDownLatch、CyclicBarrier对比
  • 队列对比
  • CAS相关
  • ThreadLocal
  • 线程池

  • 1 AQS

1.1)数据结构: 双向 链表 ,是队列的一种实现

其中sync queue是双向链表,包括head节点, tail 节点,head节点主要用作后续的调度

Condition queue是条件队列,不是必须的,其实单向链表,只有使用condition的时候,才会存在此链表

volatile 锁状态

1.2)获取锁:队头CAS尝试更新锁状态,成功,获取到锁,更新链表线程的等待节点;否则自旋获取锁,如果还不行将当前 线程 加入队尾

1.3)释放锁:修改状态值,调整等待链表

1.4)核心: CAS+自旋

1.5)Lock Support

AQS控制线程通过LockSupport来实现的,park等待一个许可,unpark为某个线程提供许可,类似object wait,notify,但是object wait,notify需要配合sychonized来实现,并且顺序不能乱,而park,unpark不需要。

1.6)Unsafe.park和Unsafe.unpark

是通过mutex,condition实现的,mutex,condition保护一个_counter的变量,当park的时候这个变量设置为0,unpark的时候变量设置为1

  • 2 锁升级

2.1)状态: 无锁->偏向锁->轻量级锁->重量级锁

2.2)Mark Word:对象头中的,里面存储 hashCode ,gc分代年龄,锁信息

Lock Record:为线程分配锁空间

自旋锁 大部分时间,锁被占用时间很短,所以没必要挂起线程,用户态与内核态切换会严重影响性能,自旋就是让系统执行忙循环,防止从用户态进入到内核态,自旋默认次数是10次,

-XX:usingSpining

自适应锁:自适应的自旋锁,自旋时间不是固定,由上次同一个锁的自旋时间和锁的持有者来决定

  • 3 锁消除

3.1)给不需要加锁的地方加上了锁,数据都是私有的,没有线程共享,所以不需要加锁

  • 4 锁粗化

4.1)将多次锁的请求合并成一个,防止多次获取锁释放锁,消耗资源,比如for循环中获取锁

  • 5 锁状态

01无锁 01偏向锁 00轻量级锁 10重量级锁

  • 6 偏向锁

对象第一次被线程获取时,锁状态为01,偏向状态为1,进入偏向锁模式

CAS尝试将线程ID记录到Mark Word中,成功,说明线程进入同步块不需要加锁

一旦其他线程尝试获取加锁,偏向锁模式失效,锁状态改为01-无锁状态或00-轻量级锁状态

缺点:如果一个对象总是被多个线程访问,偏向锁反而会多余

优点:一个线程访问,不需要加锁

  • 7 轻量级锁

当线程访问同步块的时候, JVM 会给当前线程创建一个LockRecord区域,用来复制当前锁对象头中的Mark Word,复制成功后尝试CAS将当前对象的Mark Word改为执行LockRecored的指针,更新成功,说明获取到了锁

如果更新失败就会比较当前锁对象是否已经指向Lock Record,如果是,说明已经获取到了锁,否则说明存在多个线程在竞争

如果存在两个以上的线程在竞争同一个锁对象,那么轻量级锁将升级为重量级锁,锁状态更新为10,其他线程进入阻塞状态

优点:CAS获取锁,不需要用户态和内核态的切换

缺点:如果存在多线程锁竞争,就会多出来CAS操作

轻量级锁主要有两种:自旋锁,自适应自旋锁

  • 8 volatile

8.1)作用

8.1.1) 多线程 可见性,8.1.2)禁止重排序

volatile 通过内存屏障,来保证指令不会重拍,解决了内存可见性的问题

8.2)场景

8.2.1)状态标志位

8.2.2)在并发包中支持可见性,

8.2.3)与sychonized组合作为读-写锁策略

  • 9 synchronized

作用范围:作用在方法上,锁住的是对象的实例,作用在 静态方法 上,锁住的是 Class 实例

底层原理1)同步代码块:monitor计数器为0,表示当前monitor未获取到锁,monitorenter后,获取锁成功,monitor加一。

已经拥有该monitor的线程支持可重入,monitorexit释放monitor的所有权,monitor减一。monitor为0,表示线程不再对monitor拥有所有权

2)同步方法:方法被调用时,首先检查ACC_SYNCHRONIZED是否有设置,如果设置了,执行线程先获取monitor,获取成功执行方法内容,执行完后,释放monitor

synchronized优化:适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁

3)底层:synchronized底层有两个队列,entrylist,waitSet,多个线程进入代码块的时候首先会进入entryList,当有个线程获取到monitor锁的时候, 计数器 加一;当线程被调用wait方法的时候,将释放锁,进入waitSet中等待被唤醒,当调用notify的时候进入entrylist竞争锁。线程执行完毕,释放锁,计数器减一,

  • 10 ReentrantLock

10.1)底层是AQS的实现原理

10.2)ReentrantLock先通过CAS尝试获取锁

10.2.2)如果此时锁已经被占用,该线程加入AQS队列并wait()

10.2.3)当前驱线程的锁被释放,挂在CLH队列为首的线程就会被notify(),然后继续CAS尝试获取锁,此时:

非公平锁,如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占。

公平锁,只有在CLH队列头的线程才可以获取锁,新来的线程只能插入到队尾。

10.2.4)公平锁为啥效率低

公平锁维护了一个队列,后面的线程要加锁,也要看前面有没有其他线程在等待,如果有,则自己需要挂起,加到队列后面去,然后唤醒最前面的线程。这种情况下比非公平锁多了一个挂起唤醒的线程切换的开销。而非公平锁减少了线程挂起的几率。

10.2.5)读写锁

可以允许同时多个读;但是只允许只有一个写,其他线程等待。

  • 11 Semaphore

用来控制同时访问特定资源的线程数量,通过协调各个线程,保证合理使用公共资源。可用于 流量控制 。比如批量处理数据,防止线程过多,导致 数据库连接池 爆满

  • 12 Condition

12.1)底层是等待队列,模拟线程间协作。object await,notify,ntoifyAll,

12.2)实现阻塞队列,在LinkBlockQueue,ArrayBlockQueue,LinkedBlockingDeque应用

  • 13 synchronized与ReentrantLock区别

13.1)ReentrantLock是API界别,synchronized是jvm界别

13.2)ReentrantLock需要手动关闭锁,synchronized不需要

13.3)ReentrantLock可以中断锁,获取锁状态

13.4)ReentrantLock可以配合condition使用,绑定多个条件

13.5)ReentrantLock实现读写锁

  • 14 CountDownLatch 与CyclicBarrier

14.1)CountDownLatch就好比是马拉松比赛,跑完的人不用等待其他选手是否结束,减一通过CAS, CountDownLatch只能一次操作 countdown()CAS state减一,state减为0,主线程被唤醒。 当我们调用countdown()的时候,会对计数器进行CAS减一操作,AQS内部通过释放锁的方式,对state进行减一,当state减为零时,代表计数器已经递减完毕,会umpark主线程,此时AQS阻塞队列中的节点线程全部唤醒

14.2)CyclicBarrier可以循环操作,也就是多次wait,notify, CyclicBarrier底层是配合condition进行阻塞和唤醒的

CyclicBarrier底层没有实现AQS,而是通过ReenTrantLock实现同步机制

ReenTrantLock AQS

Semaphore AQS

Condition 阻塞 唤醒

ArrayBlockingQueue poll通过 ReenTrantLock配置获取到锁,Condition配合控制队列容量,是否empty,是否full

CountDownLatch AQS 通过释放锁,state减一,当state变为0的时候,主线程被唤醒

CyclicBarrier 本身没有实现AQS,通过ReenTrantLock实现同步

  • 15)队列对比

15.1)ArrayBlockingQueue

基于数组的阻塞队列实现,在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,我们可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。

15.2)LinkedBlockingQueue

基于链表的阻塞队列,其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

使用要指定容量,默认为 Integer .MAX_VALUE

15.3)PriorityBlockingQueue

基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

  • 16 CAS

16.1)AtomicInteger

16.2)缺点

16.2.1)ABA问题 可通过AtomicStampedReference解决

16.2.2)循环时间开销大

16.2.3)只能保证一个共享变量的 原子操作

16.3)可重入锁好处

16.3.1)避免线程切换带来的资源消耗

16.3.2)避免 死锁

  • 17 ThreadLocal

底层原理

ThreadLocalMap key是ThreadLocal 的弱引用,value 是强引用

内存泄露

ThreadLocal弱引用,突然是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap 生命周期 Thread 的一样,它不会回收。

这时候就ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

使用场景

数据库连接, Session 会话管理,线程间数据隔离

  • 18 线程池

18.1)参数:

核心线程数,最大线程数,空闲线程存活时间,空间线程存活时间单位,工作队列,线程工厂,拒绝策略

过程:核心线程->工作队列->最大线程->拒绝策略

拒绝策略:抛异常,丢弃,丢弃最早的,由调用线程处理该任务(重试)

newFixedThreadPoolExecutor,newSingleThreadExecutor工作队列为LinkBlockQueue,无界 当心内存溢出

newCachedThreadPool,newScheduledThreadPool最大线程数为Integer.Max

CPU 密集型:线程数取cpu核数

IO密集型:通常是 cpu 核数的两倍的线程

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

18.2)线程如何让出cpu

18.2.1)Thread.sleep

18.2.2)Thread.yield 线程让步

18.2.3) Thread.currentThread().suspend() 线程挂起

18.2 .4) object wait

18.2 .5)condition await

18.2 .6)LockSupport.park

18.2 .7)Thread.stop

18.3)底层原理

18.3.1)mainLock锁

对workers(线程池中的线程集合)操作的时候(如添加一个worker到工作中), interrupt所有的 workers, 调用 shutdown 方法等

18.3.2)线程集合 Hash Set workers = new HashSet();

用来保存当前线程池中的所有线程,对线程池中的线程进行中断遍历

18.4)线程的几个状态

new runable running blocked

WAITING TIMED_WAITING

文章来源:智云一二三科技

文章标题:Java锁——2022最新Java面试八股文

文章地址:https://www.zhihuclub.com/195296.shtml

关于作者: 智云科技

热门文章

网站地图