Java-单例模式(饿汉与懒汉)

1. 单例模式

单例模式是一种设计模式,能够确保一个类中只有一个实例,不会创建多个实例,并提供一个全局访问点以访问该实例.

什么是设计模式?
设计模式就好比如建造房屋的过程.在建造房屋的时候,有许多常见的步骤和方法,如规划土地,设计平面图,选择材料.这些步骤的方法和目的就是为了实现一个稳定的,舒适的房屋.
设计模式提供了一种结构化的方法,可以更好地解决问题,使系统更好维护.

1.1 饿汉模式

饿汉模式是指在类加载时就创建出实例,并且在整个应用程序都保持着这个实例.特点是线程安全,因为实例在类加载的时候,就被创建出来了,不存在读线程并发访问的问题,但是缺点也很明显:浪费空间,有时候可能没有使用到该实例.就会导致浪费.

//单例模式(饿汉模式)
//在类加载的同时,创建实例
class Singleton {
    //带有static表示类属性,每个类的类对象是单例的,所以类对象的属性(static),也就是单例的,instance属性只会有唯一一份
    private static Singleton instance = new Singleton();
    //提供公开方法,获取属性.
     public static Singleton getInstance() {
         return instance;
     }
     //禁止别人new实例
    private  Singleton() {
    }
}
public class demo1 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        Singleton s3 = new Singleton();//新建出来的实例
        System.out.println(s1==s2);//同一个对象
        System.out.println(s1 == s3);
    }
}

1.2 懒汉模式 - 单线程

懒汉模式是指在需要的时候才会创建出实例,延迟初始化.特点是节省了空间,实例只有在使用时才会被创建,但是需要注意的是:在多线程情况下,需要考虑线程安全问题,因为很有可能会被创建出多个实例.

class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
    //修改操作不是原子的,会出现线程安全问题
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy() {
        
    }
}
//单例模式(懒汉模式)
public class demo2 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

在这里插入图片描述

在多线程环境下,假设有两个线程同时执行 getInstance() 方法:
线程t1执行到 if (instance == null) 时,判断 instance 为空,进入条件内部。
此时线程2也执行到 if (instance == null) 时,也判断 instance 为空,同样进入条件内部。
线程1在执行 instance = new SingletonLazy(); 时,创建了一个新的实例并赋值给 instance。
然后线程2也执行 instance = new SingletonLazy();,会再次创建一个新的实例并赋值给 instance。
这样就导致了两个线程同时创建了两个不同的实例,破坏了单例模式的原本意义。

1.3 懒汉模式 - 多线程

通过上面的代码我们了解到懒汉模式实现是线程不安全的,我们可以使用synchronized(加锁)关键字,来保证原子性.

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

    }
}

通过加锁解决了原子性的问题,也就不会创建出多个实例,但是懒汉模式下,我们首次调用加锁,创建出一个实例,但是在后续调用的时候,也加上锁了,实际上,后续调用就不必再加锁了,这里把不该加锁的地方加锁,就会很影响程序的效率.

加锁存在的缺点:

  1. 加锁会引起额外的开销:加锁和解锁过程会涉及线程的互斥锁的申请和释放等操作,会影响程序的性能.
    2.只有再第一次创建实例的时候需要加锁:当实例已经被创建后,后续的县城再次调用getInstance() 方法时,实例已经不存在并且不为空,不需要再进入同步块进行加锁操作.所以,加锁操作只需要在第一次创建实例时加锁.
class SingletonLazy {
    //避免"内存可见性"导致读取的instance出现偏差,加上volatile保证能读到最新修改到的结果.
    private volatile static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        //判断当前是否已经把instance实例创建好.
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                //判断是否要创建对象
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

其实加上volatile还有另外一个用途,避免此处赋值操作的指令重排序(指令重排序是编译器优化的一种手段,在保证原有执行逻辑不变的前提下,对代码执行顺序进行调整,使调整之后的执行效率提高).

instance = new SingletonLazy();这行代码中,就有可能会出现指令重排序.这行代码大概分为三个步骤:

  1. 给对象创建出内存空间,得到内存地址
  2. 在空间上调用构造方法,对对象进行初始化
  3. 把内存地址,赋值给instance 引用

此处可能会涉及到指令重排序,重排序后执行顺序就可能从 1-2-3,变为1-3-2,
如果是单个线程的话,先执行2还是先执行3是没有问题的.
如果是多个线程,假设t1线程是按照1-3-2的顺序执行,并且在执行完3后出现了线程切换,此时对象还没初始化,就调度给别的线程,t2线程在判定instance!=null,就会直接返回instance,并且后续可能会使用instance中的一些属性或者方法(拿到的是一个没有初始化的对象)

public class demo {
    private volatile int x = 0;
    private volatile int y = 0;
    private volatile boolean flag = false;

    public void writer() {
        x = 1;      // 写操作
        y = 2;
        flag = true; // 写操作
    }

    public void reader() {
        if (flag) { // 读操作
            System.out.println(x + y); // 打印结果
        }
    }
}

此时有两个线程,一个写线程和一个读线程,写线程负责对变量x,y进行写操作,读操作负责读取flag并执行一些操作.
假设在某一时刻,写线程执行到了x = 1,读线程也判断到了flag = true,执行了打印的操作,然而,由于指令重排序,可能会导致打印操作时,x的值为0,因为读线程可能在写线程设置x = 1之前就读取了这个值.
如果x , y 和 flag都声明为volatile类型,那么对于写线程writer来说写操作就会立即刷新到主内存中,线程每次读取就会从主内存中获取到最新的值,就可以避免到指令重排序带来的问题,确保结果是正确的.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值