Posts List
  1. Java 中的 Monitor机制
  2. synchronized 与 reentrantlock
  3. volatile关键字
  4. happen-before 规则
  5. 简述DCL失效原因,解决方法
  6. 简述 NIO
  7. GC 算法及收集器
    1. 常见的算法有:
    2. 常见的收集器有:
  8. 类加载
  9. 简述字节码文件组成
  10. 简述 ThreadLocal
  11. 什么是 CAS

查漏补缺之 Java 篇

Java 中的 Monitor机制

参考:

synchronized 与 reentrantlock

synchronized 的注意点:

  • 锁成员方法时锁对象为当前对象,即 this
  • 锁静态方法时锁对象为当前类 Class 对象
  • 可重入
  • 方法或方法块退出后即自动释放锁

reentrantlock 注意点:

  • 相对 synchronized 更加灵活,如区分读写锁、可以 tryLock、获取锁等待期间可被中断。
  • 频繁同步情况下性能趋于稳定,少量同步情况下性能稍差于 synchronized
  • 不会自动释放,所以务必使用 try..finally { // 释放锁 }

volatile关键字

参考:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

因此被volatile修饰的变量具备可见性,每一个线程对此变量的读取都保证是最新的
但是volatile并不能保证原子性,所以如果做自增或读取再写等复合操作时,并不一定能得到预期的结果。
针对自增等情况,建议使用Atomic想着的原子操作类来完成
而更复杂的操作则借助synchronizedlock来处理并发
volatile则适合单一操作的情况,如定义flag用于逻辑判断

1
2
3
4
5
6
7
8
9
// 线程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从而禁止指令重排保证其他线程拿到的是一个初始化好的单例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 保证),将单例作为静态变量并直接构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

关于第二种,如果没有懒加载的需求,甚至可以省去内部静态类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
public static Singleton singleton = new Singleton();

public static Singleton getInstance(){
return singleton;
}

private int something = 0;

private Singleton(){
something = 1000;
}
}

简述 NIO

参考:

  1. 由一个专门的线程来处理所有的 IO 事件,并负责分发。
  2. 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
  3. 线程通讯:线程之间通过 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之间)上来看是基于“复制”算法,因此不会产生内存碎片
    过程大致为初始标记 -> 并发标记 -> 最终标记 -> 筛选回收

类加载


类加载主要有以下过程:

  1. 加载类文件至内存中并生成对应的 Class 对象
  2. 验证 Class 文件,如文件格式验证、元数据验证、字节码验证、符号引用验证
  3. 准备阶段,为类的静态变量分配内存,并赋默认值,未初始化。内存来自方法区或元数据区
  4. 解析符号引用
  5. 初始化

Java 中类加载采用的是双亲委托机制。加载时均一层层交由父类去加载,只有当父类明确无法加载时,才由当前类加载器加载。

类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),加载 Java 核心类库
  2. 系统类加载器(system class loader)
  3. 扩展类加载器(extensions class loader):
  4. 用户自定义类加载器

简述字节码文件组成


上图结合文章 Java字节码结构解析 会更好理解。
类的加载阶段就是根据上图的定义将 class 二进制文件解析成 class 对象。

简述 ThreadLocal


ThreadLocal 的 get / set 方法实际上都是对当前线程内的 threadLocals 变量进行读取或赋值
每个线程的 threadLocals 都是私有变量,对其他线程不可见。
虽然每一次都是通过同一个 threadLocal 进行操作,但是实际上都转变为对当前线程内的 threadLocals 变量进行操作
操作时 threadLocal 也只作为 key 使用以及用于读取默认值,例如 sLocal.set(10) 则是以 sLocal 作为 key,将 10 存入当前线程的 threadLocals 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 方式更高效,但存在以下问题:

  1. CAS 长时间不成功导致循环时间太长,对 CPU 的开销很大
  2. ABA 问题。一个值从 A 变成 B之后又变回了 A,导致 CAS 错误的以为值相同于是执行了更新操作
  3. 只能保证一个共享变量的原子操作

本文作者:JeremyHe
本文链接:http://www.alzz.me/posts/2018/01/24/17_make_up_deficiencies_java/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

有问题?
微信。◕‿◕。