设计模式-单例模式

单例模式是一种常见的设计模式,由于有的类我们只希望保留一个对象即可,不需要创建更多的对象,这种情况下就可以使用单例模式.

单例模式的特点:
1、单例模式只能有一个实例。
2、单例类必须创建自己的唯一实例。
3、单例类必须向其他对象提供这一实例。

如何实现:

一. 饿汉模式

代码实现

public class Single {
    private static final Single INSTANCE = new Single();

    private Single(){
    }

    public static Single getInstance(){
        return INSTANCE;
    }
}

为了避免了类在外部被实例化,将构造方法限定为private,我们如果需要Singleton的实例可以通过getInstance()方法访问。该方法返回的是我们已创建好的唯一对象.

二. 懒汉模式(线程不安全)

public class Single {
    private static Single INSTANCE = null;

    private Single(){
    }

    public static Single getInstance(){
        if (INSTANCE == null) {
            try{
            	//暂停,让问题更加明显
                Thread.sleep(1000);
                INSTANCE = new Single();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return INSTANCE;
    }
}

在单线程情况下可以达到我们的目的.但在多线程情况下会不安全.例:有两个线程A,B,A首先判断INSTANCE为null,执行if内语句,暂停了1秒,就在暂停的这一秒线程B也进入if语句判断INSTANCE为null,也执行if内的代码,此时会发现线程A和线程B都执行了INSTANCE = new Single(); 这行代码.创建了两个对象,不满足我们的要求.

三.懒汉模式(线程安全)

public class Single {
    private static Single INSTANCE = null;

    private Single(){
    }
	//进行加锁操作
    public static synchronized Single getInstance(){
        if (INSTANCE == null) {
            try{
            	//暂停,让问题更加明显
                Thread.sleep(1000);
                INSTANCE = new Single();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return INSTANCE;
    }
}

对getInstance()方法添加synchronized关键字,在多线程情况下会对整个方法进行加锁操作,这种实现方式可以实现我们的需求,但是效率不是很高,锁的粒度较大.并发其实是一种特殊情况,大多时候这个锁占用的额外资源都浪费了.

四.DCL(Dounle Check Lock)双重检测

public class Single {
    private static Single INSTANCE = null;

    private Single(){
    }

    public static Single getInstance() {
        //首先判断是否为空
        if (INSTANCE == null) {  //step1
            //为空则进行加锁
            synchronized (Single.class) {   //step2
                //在判断是否为空
                if(INSTANCE == null) {   //step3
                    INSTANCE = new Single);   //step4
                }
            }
        }
        return INSTANCE;
    }
}

双重检测,顾名思义就是进行两次检测,也就是step1,和step3.
该实现方式可以保证线程安全.
多线程情况下,线程A,,线程B同时访问
1.A访问getInstance方法,进入if判断,由于INSTANCE未被实例化,进入step2,对代码块进行加锁操作.
2.B访问getInstance方法,进入if判断,由于INSTANCE未被实例化,也进入step2,但是以下代码块已经被A锁定.
3.A到step3步骤,再次判断,INSTANCE未被实例,执行step4,对INSTANCE实例化,并释放锁.
4.B对代码块进行加锁,到step3,发现INSTANCE已经被实例化过了,执行完代码块并释放锁.
5.线程A返回INSTANCE对象,而线程B返回null.

为什么要双重检测呢,只判断一次不行吗 ?
回答:
如果没有step1操作,就会变成

public static Single getInstance() {
    //为空则进行加锁
    synchronized (Single.class) {   //step2
        //在判断是否为空
        if(INSTANCE == null) {   //step3
             INSTANCE = new Single);   //step4
        }
   }
    return INSTANCE;
}

,相当于对整个方法进行加锁,还是效率问题.

如果没有step3操作,有可能会造成不安全,例如A,B同时执行到step1,A先对代码块进行加锁,返回实例,释放锁,接下来B对代码块加锁,也返回一个实例,最后再释放锁,此时AB返回的实例不是同一个对象.

五.volatile版

public class Single {
    private static volatile Single INSTANCE = null;

    private Single(){
    }

    public static Single getInstance() {
        //首先判断是否为空
        if (INSTANCE == null) {  //step1
            //为空则进行加锁
            synchronized (Single.class) {   //step2
                //在判断是否为空
                if(INSTANCE == null) {   //step3
                    INSTANCE = new Single);   //step4
                }
            }
        }
        return INSTANCE;
    }
}

每个线程在运行时,都会有一份自己私有的的本地内存,里面保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
volatile关键字有两个作用:

  • 保证可见性
    当某个线程修改volatile变量时,JMM会强制将这个修改更新到主内存中,并且让其他线程工作内存中存储的副本失效。
  • 禁止指令重拍序
    指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序,指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。
public static Single getInstance() {
    //首先判断是否为空
    if (INSTANCE == null) {  //step1
         //为空则进行加锁
         synchronized (Single.class) {   //step2
             //在判断是否为空
             if(INSTANCE == null) {   //step3
                 INSTANCE = new Single);   //step4
             }
         }
     }
     return INSTANCE;
}

step4 在A创建对象的时候,会有

  • 1.new (半初始化)
  • 2.invokespecial <T.> (构造方法)
  • 3.astore_1 (将对象引用指向堆内存.)

而这三步是有可能会被指令重排序的.有可能会变为1 -> 3 -> 2,在线程A执行到 3 的时候,线程B判断INSTANCE不为null,直接将对象拿去用,但这时候对象还是半初始化状态,只有默认值,会发生不可预知的错误.

添加了volatile关键字,可以解决上面四的B线程返回null的问题,在线程A创建完对象之前(写操作),线程B不会调用对INSTANCE的读操作,也就是( if(INSTANCE==null) ).

六.静态内部类

public class Single {
    public static class SingletonHolder{
        private static Single INSTANCE = new Single();
    }

    private Single(){
    }

    public static Single getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

使用静态内部类不会在Single加载时加载,当调用完getInstance()方法后才进行加载,此方法实现单例是线程安全的.

七.枚举

public enum Single7 {
    INSTANCE;

    public void test() {
        System.out.println("该类的其他方法");
    }

    public static void main(String[] args) {
        Single7.INSTANCE.test();
    }

}

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值