查漏补缺之 Java 篇

作者: Alvminvm | 来源:发表于2018-04-19 16:57 被阅读6次

    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
    // ... 其他复杂业务
    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


    参考:

    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 中
    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. 只能保证一个共享变量的原子操作

    相关文章

      网友评论

        本文标题:查漏补缺之 Java 篇

        本文链接:https://www.haomeiwen.com/subject/atowkftx.html