设计模式

单例模式

1. 饿汉、懒汉模式

通过特定技巧, 保证在一个进程中某个类只有一个实例对象

 

具体看代码理解

 

饿汉模式: 

饿 -> 早 (急迫) -> 类加载的时候, 就初始化对象

查看代码

// 单例, 饿汉模式
// 唯一实例创建时机非常早. 类似于饿了很久的人, 看到吃的就赶紧开始吃. (急迫)
class Singleton {

    // 类的静态成员, 在类加载的时候 (这里简单理解为, jvm一启动就加载) 初始化
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    // private修饰 构造方法
    // 确保类外不能创建对象, 这样就保证了只有一个实例对象
    private Singleton() {

    }
}

class Test {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
        
    }
}

 

懒汉模式:

懒 -> 缓 -> 用到时候再实例化对象

 

// 单例模式, 懒汉模式的实现
class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy() {}
}

class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);

        // new SingletonLazy();
    }
}

 

2. 饿汉模式、懒汉模式 是否线程安全 ?

饿汉模式线程安全, 懒汉模式线程不安全

 

静态变量instance实例创建, 在main线程执行之前, 在其他所有线程启动之前

所以, 当多个线程调用getInstance() 方法, 相当于只是读取instance变量的值 -> 安全

 

假设下面的执行顺序, 实例化了两次对象 -> 线程不安全

如何解决 -> 加锁

查看代码
// 单例模式, 懒汉模式的实现
class SingletonLazy {
    private static SingletonLazy instance = null;
    private static Object locker = new Object();

    public static SingletonLazy getInstance() {
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
    }

    private SingletonLazy() {}
}

class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);

        // new SingletonLazy();
    }
}

 

现在虽然把线程安全问题解决了,  但是仔细观察下面代码发现

public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

只有最初调用getInstance() ,存在线程安全问题, 创建实例对象之后调用就只是读取instance变量的操作, 不存在线程安全问题

 

创建实例对象之后, 调用getInstance, 明明没有线程安全问题了, 但是还是要跑一趟 加/解锁操作 -> 不好

因为, 一般来说, 某个代码里面有锁, 就基本上谈不上高性能了

public static SingletonLazy getInstance() {
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

解决 ->  在外面套个判断

查看代码
// 单例模式, 懒汉模式的实现
class SingletonLazy {
    
    private static SingletonLazy  instance = null;
    private static Object locker = new Object();

    public static SingletonLazy getInstance() {
        // 双重if  
        if (instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {}
}

class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);

        // new SingletonLazy();
    }
}

 

另外, 得针对 instance 静态变量 + volatile

1. 避免编译器优化 ( 内存可见性 )  -> 防止读内存优化到读寄存器 

2. 避免编译器优化 ( 指令重排序问题 )  

什么是指令重排序问题 ?

编译器会在逻辑等价的情况下, 改变(由代码编译而成)二进制指令的顺序, 为了提高代码的执行效率

 

看下面这个例子:

instance = new SingletonLazy(), 会大致编译成3个指令 

1. 申请内存空间

2. 调用构造方法

3. 把对象的地址赋值给变量

在单线程代码中, 即使2、3执行顺序改变, 也不会有影响 -> 申请内存空间 -> 把对象的地址赋值给变量  -> 调用构造方法

但是, 在多线程中会有问题, 看下面例子

t2线程中拿到的 instance 变量, 还没有进行初始化成员变量, 针对为初始化的成员进行操作, 会有问题

 

posted @ 2024-07-15 14:24  qyx1  阅读(26)  评论(0)    收藏  举报