JVM

https://www.cnblogs.com/gaopengfirst/p/10037887.html

图片说明
1.程序计数栈:一块较小的内存空间,是当前线程所执行的字节码的行号指示器。程序计数器只为执行java方法服务,执行Native方法时程序计数器为空
2.Java虚拟机栈:生命周期与线程相同,每个方法在执行的同时都会创建一个栈帧(方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
3.本地方法栈:与虚拟机栈所发挥的作用是非常相似的,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务
4.Java堆:在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”
5.方法区:Non-Heap(非堆),各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

Java堆内存分代管理

新生代
Eden空间:对象优先在Eden分配,空间不足时,虚拟机将发起一次Minor GC
From Survivor空间、To Survivor空间:在Minor GC时交替使用,达到一定次数后,对象会晋升到老年代。如果Minor GC后仍存活的对象无法放入Survivor,则通过分担带包机制提前将对象转移到老年代
老年代
大对象(需要大量连续内存空间)直接进入老年代;长期存活的对象进入老年代,对象每“熬过”一次Minor GC年龄增加一岁,到一定程度(默认为15岁)则晋升到老年代
永久代
存储类定义、结构、字段、方法(数据及代码)以及常量在内的类相关数据
JDK 1.8中永久代被元空间(Metaspace)取代。两者本质类似,都是对JVM规范中方法区的实现,最大区别是:永久代的大小很难确定,对永久代的调优过程非常困难;元空间并不在虚拟机中,而是使用本地内存,最大可分配空间就是系统可用内存空间

1.JVM的生命周期:

启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。
运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。
消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或System.exit()来退出。

当在电脑上运行一个程序时,就会运行一个java虚拟机,java虚拟机总是开始于main方法,main方法是程序的起点。
java的线程一般分为两种:守护线程和普通线程。守护线程是java虚拟机自己使用的线程,比如GC线程就是一个守护线程,当然你可以把自己的线程设置为守护线程,注意:main方法启动的初始线程不是守护线程。
只要java虚拟机中还有普通线程在执行,java虚拟机就不会停止,如果有足够的权限,你可以调用exit()方法终止线程。

类加载器的结构

java中的类加载器大致可以分为两类:一类是系统提供的,另一类则是由开发人员编写的。系统提供的类加载器主要有三个:
图片说明
(1)引导类加载器(bootstr classloader):用来加载java的核心库,是用原生代码(非java语言)来实现的,并不继承自java.lang.ClassLoader.负责加载jdk_home/jre/lib目录下的核心api
(2)扩展类加载器(extensions classloader):用来加载java的扩展库。java虚拟机的实现会提供一个扩展目录。该类加载器在此目录里面查找并加载java类。负责加载jdk_home/jre/lib/ext目录下的jar包
(3)系统类加载器(system classloader):它根据java应用的类路径(classpath)来加载java类。一般来说,java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承java.lang.ClassLoader类的方式来实现自己的类加载器,以满足一些特殊要求。

图片说明

图片说明

主内存:多个线程共享的内存,方法区和堆属于主内存区域。
线程工作内存:每个线程独享的内存。虚拟机栈、本地方法栈、程序计数器属于线程独享的工作内存。

1.Java创建对象的过程:

JVM加载class文件的原理是什么?
JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个Java运行时系统组件。它负责在运行时查找和装入类文件的类。
Java中的所有类都需要由类加载器装载到JVM中才能运行。类加载器的工作就是把class文件从硬盘读取到内存中。

2.双亲委派机制:

如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终达到顶层的启动类加载器
如果父类加载器可以完成类加载任务就成功返回,如果父类加载器不能完成加载任务,子加载器互尝试自己去加载

3堆内存的分配策略:

1对象优先在eden区分配,Eden区没有足够的空间,将触发一次Minor GC。
2大对象直接进入老年代(为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率
3长期存活的对象进入老年代
4动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入到老年代,无需等到MaxTenuringThreshold中要求的年龄
5空间分配担保:-XX: HandlePromotionFailure

4.Minor GC与Full GC分别在什么时候发生?

触发MinorGC(Young GC)
虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间
1、如果大于的话,直接执行minorGC
2、如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
3、如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC
触发FullGC
老年代空间不足
如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。
持久代空间不足
如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC
YGC出现promotion failure

5.Minor GC 和 Full GC 有什么不同呢?

新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor
GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上

6.分代收集器:

老生代和新生代两个区域,而新生代又会分为:Eden 区和两个 Survivor区(From Survivor、To Survivor)

7.为什么 Survivor 分区不能是 0 个?

如果 Survivor 是 0 的话,也就是说新生代只有一个 Eden 分区,每次垃圾回收之后,存活的对象都会进入老生代,这样老生代的内存空间很快就被占满了,从而触发最耗时的 Full GC ,显然这样的收集器的效率是我们完全不能接受的。

为什么 Survivor 分区不能是 1 个?
如果 Survivor 分区是 1 个的话,假设我们把两个区域分为 1:1,那么任何时候都有一半的内存空间是闲置的,显然空间利用率太低不是最佳的方案。但如果设置内存空间的比例是 8:2 ,只是看起来似乎“很好”,假设新生代的内存为 100 MB( Survivor 大小为 20 MB ),现在有 70 MB 对象进行垃圾回收之后,剩余活跃的对象为 15 MB 进入 Survivor 区,这个时候新生代可用的内存空间只剩了 5 MB,这样很快又要进行垃圾回收操作,显然这种垃圾回收器最大的问题就在于,需要频繁进行垃圾回收。

为什么 Survivor 分区是 2 个?
如果 Survivor 分区有 2 个分区,我们就可以把 Eden、From Survivor、To Survivor 分区内存比例设置为 8:1:1 ,那么任何时候新生代内存的利用率都 90% ,这样空间利用率基本是符合预期的。再者就是虚拟机的大部分对象都符合“朝生夕死”的特性,所以每次新对象的产生都在空间占比比较大的 Eden 区,垃圾回收之后再把存活的对象方法存入 Survivor 区,如果是 Survivor 区存活的对象,那么“年龄”就 +1 ,当年龄增长到 15 (可通过设定)对象就升级到老生代。

8.请说明一下垃圾回收的优点以及原理

使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有"作用域"的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。不再会被使用的对象的内存不能被回收,就是内存泄露

9.判断对象是否可回收的方法

引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的
可达性分析法:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的

可作为GC Roots的对象包括下面几种:

虚拟机栈(栈帧中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(即一般说的Native方法)的引用的对象

10.强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、软引用能带来的好处)

1.强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必
不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错
误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题
2.软引用
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如
果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可
用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中
3.弱引用
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在:只具有弱引用的对象
拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就
会把这个弱引用加入到与之关联的引用队列中。
4.虚引用
虚引用主要用来跟踪对象被垃圾回收的活动 ,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,因为软引用可以加速 JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生**

11.如何判断一个常量是废弃常量

运行时常量池主要回收的是废弃的常量。
假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是
废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。

12.如何判断一个类是无用的类

方法区主要回收的是无用的类,类需要同时满足下面 3 个条件才能算是 “无用的类” :
1、该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2、加载该类的 ClassLoader 已经被回收。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

13.垃圾回收算法:

1、标记-清除算法
算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是
最基础的收集算法,效率也很高,但是会带来两个明显的问题:
\1. 效率问题:标记和清除两个过程的效率都不高。
\2. 空间问题:标记清楚之后会产生大量不连续的内存碎片,空间碎片太多会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。
2、复制算法(适合对象存活率低的新生代)
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块
的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收
都是对内存区间的一半进行回收。
新生代中:将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块XSurvivor中,然后清理Eden和刚刚使用过的Survivor空间。
3、标记-整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4、分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以
完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须
选择“标记-清除”或“标记-整理”算法进行垃圾收集。

14.垃圾收集器

//参考链接:https://www.cnblogs.com/cxxjohnson/p/8625713.html
图片说明

Minor GC又称新生代GC,指发生在新生代的垃圾收集动作;因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

Full GC又称Major GC或老年代GC,指发生在老年代的GC;出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);Major GC速度一般比Minor GC慢10倍以上;

1、 Serial 收集器 它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程,直到它收集结束。 新生代采用复制算法,老年代采用标记-整理算法

2、ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样

3 、Parallel Scavenge 收集器它关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。 新生代采用复制算法,老年代采用标记-整理算法。

4、Serial Old 收集器,它是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

5 、Parallel Old收集器,它是Parallel Scavenge收集器的老年代版本;

6 、 并发标记清理(Concurrent Mark Sweep,CMS)收集器,是一种以获取最短回收停顿时间为目标的收集器。CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。基于标记清除算法(不进行压缩操作,会产生内存碎片)。
CMS收集器运作过程比前面几种收集器更复杂,可以分为4个步骤:
A)、初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象;
速度很快;
但需要"Stop The World";

