社招高频面试题

embedded/2024/10/21 7:45:14/

1.单例模式

面试突击50:单例模式有几种写法?

2.Mybatis缓存机制

MyBatis的一、二级缓存查询关系

一级缓存是SqlSession级别,不能跨SqlSession共享,默认开启。

二级缓存是基于mapper namespace级别的,可以跨SqlSession共享,通过配置开启。在开启二级缓存的状况下,查询数据的顺序为二级缓存→一级缓存→数据库。

一级缓存实现原理:

  • 当你执行查询后,查询的结果会被放在SqlSession创建时由MyBatis创建的一个本地缓存中。
  • 当你执行其他查询,MyBatis会检查缓存中是否有相同的查询。
  • 如果有,就会从缓存中拿结果,不会再去数据库查询。
  • 当SqlSession调用commit()方法时,MyBatis会清空其缓存。

二级缓存实现原理:

  • 二级缓存的作用范围是一个mapper namespace,多个SqlSession共享。
  • 二级缓存会将查询结果存储到一个缓存区域,这个缓存区域是按namespace分的。
  • 当一个SqlSession关闭或提交后,其中的查询结果会被存入二级缓存。
  • 当另一个SqlSession执行相同的查询时,它会检查二级缓存,如果有就会从二级缓存中获取结果。

在这里插入图片描述
本地缓存是在Executor内部构建,Executor包含了三个实现类,SimpleExecutor,BatchExecutor以及CachingExecutor,其中CachingExecutor是开启了二级缓存才会用到的,这里主要是SimpleExecutor和BatchExecutor,他们都实现了BaseExecutor,而BaseExecutor中正是进行了一级缓存的处理。

  • BaseExecutor:基础执行器,封装了子类的公共方法,包括一级缓存、延迟加载、回滚、关闭等功能;
  • BatchExecutor : 通过批量操作来提高性能。(执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。)
  • ReuseExecutor: 重复使用执行,其定义了一个Map<String, Statement>,将执行的sql作为key,将执行的Statement作为value保存,这样执行相同的sql时就可以使用已经存在的Statement,就不需要新创建了。(执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。)
  • SimpleExecutor通过类名可以看出,它是一个简单的执行类,并不会做一些处理就执行sql。(每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象)。
  • CacheExecutor有一个重要属性delegate,它保存的是某类普通的Executor,值在构照时传入。执行数据库update操作时,它直接调用delegate的update方法,执行query方法时先尝试从cache中取值,取不到再调用delegate的查询方法,并将查询结果存入cache中。(上述三种模式的装饰器模式)。

Mybatis源码阅读之–本地(一级)缓存实现原理分析

mybatis 源码分析(三)Executor 详解

3.分布式环境下的缓存一致性问题

在分布式环境下建议禁用MyBatis的一级、二级缓存,否则可能出现数据一致性问题。二级缓存虽然默认关闭,建议设置settings中配置:cacheEnabled 为false,全局关闭此。一级缓存默认开启而且不能关闭,可以设置localCacheScope属性设置为STATEMENT,查询时清空一级缓存。

MyBatis之缓存避坑: MyBatis的一级缓存、二级缓存的实现分析与分布式环境下数据一致性问题

4.全面解析 Redis 持久化:RDB、AOF与混合持久化

RDB 持久化方式是 Redis 将当前内存中的数据快照(snapshot)保存到硬盘的过程。换句话说,Redis 会创建一个代表某一时刻的数据集的磁盘文件。

例子: 想象一下相机的快门点击。每当你点击快门,你都会捕捉到那个特定时刻的场景。RDB的工作方式很相似,只不过它捕捉的是数据的状态。

RDB触发条件:理解 RDB 的本质后,你可能会问,我们如何生成这个快照呢?使用 SAVE 和 BGSAVE 命令即可。

手动触发:通过执行 SAVE 或 BGSAVE 命令。

自动触发:基于 Redis 配置文件中的 save 指令设置的条件。(默认是通过 BGSAVE 命令来触发的)

redis 配置文件 save 指令设置:
save 3600 1 # 3600秒内如果超过1个key被修改则生成 RDB
save 300 100 # 300秒内如果超过100个key被修改则生成 RDB
save 60 10000 # 60秒内如果超过10000个key被修改则生成 RDB

