从单例模式理解锁机制和类加载机制

知识源于:Multithreading and Architecture《Java Concurrency Programming》;Freeman , E.《Head First Java》

本文有借用其他博文观点的部分,贴在具体内容块里。

为什么要用单例模式?

​ 避免程序行为异常,资源使用过量,或者结果不一致。

下面列举了单例的实现,标记可用的才是线程安全的。

懒汉式 ——利用限定与加锁

package designpattern.singleton;
/**
 * 
 * JavaBasic
 * @classname LazySingleton
 * @description 懒汉式单例
 * @author Q
 * @date 2018年11月12日 下午4:54:47
 */
public class LazySingleton {
    private static LazySingleton uniqueInstance;
    private LazySingleton() {}
    
    //线程不安全的写法:
    /*public static LazySingleton  getInstance() {
        if(uniqueInstance == null) {
            //如果线程1运行到这,判断后创建前,线程2去判断依然是null
            uniqueInstance = new LazySingleton();
        }
        return uniqueInstance;
    }*/
    
    //同步方法 —— 保证同时只能有一个线程进入该方法
    /*public synchronized static LazySingleton  getInstance() {
    if(uniqueInstance == null) {
        //如果线程1运行到这,判断后创建前,线程2去判断依然是null
        uniqueInstance = new LazySingleton();
    }
    return uniqueInstance;
    }*/
    
    //同步代码块 —— 只同步if内的部分,还是会产生多个实例
    public static LazySingleton  getInstance() {
        if(uniqueInstance == null) {
            synchronized(LazySingleton.class) {
            uniqueInstance = new LazySingleton();
            }
        }
        return uniqueInstance;
    }
}

