1.reentrantlock 和 synchronized 区别
reentrantlock 和 synchronized 都是可重入锁(内置锁都是可重入的,也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加 1,而当线程退出同步代码块时,计数器将会递减,当计数值等于 0 时,锁释放。)和悲观锁,synchronized 是非公平锁。reentrantlock 默认是非公平锁,但可以使用公平锁机制。reentrantlock 相对 synchronized 多了锁中断和锁投票等机制。
2.悲观锁适合写入,乐观锁适合读取。
乐观锁常见机制是版本号机制和 cas 无锁算法。
版本号机制就是存在一个字段用来保存版本号,线程拿到版本号,在更新时用拿到的版本号与数据库版本号比较,大于数据库版本则更新,小于或等于(因为在提交前,更新操作会使版本号加 1,所以等于版本号是失败操作)则重试操作。
cas 无锁算法就是 compare and swap(比较与替换),cas 涉及三个值:
+ 需要读写的内存值 V
+ 进行比较的值 A
+ 拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
乐观锁缺点:
(1)ABA 问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
注:用 AtomicStampedReference 可以解决 ABA 问题。
(2)循环时间长开销大
自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。
悲观锁和乐观锁,使用场景
悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。
乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
适用场景:
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。
3.偏向锁/轻量级锁/重量级锁
这三种锁是针对 synchronized 的锁状态。
偏向锁就是当一段同步代码一直被一个线程访问,那么线程会自动获取锁。
轻量级锁指的是当锁是偏向锁时,被另一个线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁就是当锁为轻量级锁时,另一个线程虽然自旋,但不会一直自旋下去,当自旋一定次数后,就会进入阻塞。膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,使性能下降。
自旋锁就是线程不会进入阻塞,而是通过循环去尝试获得锁。
补充:在 JavaSE 1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized 的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。
4.各收集器对比
serial 收集器是单线程串行收集器 -- client 模式下,新生代收集器
parNew 收集器是多线程收集器 -- 和 serial 收集器可以配合 CMS 工作
parallel scavenge -- 新生代收集器,使用复制算法的收集器,也是并行多线程收集器.它的目标是高吞吐
serial Old 收集器 -- 老年代收集器,同样也是串行单线程收集器,使用标记-整理算法
CMS(concurrent mark sweep)收集器 -- 使用标记清除算法
缺点:(1)无法处理浮动垃圾,可能会发生 full GC (2)空间碎片多,给大对象分配带来困难
G1 收集器
优势:
并发/并行概念:
5.内存分配和回收策略
1.对象优先在 eden 分配,当 eden 内存不够时,会发生一次 Minor GC
2.大对象直接到老年代
3.长期存活的对象将进入老年代
4.动态对象年龄判断
5.空间内存担保 -- 老年代最大可用的连续空间 > 新生代 all 对象总空间?
1、满足,minor gc是安全的,可以进行minor gc。
2、不满足,虚拟机查看HandlePromotionFailure参数:
(1)为true,允许担保失败,会继续检测老年代最大可用的连续空间>历次晋升到老年代对象的平均大小。若大 于,将尝试进行一次minor gc,若失败,即Minor gc后存活的对象远远高于平均值,则重新进行一次full gc。
(2)为false,则不允许冒险,要进行full gc(对老年代进行gc)。
6.内存管理工具
7.JVM 老年代和新生代的比例?
堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
(本人使用的是 JDK1.6,以下涉及的 JVM 默认值均以该版本为准。)
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即 90% )的新生代空间。
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
8.jvm YGC 和 FGC 发生的具体场景
1.YGC 和 FGC 是什么
YGC :对新生代堆进行 gc。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。
FGC :全堆范围的 gc。默认堆空间使用到达 80%(可调整)的时候会触发 fgc。以我们生产环境为例,一般比较少会触发 fgc,有时 10 天或一周左右会有一次。
2.什么时候执行 YGC 和 FGC
a.Edem 空间不足,执行 young gc
b.old 空间不足,perm 空间不足,调用方法 System.gc() ,ygc 时的悲观策略, dump live 的内存信息时(jmap –dump:live),都会执行 full gc
9.类加载过程:加载、验证、准备、解析、初始化、使用、卸载。
类的加载按照顺序按部就班的执行,不过解析可能会在初始化之后才执行,这是为了支持 java 运行时绑定.
10.双亲委派模型
Tomcat 违背双亲委派模型
tomcat 违背了 java 推荐的双亲委派模型了吗?答案是:违背了。 我们前面说过:
双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个 webappClassLoader 加载自己的目录下的 class 文件,不会传递给父类加载器。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于