AOF(Append Of File)
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写执行从前到后执行一次以完成数据的恢复工作。

AOF 持久化的实现主要是以上三步:命令追加、文件写入、文件同步

命令追加: 将 redis 写操作命令追加到 aof_buf 缓冲区
文件写入: 周期性地将 aof_buf 缓冲区的命令写入 AOF 文件的内核缓冲区。
文件同步:根据配置同步策略,将 AOF 文件缓冲区的内容同步到磁盘。
其中文件同步策略 redis 提供了三种,分别是以下三种:

always:每次有命令写入时都立即同步。这提供了最高的数据安全性,但效率最低。
everysec:每秒同步一次。这是一个权衡安全性和效率的策略。最多只丢失 1 秒 的数据
no:让操作系统决定最佳的同步时间。这可能导致数据丢失,但提供了最高的效率。

RDB和AOP混合模式

在这里插入图片描述

混合持久化在 AOF 重写时,会首先将当前数据集以 RDB 格式快照的形式写入新 AOF 文件的开始位置,然后再追加新的写命令到文件末尾。

复制代码
(1)生成混合持久化文件:
Redis 在某个时刻生成了一个 RDB 快照,将其保存到 AOF 文件的前缀部分。
之后的所有写操作(例如 SET、DEL 等)都记录在 AOF 文件的后半部分。
假设 AOF 文件如下表示:

(2)恢复过程:
Redis 启动时,首先读取 AOF 文件中的 RDB 快照部分,快速加载数据集。
然后,Redis 继续读取 AOF 文件中的写操作日志,并逐个重放这些操作,以确保数据集与持久化时一致。
恢复后的顺序:

Redis 持久化模式概述

全面解析 Redis 持久化:RDB、AOF与混合持久化

微服务的特性

微服务的主要特性包括:

  • 粒度更细的服务‌:微服务架构强调按业务边界进行细粒度的服务拆分,每个服务的功能和职责单一。这要求工程师充分理解和洞察业务领域的边界,保证服务自包含、独立部署和独立演进‌。
  • 松耦合‌:服务之间是松耦合的,即服务之间的相互依赖程度低。高内聚要求将相关的元素和行为聚集在一起,而低耦合则是降低服务之间的相互依赖和相互作用‌。‌
  • 独立部署‌:每个服务可以单独独立部署,这意味着可以对单个服务进行更新和维护,而不影响其他服务‌。‌
  • 技术栈的多样性‌:每个服务可以使用不同的技术栈,包括不同的数据库和数据管理模型。通过REST API、事件流和消息代理组合彼此通信‌。

微服务的优点包括:

  • ‌提高系统的可伸缩性和可维护性‌:通过将大型单体应用拆分成多个小服务,可以更灵活地扩展和维护系统。每个服务可以独立部署和扩展,提高了系统的可伸缩性和可维护性‌。‌
  • 促进快速开发和部署‌:由于每个服务相对独立,开发人员可以并行开发不同的服务,加快开发速度。同时,服务的独立部署也使得新功能的上线更加迅速‌。‌
  • 提高系统的稳定性和可靠性‌:通过将系统拆分成多个服务,即使某个服务出现问题,也不会影响整个系统,提高了系统的稳定性和可靠性‌。

常见的阻塞队列?及其使用场景

在这里插入图片描述

JDK7提供了7个阻塞队列。分别是
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

,ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。

DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:

  • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  • 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。

SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。

LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。

  • transfer方法。如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
  • tryTransfer方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,以First单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另外插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后缀的方法更清楚。

线程池原理?

1,为什么要使用线程池

  • 降低创建线程和销毁线程的性能开销
  • 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
  • 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题
    2,线程池有哪几种类型
    Executors 的工厂方法,就可以使用线程池:
  • newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。
  • newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
  • newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收
  • newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
    3,线程池有哪几种工作队列?
    ArrayBlockingQueue (有界队列):是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    LinkedBlockingQueue (无界队列):一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    SynchronousQueue(同步队列): 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    DelayQueue(延迟队列):一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。指定时间到了之后,才能出队列;队列头的离出队时间最近。
    PriorityBlockingQueue(优先级队列): 一个具有优先级的无限阻塞队列。

ArrayListQueue、LinkedBlockingQueue、SynchronousQueue是阻塞队列
有界队列:ArrayBl
无界队列:

  • 有界队列
    ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
    LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
  • 无界队列
    PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
    DelayQueue:一个使用优先级队列实现的延迟无界阻塞队列。

