(转)单例模式超全整理(面试中常见的题目)

单例模式虽然简单,却是面试中经常出现的一类问题。

1 单例模式

单例模式的特点:

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

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

 

如何保证对象的唯一性?

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

 

实现的方法:

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

方法一    饿汉式

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;  
    }  
}  

 

该方法虽然能保证多线程下正常运行,但是效率很低,因为 person1=new Person1(); 这句话在整个程序运行中只执行一次,但是所有调用getPerson1的线程都要进行同步,这样会大大减慢程序的运行效率。所以虽然该优化解决了问题但是并不好。

 

懒汉式 (优化二)

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 的指令重排,保证在多线程环境下也能正常运行。

参考:https://cyc2018.github.io/CS-Notes/#/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F?id=%E4%B8%80%E3%80%81%E6%A6%82%E8%BF%B0

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

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

方法三缩小同步锁的范围

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

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

在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁。使用格式如下;

import java.util.concurrent.locks.ReentrantLock;

/**
 * ClassName:X <br/>
 * Function: ReentrantLock 语法格式
 * Date:     2017年11月30日 上午11:17:45 <br/>
 * @author   prd-lxw
 * @version   1.0
 * @since    JDK 1.7
 * @see      
 */
public class X {

    //定义对象
    private final ReentrantLock lock = new ReentrantLock();
    //..
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
        try{
            //需要保证线程安全的代码
            // ...method body
        }
        //使用finally块来保证释放锁
        finally{
            lock.unlock();
        }
    }
}

 

使用ReentrantLock 对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。

 

这里我们使用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;  
    }  
}  

该方法和懒汉式 (优化二)非常类似。因为一个类中 lock对象是唯一的,相当于一把类锁。

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

/**
 * ClassName:Singleton4 <br/>
 * Function: 类装载时初始化
 * Date:     2017年11月30日 下午3:08:05 <br/>
 * @author   prd-lxw
 * @version   1.0
 * @since    JDK 1.7
 * @see      
 */
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的效果

方法四 静态嵌套模式  

/**
 * ClassName:Singleton5 <br/>
 * Function: 静态内部类来实现单例,能够达到延迟加载和多线程的目的
 * Date:     2017年11月30日 下午3:02:56 <br/>
 * @author   prd-lxw
 * @version   1.0
 * @since    JDK 1.7
 * @see      
 */
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){
        //不打印任何内容,可以达到延迟加载的目的
    }
  
}  

 

上面的例子是说这个外部类的对象被内部类当成内部类的静态final域,所以只会有一个。

 注意,很多人都会把这个嵌套类说成是静态内部类,严格地说,内部类和嵌套类是不一样的,它们能访问的外部类权限也是不一样的。

ps:讲的很好,推荐看原文
静态嵌套类:

static修饰符使得嵌套类对象成为外部类的静态成员,与外部类直接关联。

这样静态嵌套类作为一个静态成员,仅能访问外部类的静态成员,因为外部类中的非静态成员与外部类对象相关,静态嵌套类就不能访问他们,这使得静态嵌套类的功能变的很弱,可用之处很少。

另外因为静态嵌套类是依附于外部类而非外部类对象的,所以不同的外部类对象共享一个静态嵌套类

内部类:

没有static修饰意味着一个内部类是和外部类对象关联的,也可以说一个内部类对象存在于外部类对象中,是外部类对象的一个成员,因此内部类对象可以访问外部类对象的全部成员,包括私有成员。

因为内部类依赖于外部类对象而存在,所以不能定义任何静态成员

内部类对象可以访问外部类的所有成员变量,包括私有成员,这是Java闭包的原理;

因为内部类隐含对外部类的引用,所以外部类就不能被JVM的垃圾回收机制自动垃圾回收

不同的外部类对象之间没有公共的内部类对象成员。

变量遮蔽Shadowing
嵌套类和封装类以及局部方法区的变量作用域有重叠,如果有同名变量将发生变量遮蔽。

推荐嵌套类理由:

  1. 实现代码简洁。
  2. 延迟初始化。调用getSingleton才初始化Singleton对象。
  3. 线程安全。JVM在执行类的初始化阶段,会获得一个可以同步多个线程对同一个类的初始化的锁。

如何实现线程安全?
线程A和线程B同时试图获得Singleton对象的初始化锁,假设线程A获取到了,那么线程B一直等待初始化锁。线程A执行类初始化,就算双重检查模式中伪代码发生了重排序,也不会影响线程A的初始化结果。初始化完后,释放锁。线程B获得初始化锁,发现Singleton对象已经初始化完毕,释放锁,不进行初始化,获得Singleton对象

