2023 Java后端面经精简(锁篇)

2023 Java后端面经精简(锁篇)_第1张图片

Synchronizd:

是由JVM提供的关键字,可以作用在类和方法上,锁如果写在类或者静态方法上锁住的是这个类,如果写在方法上锁住的是这个实例。
加锁即一个线程拿到monitor对象,会改变对象头中相关的锁信息,锁信息中保存了monitor对象的起始地址,当一个monitor被一个线程持有后,它便被加锁了,而monitor(管程对象)在JVM虚拟机中是通过ObjectMonitor(C++)实现的,ObjectMonitor保存了两个队列,_WaitSet和_EntryList,用于保存ObjectWait(等待锁的线程)队列,当多个线程同时进入同一同步代码块是,首先会进入EntryList集合中,当线程获取到对象的monitor后会进入Owner区域并将计数加一,若线程调用wait方法,将释放所持有的monitor,Owner变量回复为null,计数-1,同时进入waitset队列。若当前线程执行完毕也将释放monitor。
2023 Java后端面经精简(锁篇)_第2张图片

在字节码中 如果是synchronized是写在代码块上那么是通过monitorEnter和monitorExit实现的,如果是在方法上,那么就有一个ACC_synchronized标志来标记这是一个同步方法。jdk1.6之后,对synchronized进行了优化,会进行锁优化的一个过程,锁升级的过程:无锁,偏向锁,轻量级锁,重量级锁。偏向锁是通过修改对象头中的mark word,线程拿到锁后,会设置线程ID和是否为偏向锁到信息中。轻量级锁的话是通过CAS(比较并替换)来实现的,CAS的底层是通过一个标志位的增加,增加到某个值后来实现的。
2023 Java后端面经精简(锁篇)_第3张图片

别人整理的非常清楚的图

ReetrantLock(基于AQS):

是api级别的,在lock()之后必须unlock()。可以绑定多个条件(Condition),可以实现可轮询锁和可中断锁、公平锁、定时锁。
两者的共同点是 都控制了多线程对共享对象的访问,都是可重入锁,都保证了可见性和互斥性。

乐观锁和悲观锁该如何选择:

乐观锁的思想是该线程在读取数据的时候其他线程不会修改数据。通常的实现方法是CAS
悲观锁的思想是该线程在读取数据的时候其他线程会修改数据。
乐观锁的优势是可以提高并发访问效率,如果发生冲突的话会往上抛,然后再重来一次。悲观锁的优势是可以避免发生冲突,但是会降低效率。
选择哪一种锁的话是看访问效率和一旦发生冲突的严重性。如果系统被并发访问的概率很低,并且发生冲突后的结果不太严重的话,可以使用乐观锁,否则使用悲观锁。
当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

volatile:

保证对象的可见性和有序性
可见性是通过控制 线程直接与主内存进行数据更新操作来保证的,因为线程一般都有自己的线程缓存,设置volatile就是让线程跳过自己的缓存。
有序性是通过在指令执行顺序中增加内存屏障来实现,保证对象的有序性
volatile不能保证原子性
其使用需要满足两个条件:1.对变量的写操作不依赖于当前值 2.该变量没有包含在其他变量的不变式中

JUC(java.util.concurrent):

AQS(相当重要):

顾名思义一个抽象的队列同步器,通过维护一个volatile state变量和一个先进先出的线程等待队列来实现。加锁的话 先通过CAS的方法判断当前state是否为0,如果为0,就用setExclusiveOwnerThread(Thread.currentThread())方法来加锁,如果不为0,则走acuqire()方法,acquire方法里会再次判断state的值,如果是0加锁,不是0就判断是否为同一线程,如果是则变为可重入锁,state+1,不是同一线程的话就将线程加到等待队列中。unlock的话 是调用release方法,让其释放锁,同时调用unparkSuccessor方法唤醒阻塞线程。