SynchronousQueue:一个内部只能包含一个元素的队列。

4. 怎么理解无界队列和有界队列
有界队列即长度有限,满了以后ArrayBlockingQueue会插入阻塞。
无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞,但是可能会出现OOM异常。
5. 线程池中的几种重要的参数及流程
在这里插入图片描述
1.向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,如果不大于,就创建一个核心线程来执行任务。
2.如果大于核心线程数,就会判断缓冲队列是否满了,如果没有满,则放入队列,等待线程空闲时执行任务。
3.如果队列已经满了,则判断是否达到了线程池设置的最大线程数,如果没有达到,就对线程做扩让,创建临时线程来执行任务。
4.如果已经达到了最大线程数,则执行指定的拒绝策略。这里需要注意队列的判断与最大线程数判断的顺序,不要搞反。

java">public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;
}
  • corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
  • maximumPoolSize:线程池最大线程数最大线程数
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止
  • unit:参数keepAliveTime的时间单位TimeUtil类的枚举类(DAYS、HOURS、MINUTES、SECONDS 等)
  • workQueue:阻塞队列,用来存储等待执行的任务
  • threadFactory:线程工厂,主要用来创建线程
  • handler:拒绝处理任务的策略
    ----AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认这种)
    ----DiscardPolicy:也是丢弃任务,但是不抛出异常
    ----DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    -----CallerRunsPolicy:由调用线程处理该任务

6.线程池的数量如何确定?
在遇到这类问题时,先冷静下来分析

  1. 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
  2. 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
    如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
    如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
    一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/线程 CPU 时间 )* CPU 数目
    这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)

volatile关键字

volatile是Java中的一个关键字,它的主要作用是用来保证可见性和禁止指令重排序。

volitile关键字:
Volitile关键字:保证内存可见性,禁止指令重排序

内存可见性问题:在多线程环境下,读和写发生在不同的线程中,可能会出现:读线程不能及时的读取到其他线程写入的最新的值,这就是所谓的可见性问题。

从硬件层面:CPU/内存/IO设备之间存在读取速度的差距和存储大小的差异;为了减小各类型设备之间的读取效率差异,增加CPU的处理效率;CPU层面增加了高速缓存,但是带了缓存一致性问题,

缓存执行问题的两种解决方案:

  • 总线锁,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个LOCK#的信号,这个信号使得其他CPU无法通过总线来访问到共享内存中的数据,总线锁定把CPU和内存之间的通信锁住了,在锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁的开销比较大,这种机制显然是不合理的。

缓存一致性机制就整体来说,是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,从内存中重新读取,用MESI阐述原理如下:

  • 缓存锁,最好的办法是控制控制锁的保护粒度,我们只需要保证对于被多个CPU缓存的同一份数据是一致的就行。在P6架构的CPU后,引入了缓存锁,如果当前数据已经被CPU缓存了,并且是要写回到主内存中的,就可以采用缓存锁来解决问题。MESI有四种状态,E独占,M修改,S共享,I无效,根据MESI,CPU某核(比如CPU1)的缓存行(包含变量x)是M、S或E的时候,如果总线嗅探到了变量x被其他核(比如CPU1)执行了写操作(remote write),那么CPU0的缓存行会置于无效I(无效),在CUP0后续对该变量的操作的嘶吼发现是I状态,就会去主存中同步最新的值。
    Store Bufferes(写缓冲区)

通过内存屏障禁止指令重排序
X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)