注意:静态嵌套类不会自动初始化,只有调用静态嵌套类的方法,静态域,或者构造方法的时候才会加载静态内部类。
java静态内部类加载顺序

方法五  单元素枚举型(最佳)

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

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

 

单例模式之枚举实现

  在这种实现方式中,既可以避免多线程同步问题;还可以防止通过反射和反序列化来重新创建新的对象。在很多优秀的开源代码中,我们经常可以看到使用枚举方式来实现的单例模式类。

因为Java虚拟机会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。

由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏,因此在很多书和文章中都强烈推荐将该方法作为单例模式的最佳实现方法。

public enum Singleton {
     INSTANCE;
     public void businessMethod() {
          System.out.println("我是一个单例!");
     }
}

 

 

public class Singleton {
    private Singleton(){
    }   
    public static enum SingletonEnum {
        SINGLETON;
        private Singleton instance = null;
        private SingletonEnum(){
            instance = new Singleton();
        }
        public Singleton getInstance(){
            return instance;
        }
    }
}

 

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

 

输出结果为true

 

Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式?【享学Java】

ps::举例说明枚举类型的单例能够避免反射构造对象。以及序列化的安全性

反射在通过newInstance创建对象时,会检查该类**是否ENUM修饰**,如果是则抛出异常,反射失败;

枚举类型对序列化、反序列也是安全的

 

写个多线程,证明线程创建是安全的

    @Test
    public void threadTest() {
        Singleton t1 = Singleton.SingletonEnum.SINGLETON.getInstance();
        Singleton t2 = Singleton.SingletonEnum.SINGLETON.getInstance();
        System.out.println(t1 == t2);
        Map<Singleton, String> singleMap = new HashMap<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> singleMap.put(Singleton.SingletonEnum.SINGLETON.getInstance(), "1")
            ).start();
        }
        System.out.println(singleMap.size());

    }

输出的size是1

2 其它问题

延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。可以简单理解为,只有在使用的时候,才会发出sql语句进行查询。
懒加载---即为延迟加载,顾名思义在需要的时候才加载,这样做效率会比较低,但是占用内存低,iOS设备内存资源有限,如果程序启动使用一次性加载的方式可能会耗尽内存,这时可以使用懒加载,先判断是否有,没有再去创建
延迟加载也叫动态函数加载,它是一种模式允许开发者指定程序的什么组件不应该在程序启动的时候默认读入内存。通常情况下,系统加载程序会同时自动加载初始程序和从属组件。在迟加载中这些组件只在调用的时候才加载。当程序有许多从属组件而且并不常用的时候,迟加载可以用于提高程序的性能。

延迟加载可能出现的问题:
第一,延迟加载搞不好就容易导致N+1 select问题,性能反而不能保障
第二,延迟加载一般是在ORM中采用字节码增强方式来实现,这种方式处理后的对象往往会导致对象序列化失效,而在大型web应用中一般都会采用 独立的缓存架构,一但应用系统引入独立的缓存系统对应用数据对象进行缓存,采用延迟加载后的对象序列化将失效,导致缓存失败。
第三,ORM中的延迟加载将业务对象的加载关系搞得不清不楚,如果某天想换ORM,那么还得针对不同的ORM处理延迟加载关系,即使不还ORM后来人想理解加载关系也会很头疼。
第四,延迟加载目的是一方面是对了使应用只加载必要的数据,减少数据传输量,提高查询速度。另一方面,为了减轻数据库的进行不必要查询而进行运行增加的压力,避免一次性进行过多的查询,减少系统消耗。对于第一个问题,通过必要的缓存一般可以解决,对于这点系统消耗一般还是可以承受;对于第二个问题,通过在业务层进行单表查询配合必要的索引一般也是不存在问题的。
第五,从另外一方面考虑,ORM需要承担的仅仅是O R M,和事务、缓存等特性一样,它们应该由其他更有资格的家伙来承担,不需要搞那么负载,否则对于以后的底层扩展,那可是一个艰巨的工作。

 

/** * ClassName:Singleton4 <br/> * Function: 类装载时初始化 * Date:     2017年11月30日 下午3:08:05 <br/> * @author   prd-lxw * @version   1.0 * @since    JDK 1.7 * @see   */public class Singleton4 {private static Singleton4 instance = new Singleton4();        private Singleton4() {      System.out.println("初始化");    }        public synchronized static Singleton4 getInstance() {          return instance;      }  
public static void main(String[] args) {//任何代码都不写,此时打印一次"初始化"//getInstance();//注释打开,只打印一次"初始化"}
}

posted @ 2016-11-10 11:17  CS408  阅读(3740)  评论(0编辑  收藏  举报