(B)、并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
应用程序也在运行;
并不能保证可以标记出所有的存活对象;

(C)、重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;

(D)、并发清除(CMS concurrent sweep)
回收所有的垃圾对象;

7、G1 收集器是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
1.并行与并发
能充分利用多CPU、多核环境下的硬件优势;
可以并行来缩短"Stop The World"停顿时间;
也可以并发让垃圾收集与用户程序同时进行;
2.分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
能够采用不同方式处理不同时期的对象;
虽然保留分代概念,但Java堆的内存布局有很大差别;
将整个堆划分为多个大小相等的独立区域(Region);
新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;

3.结合多种垃圾收集算法,空间整合,不产生碎片
从整体看,是基于标记-整理算法;
从局部(两个Region间)看,是基于复制算法;
这是一种类似火车算法的实现;
都不会产生内存碎片,有利于长时间运行;

4.可预测的停顿:低停顿的同时实现高吞吐量
G1除了追求低停顿处,还能建立可预测的停顿时间模型;
可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;

G1收集器运作过程,不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
(A)、初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象;
且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;
需要"Stop The World",但速度很快;

(B)、并发标记(Concurrent Marking)
进行GC Roots Tracing的过程;
刚才产生的集合中标记出存活对象;
耗时较长,但应用程序也在运行;
并不能保证可以标记出所有的存活对象;

(C)、最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
上一阶段对象的变化记录在线程的Remembered Set Log;
这里把Remembered Set Log合并到Remembered Set中;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
采用多线程并行执行来提升效率;

(D)、筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本;
然后根据用户期望的GC停顿时间来制定回收计划;
最后按计划回收一些价值高的Region中垃圾对象;

  回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
  可以并发进行,降低停顿时间,并增加吞吐量;

14.1 Java锁有哪些种类?

公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方***自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}

synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock

乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,可以参考自旋锁的实现

15.volatile的作用

保证共享变量的可见性:使用volatile修饰的变量,任何线程对其进行操作都是在主内存中进行的,不会产生副本,从而保证共享变量的可见性。防止局部指令重排序:happens-before规则中的volatile变量规则规定了一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。volatile如何防止指令重排序

volatile是通过内存屏障来防止指令重排序的。volatile防止指令重排序具体步骤:

在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。