Store Memory Barrier(写屏障) ,告诉处理器在写屏障之前的所有已经存储在存储缓存(storebufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的
Load Memory Barrier(读屏障) ,处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
Full Memory Barrier(全屏障) ,确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作

JMM层面:
经过上面的分析,我们已经知道了volatile变量可以通过缓存一致性协议保证每个线程都能获得最新值,即满足数据的“可见性”。

除了显示引用volatile关键字能够保证可见性以外,在Java中,还有很多的可见性保障的规则。
从JDK1.5开始,引入了一个happens-before的概念来阐述多个线程操作共享变量的可见性问题。所以
我们可以认为在JMM中,如果一个操作执行的结果需要对另一个操作课件,那么这两个操作必须要存在
happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程

synchronized关键字

Syncchronized锁住了什么?
Synchronized锁的3种使用形式(使用场景):
Synchronized修饰普通同步方法:锁对象当前实例对象;
Synchronized修饰静态同步方法:锁对象是当前的类Class对象;
Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象,这个对象可以是某个对象(xlock),也可以是某个类(Xlock.class);

Syncchronized的锁存在哪里?

对象是存放在堆内存中,对象大致可以分为三个部分,分别是对象头,实例变量,填充字节。

对象头主要是由MarkWord和Klass Point(类型指针组成)。
实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
填充字符,因为虚拟机要求对象字节必须是8字节的整数倍。
Syncchronized锁的对象存储在对象头的MarkWord中。每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器

锁分为四种类型:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
除了 偏向锁可以转换为无状态锁,其他的锁都只能升不能降。

偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

总结:偏向锁:只有一个线程进入临界区;
轻量级锁:多个线程交替进入临界区;
重量级锁:多个线程同时进入临界区。

volatile与 synchronized的区别

通过使用volatile关键字,是强制的从公共内存中读取变量的值, 是线程同步的轻量级实现, 所以volatile性能肯定比synchronized要好,并且 volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。
随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。


http://www.ppmy.cn/embedded/129211.html

相关文章

【软件测试】JUnit

Junit 是一个用于 Java 编程语言的单元测试框架&#xff0c;Selenium是自动化测试框架&#xff0c;专门用于Web测试 本篇博客介绍 Junit5 文章目录 Junit 使用语法注解参数执行顺序断言测试套件 Junit 使用 本篇博客使用 Idea集成开发环境 首先&#xff0c;创建新项目&#…

Linux学习第二天

3.vmware的功能&#xff1a; 快照 创建快照&#xff1a; 拍摄此虚拟机的快照&#xff1a;记录保存虚拟机的当前状态&#xff0c;如果系统出现故障&#xff0c;可以通过快照还原&#xff08;错删系统时可以找到快照的系统状态&#xff0c;然后恢复系统&#xff09; 恢复快照…

WSL2配置代理解决git网络不通畅的问题

简述 书接上文&#xff0c;在WSL2下使用CrossSim&#xff0c;git的时候网络很差&#xff0c;通过代理解决这个问题。 旧版的解决方案一般是通过cat /etc/resolv.conf获取IP然后配置主机的端口&#xff0c;这样有时候并不能访问&#xff0c;并且一般会出现该问题&#xff1a;ws…

【人工智能-初级】第3章 k-最近邻算法(KNN):分类和Python实现

文章目录 一、KNN算法简介二、KNN算法的工作原理2.1 欧氏距离 三、K值的选择四、KNN算法的优缺点4.1 优点4.2 缺点 五、Python实现KNN分类5.1 导入必要的库5.2 加载数据集并进行预处理5.3 创建KNN分类器并进行训练5.4 模型预测与评估5.5 可视化K值对模型性能的影响 六、总结6.1…

《京东金融APP的鸿蒙之旅系列专题》鸿蒙工程化:Hvigor构建技术

作者&#xff1a;京东科技 杨拓 一、构建工具概述 Hvigor构建工具是一款基于TypeScript实现的构建任务编排工具&#xff0c;专为提升构建和测试应用的效率而设计。它主要提供以下关键功能&#xff1a; 1.任务管理机制&#xff1a;包括任务注册和编排&#xff0c;帮助开发者高效…

外包干了3周,技术退步太明显了。。。。。

先说一下自己的情况&#xff0c;大专生&#xff0c;21年通过校招进入武汉某软件公司&#xff0c;干了差不多3个星期的功能测试&#xff0c;那年国庆&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我才在一个外包企业干了3周的功…

个人主页模版(源代码开源)

个人主页模版&#xff08;源代码开源&#xff09; 项目预览地址&#xff1a;My Website Github仓库地址&#xff1a;personal_page_public Hello,这里是个人主页模版代码的开源页。 源代码自取&#xff0c;就在项目文件中。当然&#xff0c;你也可以在 r e l e a s e s rel…

【无标题】react组件封装

子组件制作 import { useState,useRef, useEffect} from "react"const Table (data)> {const {value ,option} dataconsole.log(value)const [stata,setValue] useState()const useRefs useRef(value)useEffect(()> {useRefs.current.value value })c…