双重检查 —— 在懒汉基础上,错开两次判断if的时机(可用

package designpattern.singleton;

public class DoubleCheckLockingSingleton {
    //volatile 保证内存可见性
    private volatile static DoubleCheckLockingSingleton uniqueInstance;
    private DoubleCheckLockingSingleton() {}
    public static DoubleCheckLockingSingleton getInstance(){    
        //因为volatile夺锁的只会有线程1和2
        if(uniqueInstance == null) {   
synchronized(DoubleCheckLockingSingleton.class){
                if(uniqueInstance == null) {
                    //只有第一个线程会执行此处代码。
                    uniqueInstance = new DoubleCheckLockingSingleton();
                }
            }
        }
        return uniqueInstance;
    }
}

Java内存模型——JMM(Java Memory Model)

每个线程有自己的工作内存,线程只能对自己的工作内存变量副本进行操作(只对自己的写,优先读自己的)。不同线程间不能直接访问对方的工作内存,线程间变量传递都需要主内存完成

什么是内存可见性?

如果线程2读内存时,线程1对变量的修改还没来得及写,也就是说普通的共享变量写入内存的时机是不确定的,那么就会导致不可见

CPU为了提高处理性能,并不直接和内存进行通信,而是将内存的数据读取到内部缓存(L1,L2)再进行操作,但操作完并不能确定何时写回到内存

synchronized 关键字

控制代码段互斥的被执行,阻塞性质。可以防止多个线程执行同一个对象锁住的同步代码段。

synchoronized可应用的位置?
  1. 非static方法

  2. static方法

  3. synchronized(对象)

什么是monitor,同步具体怎么实现?

监视器monitor,每个对象都拥有一个。

synchoronized通过成对的()monitorenter和monitorexit,来保证一个monitor的lock锁同一时间只被一个线程获得。

monitor有一个进入数,为0表示没有线程进入,此时进入的线程为持有者,且对进入数当>0时

volatile 关键字

​ 参考:https://www.jianshu.com/p/7798161d7472;https://www.jianshu.com/p/195ae7c77afe;

volatile如何实现可见性?

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值(JMM通过将工作内存的设为失效,以让其直接去读内存)。

volatile变量的内存可见性是基于内存屏障实现的

什么是内存屏障(Memory Barrier)?

内存屏障保护这句指令不会被重排序,在汇编中,操作volatile的这句指令会多处一个lock前缀指令。

这个lock前缀指令相当于:
1、将当前CPU缓存中的数据写回到主内存;
2、这个写回内存的操作会导致在其它CPU里缓存了该内存地址的数据无效。(其他CPU嗅探出的)

经典的非原子性操作——i++

i++操作可被分解成三步原子操作,每一步之间都可能被打断

  1. 从内存中取当前i值
  2. 计算 i + 1
  3. 将新的i值写回到内存
volatile常用在哪里?
  1. 状态标记量:业务逻辑的开关
  2. double-check Singleton

饿汉式 —— 利用JVM的类加载机制

package designpattern.singleton;
/**
 * JavaBasic
 * @classname EagerSingleton
 * @description 饿汉式单例 —— 在调用前就创建好了实例(线程安全,可能浪费)
 * @author Q
 * @date 2018年11月12日 下午4:48:14
 */
public class EagerSingleton {
    
    /*静态常量
    private static final EagerSingleton uniqueInstance = new EagerSingleton();
    */
    
    //静态代码块
    private static EagerSingleton uniqueInstance;
    static {
        uniqueInstance = new EagerSingleton();
    }
    
    private EagerSingleton() {}
    public static EagerSingleton  getInstance() {
        return uniqueInstance;
    }
}

IODH(Initialization on Demand Holder)静态内部类——推荐⭐

package designpattern.singleton;

public class IODHSingleton {
    private IODHSingleton() {}
    //静态内部类
    private static class SingletonInstance {
        //私有静态常量实例
        private static final IODHSingleton uniqueInstance = new IODHSingleton();
        //这里对外层的IODHSingleton首次创建实例,此时IODHSingleton初始化
        //对于内部类SingletonInstance来说,这是静态常量,应在其准备阶段初始化
    }
    
    public static IODHSingleton getInstance() {
        //调用内部类的静态字段,此时内部类初始化
        return SingletonInstance.uniqueInstance;
    }
}

JVM类加载机制

ClassLoader 是一个抽象的class,加载class文件,并且在JVM中的庚哥对应内存分区中生成各数据结构。

JVM内存模型
类加载机制分为五个步骤?

类的加载过程分为五个阶段:加载 → 连接(验证 →准备 → 解析) → 初始化

加载:.class中的二进制数据读取到内存,将其字节流代表的静态存储结构转化为方法区中的运行时数据结构,并且在内存中生成该类的 java.lang.Class 对象,作为访问方法区数据结构的入口

类加载后内存分布.png

加载过程在JVM外部,实现加载的代码成为类加载器。HotSopt VM采用双亲委派模型。比如Object,无论用哪个类加载器,加载任务都传递到启动类加载器(这个类的最顶层的加载器)加载,保证在各加载器环境中都是同一个类。

验证:对格式、元数据、字节码和符号引用验证

准备:为类变量(static修饰)在方法区中分配内存,并赋默认值


解析:将常量池的符号引用替换为直接引用

初始化:为类静态变量赋代码给定的初始值

类在什么时候初始化?

类在Java代码中首次主动使用的时候(这个只是理论上,不排除JVM在运行期间提前预判)

  1. 首次创建某个类的新实例:

    new、反射[obj.getclass();/Object.class;Class.forName("java.lang.Object")]、克隆[https://www.jianshu.com/p/231a2008e91f]、反序列化[https://www.jianshu.com/p/551d6b9ae6c8]

  2. 首次调用某个类的静态方法时

  3. 首次使用某个类或接口的静态字段

  4. 首次调用Java的某些反射方法

  5. 首次初始化子类会导致父类初始化(通过子类使用父类的静态变量 static只会使父类初始化)

  6. 在虚拟机启动时,含有main()的那个启动类

什么时候不会类不会加载和初始化?
  1. 构造某个类的数组

  2. 引用某个类的静态常量static final

    常量final为什么一定要写static?

    因为static修饰后是类成员而不是变量成员,不会出现随多个对象拷贝多份的情况。

谢谢观看,如果有用麻烦点个喜欢,对我是莫大的鼓励。

你可能感兴趣的:(从单例模式理解锁机制和类加载机制)