设计模式详解之单例模式

本篇文章为转载,非原创!

点击跳转原文

单例模式初识

(1)单例模式的特点:

  • 一是某个类只能有一个实例
  • 二是它必须自行创建这个实例
  • 三是它必须自行向整个系统提供这个实例

应用情况:对于多个对象使用同一个配置信息时,就需要保证该对象的唯一性。

(2)如何保证对象的唯一性?

  • 一不允许其他程序用new创建该类对象。
  • 二在该类创建一个本类实例
  • 三对外提供一个方法让其他程序可以获取该对象

(3)实现的方法:

  • 一是构造函数私有化
  • 二是类定义中含有一个该类的静态私有对象
  • 三是该类提供了一个静态的公共的函数用于创建或获取它本身的静态私有对象

方法一饿汉式

public class Person1 {  
//定义该类的静态私有对象  
    private static final Person1 person1 =new Person1();  
    //构造函数私有化  
    private Person1(){  
        };  
//一个静态的公共的函数用于创建或获取它本身的静态私有对象  
    public static Person1 getPerson1() {  
        return person1;  
    }  
}

该方法虽然在多线程下也能正确运行但是不能实现延迟加载(什么是延迟加载?

资源效率不高,可能getPerson1()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化。

方法二懒汉式

public class Person1 {  
    private static Person1 person1 =null;  
    private Person1(){  
        }  
    public static Person1 getPerson1() {  
        if(person1==null){  
        person1=new Person1();  
        }  
        return person1;  
    }  
}

该方法只能在单线程下运行,当在多线程下运行时可能会出现创建多个实例的情况。

对该方法可以进行优化

优化一

public class Person1 {  
    private static Person1 person1 =null;  
    private Person1(){  
        }  
    public static synchronized Person1 getPerson1() {  
        if(person1==null){  
        person1=new Person1();  
        }  
        return person1;  
    }  
}

优化二

public class Person1 {  
    private static Person1 person1 =null;  
    private Person1(){  
        }  
    public static Person1 getPerson1() {  
        if(person1==null){  
            synchronized(Person1.class){  
                if(person1==null)  
                person1=new Person1();  
            }  
        }  
        return person1;  
    }  
}

这个优化比较好的解决了多线程问题,而且效率也很好,同时也兼顾了lazy loading。

今天看了《大话设计模式》终于明白双重判空的的意义:

对于person1存在的情况,就直接返回。当person1为null并且同时存在两个线程调用getPerson1()方法时,它们都将通过第一重的person1==null的判断。

然后由于类锁机制,这两个线程只有一个可以获得锁并进入,另一个在外排队等候,必须要其中一个进入并出来后,另一个才能进入。

而此时如果没有了第二重的person1==null是否为null的判断,则第一个线程创建了实例,而第二个线程获得锁后还是可以继续再创建新的实例,这就没有达到单例的目的。

ps:这讲解真的是通俗易懂。看完之后,对于单例怎么写,为什么要这么设计,都有了个清晰的认识。知识在书中往往有一个比较透彻的讲解,I like it。

对于getPerson1()方法的访问控制符,之前也一直停留在知道的阶段,为什么这么用并不清楚。然后自己写了个代码,使用private修饰,发现这个方法只能在当前类中引用,在类外就无法使用了。所以必须用public,实践是学习代码最快的方式,只看不练跟没学一样!!!!

volatile关键字在多线程中防止指令重排序。

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

考虑下面的实现,也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句。

if (uniqueInstance == null) {
    synchronized (Singleton.class) {
        uniqueInstance = new Singleton();
    }
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

由一个单例模式引发的对指令重排的思考

happens-before规则,指令重排序在多线程中的不安全性。

方法三缩小同步锁的范围

我们只是需要在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例。而当实例已经创建之后,我们已经不需要再做加锁操作了。

使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器,同样都符合“加锁”——>修改——>释放锁 的操作模式。

这里我们使用Java中的Lock对象,并进行双重校验:

import java.util.concurrent.locks.ReentrantLock;  
public class Person1 {  
    private static Person1 person1 =null;  
    private static ReentrantLock lock = new ReentrantLock(false); // 创建可重入锁,false代表非公平锁    
    private Person1(){  
        }  
    public static Person1 getPerson1() {  
        if(person1==null){  
            lock.lock();  
            try{  
                if(person1==null)  
                person1=new Person1();  
            }finally{  
                lock.unlock();  
            }  
        }  
        return person1;  
    }  
}

可行的解决办法;类装载时初始化实例

public class Singleton4 {
    private static Singleton4 instance = new Singleton4();  
      
    private Singleton4() {  
        System.out.println("初始化");
    }  
  
    public static Singleton4 getInstance() {  
        return instance;  
    }  

    public static void main(String[] args) {
//        任何代码都不写,此时打印一次"初始化",不能达到延迟加载
//        getInstance();    //注释打开,只打印一次"初始化"
    }

}

这个方法是在类装载时就初始化instance,虽然避免了多线程同步问题,但是没有达到lazy loading的效果。

方法四 静态内部类方法实现

public class Singleton5 {  
      
    private Singleton5() {  
        System.out.println("初始化成员");
    }  
  
    private static class SingletonHolder {  
        private static final Singleton5 INSTANCE = new Singleton5();  
    }  
  
    public static Singleton5 getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
    
    
    public static void main(String[] args){
        //不打印任何内容,可以达到延迟加载的目的
    }
  
}

这种方法也能保证线程安全,而且也达到了lazy loading的效果。

方法五 枚举

public enum Person1 {  
    person1;  
    String name=new String("ssss");  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
}
public class test {  
    public static void main(String[] args) {  
        Person1 person1 =Person1.person1;  
        Person1 person2=Person1.person1 ;  
        person1.setName("aaa");  
        person2.setName("bbb");  
        System.out.println(person1.getName());   
        System.out.println(person2.getName());   
    }  
}

输出结果都为bbb

总结:相对来说懒汉式 (优化二),缩小同步锁范围,静态内部类,枚举等方法是比较好的方法。

posted @ 2020-06-01 20:37  CryFace  阅读(114)  评论(0编辑  收藏  举报