资源共享的方式有两种:
1.独占式 (ReentrantLock)
2.共享式(Semaphore,CountDownLatch)
2023 Java后端面经精简(锁篇)_第4张图片
2023 Java后端面经精简(锁篇)_第5张图片

Semaphore(基于AQS):

一种基于计数的信号量,可以通过设置一个阈值来控制可运行的线程的数量。通过acquire()方法来获取许可。

CountDownLatch(基于AQS):

也是一种计数的形式,通过设置一个阈值来控制一共可以运行线程的数量,使用countDown()进行计数,使用await()方法阻塞主线程等待所有线程(线程数量等于阈值数量的)完成。

CyclicBarrier:

与CountDownLatch相似多次获得锁对象
区别:
CountdownLatch适用于所有线程通过某一点后通知方法,而CyclicBarrier则适合让所有线程在同一点同时执行
CountdownLatch利用继承AQS的共享锁来进行线程的通知,利用CAS来进行–,而CyclicBarrier则利用ReentrantLock的Condition来阻塞和通知线程

FutureTask(基于Runnable和Future):

提供了get()方法获取结果,isCancelled()方法获取线程是否取消(包括中断),isDone()方法可以查看线程是否完成。

ForkJoinPool(基于AbstractExecutorService):

用于解决父子任务有依赖并行计算问题,是分而治之的思想。
使用方法:
1.定义 RecursiveTask 或 RecursiveAction 的任务子类。
2.初始化线程池及compute()方法计算任务,丢入线程池处理,取得处理结果。

ThreadPoolExecutor(基于AbstractExecutorService):

一种常规的线程池的创建方式,核心参数有 :

  1. 核心线程数量(设置数量时注意的点是看任务时更需要IO读写还是更需要CPU运算,如果是更多的是IO操作,那就属于IO密集型,可以参考的设置是核心数/2,如果是更需要CPU运算,就是属于CPU密集型,可以参考的设置是核心数+1)
  2. 最大线程数量
  3. 允许非核心线程完成任务后的最大存活时间
  4. 前一个时间的时间单位
  5. 阻塞队列(常用的:ArrayBlockingQueue 基于数组的有界阻塞队列、LinkedBlockingQueue
    基于链表的有界阻塞队列、PriorityBlockingQueue 支持优先级排列的无界阻塞队列)
  6. 线程池工厂
    线程池的拒绝策略(AbortPolicy,直接抛出异常,阻止线程正常运行、CallerRunsPolicy,如果被抛弃的线程任务未关闭,则执行该任务(相当于重试)、DiscardOldestPolicy,移除线程队列中最早的一个任务,并尝试执行当前任务、DiscardPolicy,丢弃当前线程任务并不做任何处理、自定义拒绝策略)

执行流程:
任务进来,线程池会先判断当前线程数是否小于核心线程,如果小于则会将该任务添加到核心线程中,如果大于则添加到任务队列中,如果队列未满,就会通过非核心线程来执行队列任务,如果队列添加任务失败,就会创建非核心线程来执行任务。当创建非核心线程大于最大线程数,就会执行线程池的拒绝策略。
2023 Java后端面经精简(锁篇)_第6张图片
2023 Java后端面经精简(锁篇)_第7张图片

Atomic类:

底层是基于CAS实现的,可以实现++操作的原子性
2023 Java后端面经精简(锁篇)_第8张图片

分布式锁(基于Redis)

Redisson 在基于 NIO 的 Netty 框架上,充分的利⽤了 Redis 键值数据库提供的⼀系列优势,在 Java 实用⼯具包中常⽤接⼝的基础上,为使⽤者提供了⼀系列具有分布式特性的常用工具 类。使得原本作为协调单机多线程并发程序的⼯具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特⾊的分布式服务,更进⼀步简化了分布式环境中程序相互之间的协作
2023 Java后端面经精简(锁篇)_第9张图片

你可能感兴趣的:(java)