Java 中的 Monitor机制
参考:
synchronized 与 reentrantlock
synchronized 的注意点:
- 锁成员方法时锁对象为当前对象,即 this
- 锁静态方法时锁对象为当前类 Class 对象
- 可重入
- 方法或方法块退出后即自动释放锁
reentrantlock 注意点:
- 相对 synchronized 更加灵活,如区分读写锁、可以 tryLock、获取锁等待期间可被中断。
- 频繁同步情况下性能趋于稳定,少量同步情况下性能稍差于 synchronized
- 不会自动释放,所以务必使用
try..finally { // 释放锁 }
volatile关键字
参考:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
因此被volatile
修饰的变量具备可见性,每一个线程对此变量的读取都保证是最新的
但是volatile
并不能保证原子性,所以如果做自增或读取再写等复合操作时,并不一定能得到预期的结果。
针对自增等情况,建议使用Atomic
想着的原子操作类来完成
而更复杂的操作则借助synchronized
、lock
来处理并发
volatile
则适合单一操作的情况,如定义flag
用于逻辑判断
// 线程1
// ... 其他复杂业务
flag = true;
// ...
// 线程2
if (flag) {
// do something
}
happen-before 规则
参考:
个人理解,Jvm 屏蔽了硬件使得程序可以跨平台运行,JMM(Java内存模型)则是对真实硬件内存架构的屏蔽。
在涉及多线程上,JMM 通过happen-before 规则来解决线程之间的通信和同步。
开发者参考这份指南,JMM 遵守这份规则,从而保证所写的正确同步的多线程程序执行的结果与预期一致
而编译器也能根据这份规则尽可能的优化程序的并发度,使得编译出来的程序更加高效的使用硬件资源
常见的规则有8个:程序顺序规则、监视器锁规则、volatile 变量规则、传递性、线程启动规则、线程中断规则、线程终结规则、对象终结规则
简单说下就是:
了解 JVM,帮助我们知悉程序的运行环境和运行情况
了解 JMM,帮助我们了解程序的内存管理情况如分配、回收
了解 hb 规则,帮助我们写出多线程安全且高效的程序
更详细的内容可以细读一下上面列的参考文章
简述DCL失效原因,解决方法
参考:
在阅读上面的参考文章之前注意了
请务必认为文章中的单例对象一定有其他需要初始化的变量,否则 DCL 不存在失效之说。
请务必认为文章中的单例对象一定有其他需要初始化的变量,否则 DCL 不存在失效之说。
请务必认为文章中的单例对象一定有其他需要初始化的变量,否则 DCL 不存在失效之说。
因为失效的原因,简单点说就是由于指令重排线程A先做了变量的赋值但还未执行初始化,于是线程B拿到了一个未初始化好的单例对象,于是 GG 了...
下面是两种解决方法:
1. 将单例对象声明为volatile
从而禁止指令重排保证其他线程拿到的是一个初始化好的单例对象
public class Singleton {
//通过volatile关键字来确保安全
private volatile static Singleton singleton;
private int something = 0;
private Singleton(){
something = 1000;
}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
2. 利用类加载并初始化在多线程时依旧只会被加载一次的特性(由 Jvm 保证),将单例作为静态变量并直接构造
public class Singleton {
private static class SingletonHolder{
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
private int something = 0;
private Singleton(){
something = 1000;
}
}
关于第二种,如果没有懒加载的需求,甚至可以省去内部静态类
public class Singleton {
public static Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
private int something = 0;
private Singleton(){
something = 1000;
}
}
简述 NIO
参考:
- 由一个专门的线程来处理所有的 IO 事件,并负责分发。
- 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
- 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
GC 算法及收集器
参考:
常见的算法有:
-
标记清除算法
首先遍历标记出所有存活的对象,标记完成后清除未标记的对象。此算法缺点在于效率低下并且会产生内存碎片。 -
复制算法
将内存分成两份,每次 GC 时将存活的对象复制至另一份内存中,复制完后清理内存。典型的空间换时间,缺点是浪费内存空间,优点则是简单高效,并且不需要考虑内存碎片问题 -
标记压缩算法
对标记清除算法的改进,在标记完后将存活对象移至一端再作清除来避免内存碎片问题。 -
分代算法
将内存分为新生代和老年代。新生代中经历几次 GC 后依旧存活的对象将被移至老年代。
新生代存活率低,采用简单高效的复制算法
老年代存活率高,采用标记压缩算法来避免额外空间的分配担保
常见的收集器有:
-
Serial / Serial Old 收集器
串行收集器,GC 过程会暂停其他所有工作线程。简单高效,单线程中效率最高 -
ParNew 收集器
新生代 GC 策略。采用复制算法并行工作。Serial 的多线程版 -
Parallel Scavenge / Parallel Old
“吞吐量优先”收集器,并行工作,具有自适应调节策略。其目标是达到一个可控制的吞吐量。 -
CMS 收集器
全称Concurrent Mark Sweep。目标是获取最短回收停顿时间。
过程大致为初始标记 -> 并发标记 -> 重新标记 -> 并发清除
优点是:并发收集、低停顿
缺点是:
对CPU资源非常敏感。当 CPU 较少时,并发收集过程中对应用程序的影响较大
无法处理浮动垃圾。由于是并发收集,收集过程中程序依旧在产生垃圾,而这些浮动垃圾只能等下次 GC 时进行回收
采用是标记清除算法,会产生大量内存碎片。在无法分配连续的大空间时只能触发 Full GC 解决 -
G1收集器
将整个Java堆划分为多个大小相等的独立区域(Region)。从整体上看采用“标记整理”算法,从局部(两个Region之间)上来看是基于“复制”算法,因此不会产生内存碎片
过程大致为初始标记 -> 并发标记 -> 最终标记 -> 筛选回收
类加载
类加载主要有以下过程:
- 加载类文件至内存中并生成对应的 Class 对象
- 验证 Class 文件,如文件格式验证、元数据验证、字节码验证、符号引用验证
- 准备阶段,为类的静态变量分配内存,并赋默认值,未初始化。内存来自方法区或元数据区
- 解析符号引用
- 初始化
Java 中类加载采用的是双亲委托机制。加载时均一层层交由父类去加载,只有当父类明确无法加载时,才由当前类加载器加载。
类加载器:
- 启动类加载器(Bootstrap ClassLoader),加载 Java 核心类库
- 系统类加载器(system class loader)
- 扩展类加载器(extensions class loader):
- 用户自定义类加载器
简述字节码文件组成
上图结合文章 Java字节码结构解析 会更好理解。
类的加载阶段就是根据上图的定义将 class 二进制文件解析成 class 对象。
简述 ThreadLocal
ThreadLocal 的 get / set 方法实际上都是对当前线程内的 threadLocals 变量进行读取或赋值
每个线程的 threadLocals 都是私有变量,对其他线程不可见。
虽然每一次都是通过同一个 threadLocal 进行操作,但是实际上都转变为对当前线程内的 threadLocals 变量进行操作
操作时 threadLocal 也只作为 key 使用以及用于读取默认值,例如
sLocal.set(10)
则是以 sLocal
作为 key,将 10 存入当前线程的 threadLocals 中
public class ThreadLocal<T> {
// 其他代码...
// 以下是简化的 get 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
return (T)e.value;
}
// 以下是简化的 set 方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
map.set(this, value);
}
}
适用场景:每个线程需要有自己单独的实例,实例需要在多个方法中共享,但不希望被多线程共享
例如 Android 中的 Looper.myLooper()
就是使用 ThreadLocal 实现,从而保证每一个线程调用 myLooper()
时拿到的都是属于自己的 looper 对象
什么是 CAS
参考:
CAS 全称是 Compare And Set。这是一个由处理器提供支持的操作,并且是原子性操作不可中断。其原子性则由处理器通过总线锁或者缓存锁定来保证
CAS 操作包含一个内存地址V、一个期望值A和一个新值B,只有当内存地址V中的值与期望值A相等,才会将内存地址V的值更新为新值B。整个过程不可中断
我们常用的 Atomic 包中的类以及非阻塞的线程安全队列其实现原理就是 CAS
拿 AtomicInteger 举个例子,当前线程A 与线程 B 同时进行自增操作
线程A 首先从主内在V中取得值为0,保存至线程本地内在副本变量A1中,此时。。。线程A睡觉去了zzzz
线程B 运行,也从主内存V中取得值为0,保存至线程本地内存副本变量A2,接着A2+1,得到新值 B2 为 1。然后划重点了,线程B 进行 CAS 操作,比较 V 和 A2的值,都为 0,于是将 B2 更新至主内存 V中。
自增完成,此时主内存V的值为 1
线程A睡醒,接着睡前的操作对A1+1,得到新值B1 为 1,线程A也同样进行CAS操作,比较 V 和 A1 的值,1 != 0,于是B1不进行赋值操作,CAS 操作返回 false。线程A只好从头开始,取值,运算,CAS 操作,直到成功
通过以上流程,AtomicInteger 实现线程安全的自增操作。语言层没有涉及到同步操作,而是由硬件提供的CAS 操作来完成。
基于 CAS 的线程安全机制相比 synchronized 方式更高效,但存在以下问题:
- CAS 长时间不成功导致循环时间太长,对 CPU 的开销很大
- ABA 问题。一个值从 A 变成 B之后又变回了 A,导致 CAS 错误的以为值相同于是执行了更新操作
- 只能保证一个共享变量的原子操作
网友评论