本文记录一些零散的小知识,有关redis,jvm,java等。主要是最近面试积累下来的问题。
- redis使用什么语言编写? 答案:使用ANSI C。即标准C语言编写。
- redis内部数据结构? 答案:对用户可用的外部数据结构是string、list、hash、set、sorted set五个结构。而实现这些结构,内部使用的是dict、sds、ziplist、quicklist、skiplist五个结构相互配合实现的(其实不止这5个,还有一些其他的,主要的是这5个)。 其中,dict是一种基于哈希的字典,和java的hashmap实现类似,也是通过数组加链表并且自动扩容来完成的。区别在于扩容时执行的rehash并不是原地立刻执行,而是增量式执行,分散到后续的get,put等操作中分步执行,最大限度减小单次请求的响应时间。其内部有两个数组,平时只使用一个数组,而处于rehash状态时将同时使用两个数组,并有一个指向旧数组的指针来记录rehash的进度。新数组的容量是旧数组的两倍,当get的时候会从两个数组中取数据并顺便将旧数组的数据往新数组拷贝,put的时候则直接在新数组中插入,最终,旧数组全部完成后,由新数组作为旧数组,旧数组则回收掉。 sds则是一种简单动态字符串结构,其代表的字符串可以动态修改追加且二进制安全(不受字符编码影响)。主要用来实现面向用户的
string
这种结构。 ziplist听名字就知道,是一个特殊编码的双向链表。其内部支持存放整数和字符串两种类型,以常数时间复杂度从两头存取数据。虽然是链表,它却和大多数链表实现不同,它没有指针,数据是紧凑地放在一大块内存之中,且采取紧凑的变长编码方式存储数据,不会有空隙或者分隔符之类的东西,其内部会记录偏移量来代替指针,其数据是按照小端模式存储的。之所以这么做是因为这样内存利用率更高,产生碎片可能性更小。该结构主要用来实现用户可见的hash
结构,不过,当键值对超过512的时候,会使用dict来代替ziplist,或者插入value长度大于64的时候。这是因为,ziplist虽然像链表,但是其实不擅长数据的修改,某个数据变长时,少不了内存拷贝操作。另外,从中间查找数据时,需要遍历,时间复杂度为O(n),和dict哈希表还是没法比的。 quicklist也是一种双向链表,但是它是真的具有指针的双向链表,而每个节点之中,又有一个ziplist组成。很明显,这种结构即想使用ziplist带来的好处,又想弥补ziplist带来的缺陷。因此,每个节点中的ziplist可存放的数据数目是有限的,既不能太多又不能太少。该结构主要用来实现用户可见的list
结构,当节点数太多时,redis支持使用压缩算法将中间的节点压缩,进而提高某些应用场景(两头存取数据频繁,中间存取数据不频繁)的性能。 skiplist也是一种双向链表,但是是一种特殊的双向链表,即跳跃表。有些节点除了指向下个节点外,还有可能指向下下个节点,在这种结构下,能够提高查找的性能,理想情况下,查找性能可以提升到O(logN),和二叉查找树性能相当。而且这种结构实现起来比树简单,平均指针数更少,插入删除操作不需要重新调整多个节点。另外,这个链表和树一样,其节点总是有序排列的,每个节点也都有key和value组成。这种结构是为了支持面向用户的sorted set
结构。但是,请注意sorted set并不是只由skiplist完成的,还需要dict和ziplist的帮助。 intset是在数据量较小时,且存储数据是整数时,实现面向用户的Set
格式的。intset与ziplist差不多,区别在于其只能存储整数,且是有序的,便于进行二分查找。intset一旦选定了数据项的编码,所有数据项的长度就定死了,采取的是非变长编码方式(不过根据后续插入的数据,会自动升级编码,所有数据项的长度也会随之改变)。intset使用二分查找,时间复杂度应该是O(logN)。
- redis网络模型?redis单线程为什么还这么快? 原因有以下几点:
- redis是基于内存的key-value数据库,内存读取数据非常迅速,因此,唯一的瓶颈在于网络IO消耗。
- redis解决网络IO的手段是多路IO复用模型,虽然是单线程,但是利用多路IO复用模型可以高效处理网络事件。首先它有自己实现的网络事件驱动代码,来代替libevent这样重量级的网络库。它有一个事件循环中心(EventLoop),其中有一个名为event的list保存它注册的网络IO事件,在没有事件的时候,线程主要使用特定平台的底层方法来轮询事件,当有事件发生的时候,会使用预先注册的回调函数来处理事件。事件循环中心还有一个叫fired的list,用来存放轮询到的发生的事件句柄,并按照顺序一个一个地使用回调函数处理。 单线程好处:
- 代码清晰逻辑简单,早期的redis只有五千多行代码
- 不用考虑锁和并发问题
- 不存在多线程切换开销
- spring中ApplicationContext和beanfactory区别? ApplicationContext是beanFactory的包装,也可以说是继承关系。 beanFactory初始化bean是延迟的,ApplicationContext是启动时立即初始化。 ApplicationContext额外提供国际化、资源访问、事件传播、AOP、分层上下文等功能。 beanFactory的processor需要手动注册。
- spring中xml定义相同的bean会怎样? 同名的bean,后面会覆盖前面的,context里面也有一个变量作为开关,false的时候,直接报错。 如何控制这个开关呢? 见代码:
<servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-mvc.xml</param-value> </init-param> <!-- 增加如下配置 --> <init-param> <param-name>contextInitializerClasses</param-name> <param-value>com.zyr.web.spring.SpringApplicationContextInitializer</param-value> <!--这个类与方案2中的是一个类 --> </init-param> <load-on-startup>1</load-on-startup></servlet>
其中SpringApplicationContextInitializer是自定义的,实现了ApplicationContextInitializer接口的类。在这个类中可以设置这个变量值。
- spring中ApplicationContext和Spring MVC的那个容器的关系和区别? 首先,web应用有一个web.xml,由服务器去根据这个文件初始化一个叫做servletContext的上下文。然后,我们需要通过listener,注册ContextLoaderListener,这个listener会在servletContext初始化好后收到事件,然后创建一个WebApplicationContext,这个上下文是Spring的根容器,我们绝大多数的DAO和Servcie的Bean都注册在这个容器里。然后SpringMVC需要我们注册一个或多个DispatcherServlet,这个servlet就用来分发http请求给controller。这个Servlet在初始化的时候,自己也会创建WebApplicationContext,并保持根容器的引用。每个servlet都有自己的context,并且都保持根容器的引用。因此,在查找一个bean的时候,子容器找不到,还会在父容器里找一下子。根容器维护全局共享的bean,而每个DispatcherServlet维护专属自己的Bean,默认情况下这些bean是通过xx-servlet.xml来声明的。无论是根容器还是子容器,都会与ServletContext建立绑定关系,双方都能拿到对方。 附加问题:根据根容器可以拿到子容器吗? 答案:可以,但是不可以直接拿到,先拿到ServletContext,然后拿出所有的Attribute,遍历,并一个一个去鉴定是否是其子容器,鉴定方法是调用子容器的getParent方法拿到父容器,然后依次比较是否和根容器相等。在Spring Mvc环境默认配置下,由于只有唯一的一个父容器,那么只需要判断拿到的父容器不为空,就可以了。
- ConcurrentHashMap读加锁吗? 答案:在1.7+版本中,读不加锁,而是通过UNSAFE提供的原子方法来实现的,其读取的时候使用volatile语义实现不加锁也能并发读。1.6以及以下版本则在寻找entry的时候不加锁,在某些条件下读取value会加锁。其中,无论是每个分段内部的数组还是每个节点内部的value值,都是volatile。
- java都有那些线程池? 主要是Executors类中提供的一系列静态方法创建出的线程池。这些线程池其实都是ThreadPoolExecutor和ScheduledThreadPoolExecutor基于不同配置实现的。下面罗列出来:
- newFixedThreadPool:固定线程数的线程池,线程至多创建int入参个线程,任务放在LinkedBlockingQueue阻塞队列中,该队列长度为Integer最大值,当线程不够用时,任务就要在该队列中等待,等线程空闲了去消费。
- newSingleThreadExecutor:单线程的线程池,任务放在LinkedBlockingQueue阻塞队列中,如果那个单线程死了,会有新的线程创建顶替它,确保始终有一个线程,并且同一时刻只有一个任务在执行。该线程池一旦创建就是不可变的。
- newCachedThreadPool:缓存线程的线程池。线程数可以从0一直扩张到Integer最大值,每个线程会在闲置后在池子里再活一分钟,如果一分钟都没有任务过来,就销毁这个线程。线程池的队列用的是SynchronousQueue,这个队列中没有存放数据的地方,每个Put都要等待一个take操作,反之亦然。
- newSingleThreadScheduledExecutor:单线程的周期性线程池。内部使用DelayedWorkQueue。
- newScheduledThreadPool:固定核心线程数的周期性线程池。内部使用DelayedWorkQueue。
- Mysql语句的执行顺序? FORM: 对FROM的左边的表和右边的表计算笛卡尔积。产生虚表VT1 ON: 对虚表VT1进行ON筛选,只有那些符合的行才会被记录在虚表VT2中。 JOIN: 如果指定了OUTER JOIN(比如left join、 right join),那么保留表中未匹配的行就会作为外部行添加到虚拟表VT2中,产生虚拟表VT3, rug from子句中包含两个以上的表的话,那么就会对上一个join连接产生的结果VT3和下一个表重复执行步骤1~3这三个步骤,一直到处理完所有的表为止。 WHERE: 对虚拟表VT3进行WHERE条件过滤。只有符合的记录才会被插入到虚拟表VT4中。 GROUP BY: 根据group by子句中的列,对VT4中的记录进行分组操作,产生VT5. CUBE | ROLLUP: 对表VT5进行cube或者rollup操作,产生表VT6. HAVING: 对虚拟表VT6应用having过滤,只有符合的记录才会被 插入到虚拟表VT7中。 SELECT: 执行select操作,选择指定的列,插入到虚拟表VT8中。 DISTINCT: 对VT8中的记录进行去重。产生虚拟表VT9. ORDER BY: 将虚拟表VT9中的记录按照进行排序操作,产生虚拟表VT10. LIMIT:取出指定行的记录,产生虚拟表VT11, 并将结果返回。
- redis一致性哈希分区? 当redis使用集群来存储的时候,可以使用分区的方式将过量的数据分散到集群的不同机器中。 然而,如何分配数据以及在增删节点时仍然保证合理分配数据就成为一个问题。 常见的做法是范围分区,有一个记录键在某个范围应该属于那个机器的分区表。这样的好处是实现简单,缺点是需要额外维护分区表。 另一种做法是使用哈希分区,哈希分区即使用CRC32等哈希函数将键值转化为一个数字哈希值,然后通过对节点数取余的方式来确定数据究竟分到哪个节点。这种方法也有缺陷,主要是在增删节点时,会影响几乎所有数据的节点分配,导致绝大多数数据分配在增删节点前后出现不一致的情况。 针对哈希分区的缺点进行改进的算法就叫一致性哈希分区。它的算法:将所有可选的哈希值组成一个环状的数值空间,集群中的每个机器节点通过将自己的IP地址或者机器名算出的哈希值,对应到环中的某个数值上。当分配某数据时,算出其哈希值并对应到环上,然后顺时针寻找第一个相遇的机器节点哈希值,第一个相遇的机器便是这个数据要被分配到的地方。 大致如下图: {% asset_img hash_redis.png %} 上图中橘黄色节点就是机器节点哈希值映射到的节点,虚线箭头展示了一个数据被分配的过程。这个算法的好处在于,当有一个新的机器加入或者现有的机器被删除时,环上只有少部分数据的分配受到了影响,绝大多数数据还是分配到原来的地方。 另外,当机器数太少时,有可能出现下图这种情况: {% asset_img hash_redis_min.png %} 这种情况下,gamma节点被分配到了太多的数据,而alpha节点却只被分配到了少量数据。为了平衡各个节点的数据,可以增加一些虚节点,比如在上面那个图beta的位置增加一个alpha的虚节点,让环中的一部分数据碰到虚节点的时候,也分配到alpha而不是分配到gamma。 实现这种算法的方式大概有以下几种: 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。 代理分区意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy 查询路由意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster虽然没有使用一致性哈希算法(其使用预分配的哈希槽算法),但是它也实现了一种混合形式的查询路由,即并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。
- SQL语句中in和exist的区别和应用场景? 主要指下面两种语句的区别:
select * from table1 where id in (select id from table2);
select * from table1 where exist (select * from table2 where table1.id = table2.id);
in查询会先执行子查询得到临时表,然后针对table1中的每行记录来在临时表中查询id相等的情况,此时由于是临时表,索引是帮不上忙的。因此,当table2远比table1大的时候,in的效率可能很差。 exist查询则针对table1的每行记录,都执行一遍子查询来得到true还是false这样一个布尔值,由于子查询执行时可以使用到索引,因此是非常快速的,即使table2很大效率也还可以接受,但是,当table1远比table2大的时候,exist要执行非常多遍子查询,效率就很差。 当table1和table2大小相当的时候,两种查询方式效率相当。
- java中Object的wait方法有什么用,怎么用?和Thread.sleep方法有什么区别? Object类中有四类方法:和获取其对应Class对象相关的getClass方法、equals以及hashCode等被子类使用的方法、wait以及notify等用来在同步块中使用的线程相关方法、与垃圾回收相关的finalize方法。 wait方法用来让当前线程释放Object类(或其子类)对应的对象锁并休眠,参数可以设置超时时间。该方法必须在对象锁的同步块中调用才起作用,否则会抛出IllegalMonitorStateException异常。 有四种方式可以唤醒休眠的线程: 1>notify方法 2>notifyAll方法 3>超时时间到 4>线程被中断 其中,notify方法会随机地从那些调用对象锁wait方法的线程中选择一个来唤醒。notifyAll则直接唤醒全部。 无论哪种方式唤醒了线程,线程都不能立即得到执行,而是参与到锁争夺的过程中,直到争夺到锁之后才会从wait代码处继续执行,继续执行时所有的状态都和休眠前保持一致,比如本地变量什么的。 另外,在使用wait方法的时候,一般都将其放进while循环语句中,只要循环条件满足,就继续休眠,直到条件不满足为止。这样才可以保证线程在正确的条件下被唤醒。 **和Sleep方法的区别:**其实很简单,sleep方法也使线程休眠,并可以设置超时时间。也可以进行中断。但是,sleep方法不会使线程释放其持有的对象锁,在线程唤醒时自然也不会去争夺锁(但是可能还是会等待CPU分配时间片给它,即也不是立即执行的)。而且也不需要在同步块中调用。Sleep方法还是一个静态方法,而wait方法是对象方法,必须依托于某个对象。 这里有注意一点:wait方法只会释放其对象代表的对象锁,如果此时线程还持有另一个对象代表的对象锁,那么这个锁是不会被释放的。即此处是有死锁风险的。
- JAVA中类锁,对象锁分别是什么?怎么获得? 在官方文档中,锁被称为监视器(monitor),在我看来,锁只有一种,就是对象。网上有些人所谓的类锁,只不过是对整个JVM机制理解不深罢了。类锁的本质是类对应的Class对象,在类的加载过程结束之后,JVM会为每个类创建一个对应的Class对象,通过该对象可以拿到类的一些元信息。对于类来说,Class对象在同一个加载器下是唯一的,只有一个。而所谓的类锁,不过就是锁住的是这个对象罢了。一般认为类所对应的Class对象存在方法区中,当然,也有可能实例存在堆中,方法区保持一个指向堆的引用而已。 既然知道了锁的本质,那么获得方法就很好解释了: 所谓的类锁,就是将synchronized关键字加在静态方法上,或者synchronized关键字组成的同步块中,用Class对象来做锁。 而所谓的对象锁,就是将synchronized关键字加在非静态方法上,或者synchronized关键字组成的同步块中,用除了Class对象以外的对象来做锁。通常,this关键字获得方法对应的对象也是一种方式。 至于有人说的什么静态变量还能获得类锁,什么静态代码块获得类锁,纯属扯淡。
- http和RPC的区别与联系? 回答:如果有人问你http和RPC有什么不同,其实这是很不专业的问法,RPC是一个很笼统的概念,HTTP也随着发展进化出了1.1和2两个不同的版本。 RPC全称是远程过程调用,是比http要宽泛的概念,有很多RPC框架,其本身底层就使用的是http,比如谷歌的gRPC就使用HTTP2和protobuf。当讨论这种RPC的时候,http根本就不能用来和RPC所比较。 如果问的是阿里dubbo那种RPC,自己基于TCP协议定义了报文格式的,的确和HTTP协议有一些不同,少去了HTTP1.1协议那冗长的请求头,数据更加紧凑。但是要和gRPC相比的话,HTTP2的全双工以及压缩过的请求头,加上谷歌protobuf紧凑的数据格式,两者在速度和容量上应该是不相上下。 另外,现在很多RPC框架都提供了一些额外的功能,什么服务治理,服务发现之类的功能,我们使用http协议去完成服务间通信的时候,可能这些额外的功能还需要再选用其他组件去完成。 还有就是RPC框架会更加容易管理,因为客户端和服务端之间的方法调用格式是固化在代码里的,而http可能是通信双方通过文档来交流和规范的,url地址,请求参数,返回参数所有这些信息,通信双方都需要花力气去维护,甚至还可能要手工编解码,比如JSON格式(现在java有一些http客户端框架是可以做到使用接口来维护这些信息并自动编解码。protobuf更是直接将数据格式用proto文件固化了下来并自动帮你生成编解码代码)。 有一些误区要说明:网上有些人说什么http是短连接,rpc是长连接,所以http比rpc效率差,纯属扯淡,http也是可以长连接的,从1.1版本就默认开启长连接了。竟然还有人说什么RPC不需要三次握手,更是扯淡,RPC只要基于TCP协议,就肯定少不了三次握手的过程。 在本人看来,http也好,rpc也好,只要速度快,容量高,满足需求,两者根本没有优劣之分。微服务的提出者在微服务的论文中设想的微服务间的交流协议,就是http而不是什么RPC。RPC也不是新兴概念,它和http一样古老,只不过最近被人们捡起来了而已。
- API Gateway有什么用处? 类似于设计模式的facade(门面模式),API Gateway有以下好处: 解耦客户端和服务端 将多个api请求合并成一个请求 提供诸如监控、授权、负载均衡、缓存、静态响应处理等多个功能 也有缺点: API Gateway必须是一个高可用组件,因为其承载的绝大多数网络流量。 API Gateway依赖着其下游的微服务们,因此下游微服务的改动将导致程序员花额外的工作来维护API Gateway服务。 API Gateway必须配合服务发现机制来使用,传统的硬编码地址对于大量微服务应用的场景是不合适的。当然,通过DNS来进行分发也是不合适的,因为其受到运维团队制约,每次新增或者减少服务都要维护DNS,涉及运维人员。
- zookeeper底层数据结构? zookeeper的数据主要是存放在内存中的,并通过记录操作日志和快照的形式持久化数据到硬盘中。 数据结构是层次化的,给人感觉像文件系统,区别是每个路径代表的是一个节点Znode,而每个Znode既可以存放数据,又可以有子节点,对应文件系统就是每个节点既可以是文件又可以是文件夹。 由于zookeeper的设计目标不是存放数据,而是作为分布式的协调服务。因此,Znode被设计只能存放1Mb大小的数据。 有一些特殊的节点比如: Ephemeral Nodes:临时节点,会话结束的时候会被自动删除,因此不允许有子节点(zookeeper常用的删除命令不允许删除有子节点的节点)。 Sequence Nodes:序列节点,这种节点会自动在节点名后面增加一串数字,使节点拥有唯一命名。数字被格式化成10位,至多有2147483647个数字。这种节点配合临时节点使用,可以做分布式锁、自动选主等多种分布式架构需要的功能。
- 自旋锁,锁消除,锁粗化,轻量级锁,偏向锁分别是什么?有什么用? 这些名词全部都是在1.5以及之后的版本所做的关于锁的优化,它们有一些被自动使用在同步代码块获取锁的过程中,对用户是透明的,不可感知的。 自旋锁:在获取同步锁的时候,如果锁已经被其他线程占用,当前线程只好阻塞等待锁的释放。线程阻塞这一过程是通过调度操作系统来完成的,因此需要程序从用户态转入内核态中来挂起线程和恢复线程。这种操作非常耗时。所谓自旋锁就是在线程获取锁失败的时候,并不直接通过内核态来挂起线程,而是让当前线程执行一个循环来不停地询问锁是否已经可以获得。这样做的好处是免去了切换至内核态以及阻塞线程的开销,坏处是这个循环也是要占用CPU时间片的,如果循环太久而什么也没做,对性能是有影响的。因此一般自旋锁都是有循环次数的,次数到了还没获得锁就可以进入内核态挂起线程。之所以自旋锁能够起作用是因为绝大多数的同步代码,线程持有锁的时间其实并不长,因此自旋锁能够起到很好的作用。在JDK1.4中自旋锁就引入了,但是默认是关闭的。默认10次自旋。到JDK1.6,有了自适应的自旋锁而且默认是打开的,会根据上一次自旋锁的成功情况来决定本次自旋锁的次数。 锁消除:锁消除就是虚拟机及时编译器发现如果这段同步代码并未涉及到共享数据的竞争,那么就会自动将加锁同步的操作消除掉,避免额外开销。这主要是通过逃逸分析来得到的。 锁粗化:如果发现有一连串连续地或者嵌套地加锁操作,而且都是针对同一个对象,那么此时就会自动扩大加锁的范围并只加一次锁。消除频繁地加锁释放锁带来的开销。 轻量级锁:JDK1.6引入的新机制,它的实现基于这样一个事实:
绝大多数时候,是不会发生两个线程同时争夺锁的情况的
,因此如果一个线程去获取锁的时候使用传统的重量级锁(写互斥量),开销太大。那怎么实现一个比较轻量级的锁呢?其实是使用CAS操作来实现的。在JVM中,每个对象都有一个对象头,32位机有32位,64位机有64位。对象头英文名为“Mark Word”,里面记录了对象的hachCode和锁的标志位,当对象没有被锁住时,标志位是01。当一个线程来获取锁时,会先将Mark Word的内容在自己的线程栈帧中复制一份,并设置一个字段叫owner指向这个对象,然后利用CAS操作来更新对象的Mark Word,将Mark Word更新成指向栈帧的指针,同时更新锁标志位(Mark Word最后两位)为00,即轻量级锁。如果成功了,此时对象就算是被加了轻量级锁,如果失败了,线程可能会像上面提到的,先自旋等待一段时间,如果还获取不到锁,则直接执行锁膨胀的过程,将Mark Word的标志位更新成重量级锁,指针指向互斥量。在做完这一步操作后,线程就会阻塞等待。轻量级锁的解锁过程则也是使用CAS操作完成的,已经获得锁的线程通过CAS操作来交换自己栈帧里Mark Word和对象头上的Mark Word,如果失败了,说明对象头上的Mark Word已经被其他线程竞争并且更改过,那就直接退出并唤醒那些等待的线程来争夺重量级锁。如果成功了,对象就重新恢复到了01这种未加锁状态。 偏向锁:同样是在JDK1.6之后引入的,轻量级锁好歹还有个加锁的过程,偏向锁则更进一步,在基于绝大多数时候,是不会发生两个线程同时争夺锁的情况的
这个事实基础上,当有一个线程第一次获取锁之后,之后它再次获取相同的锁时,就干脆不再进行加锁的过程,直接认为锁已经加成功了。在获取偏向锁成功时,对象头Mark Word有了变化,锁标志位还是01,但是锁原来存放hashCode的地方会存放获取到偏向锁的线程id以及偏向时间戳,还有1bit本来是0会被置为1代表当前处于偏向锁模式。当一个线程来获取锁时,发现处于偏向模式,会先比较线程id是不是自己的线程id,如果是,直接立即返回认为加锁成功,如果不是,那就先看获取偏向锁的线程还活着没,如果没活着,就尝试CAS把自己写进去,自己获得偏向锁。如果活着,那么就会暂停当前持有偏向锁的线程,然后在这个线程的栈帧中lock record区域复制对象头,并将Owner指向该对象,同时CAS设置对象头指针指向栈帧中的位置,相当于这个线程帮助已经获得偏向锁的线程获取了轻量级锁,然后唤醒锁帮助的线程继续做事情,自己就自旋等待。如果超过了自旋次数,就会升级锁到重量级锁,这和之前描述的过程是一样的。总结而言,锁升级的过程就是从偏向锁到轻量级锁到重量级锁。如果程序中存在大量的锁竞争,同时获取锁的现象,这种优化反而会浪费性能,因此并不是万能的。JVM也提供了相应参数来关闭偏向锁或者轻量级锁。在JDK1.6+版本,这些锁优化默认是开启的。(这段描述也是看了别人的文章总结的,可能二手货抄来抄去也是不太准确,有机会的话自己研究源码,再来更新这段文字吧)