实现Singleton 模式

 

目录

1 “懒汉式”与“饿汉式”的区别

“懒汉式”

“饿汉式”

2 GetInstance与new区别:

3 实现Singleton 模式 7种方式

方式1  饿汉,常用

方式2 懒汉,不安全

方式3 加锁的懒汉,性能低

方式4 -静态块,可以

方式5 静态内部类 推荐

方式 6枚举,推荐

方式7 . 双重校验锁 推荐

主函数

8 拓展问题: 定义一个表示总统的类型President,可以从改类型继承出FrechPresident和AmericanPresident, 这些派生类只能产生一个实例。


 

1 “懒汉式”与“饿汉式”的区别

所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。

“懒汉式”

“懒汉式”是在你真正用到的时候才去建这个单例对象:

  1. 私有化构造器
  2. 创建一个私有的实例static 先不实例化 为 null
  3. 通过公共方法调用 static 在方法里面进行判断,if = null
    实例化 !=null 直接return
比如:有个单例对象
public class Singleton{
    private Singleton(){}
    private static Singleton singleton = null;  //不建立对象
    public static synchronized Singleton getInstance(){
             if(singleton == null) {        //先判断是否为空                
                 singleton = new Singleton ();  //懒汉式做法
             }
             return singleton ;
     }
}

“饿汉式”

“饿汉式”是在不管你用的用不上,一开始就建立这个单例对象:比如:有个单例对象

:一个类只能创建一个对象

  1. 私有化构造器
  2. 在类的内部创建一个类的实例,且为static
  3. 私有化对象,通过公共方法调用
  4. 此公共方法只能通过类来调用,因为设置的是static,同时类的实例也是static
public class Singleton{
    public Singleton(){}
    private static Singleton singleton = new Singleton();  //建立对象
    public static Singleton getInstance(){
        return singleton ;//直接返回单例对象   
   }
}

2 GetInstance与new区别:

new的使用:如Object _object = new Object(),这时候,就必须要知道有第二个Object的存在,而第二个Object也常常是在当前的应用程序域中的,可以被直接调用的

GetInstance的使用:在主函数开始时调用,返回一个实例化对象,此对象是static的,在内存中保留着它的引用,即内存中有一块区域专门用来存放静态方法和变量,可以直接使用,调用多次返回同一个对象。

实现Singleton 模式 7种方式

方式1  饿汉,常用

首先是写明私有的构造方法防止被new,然后直接就实例化,最后调用,不存在线程安全问题。

/**
 * 单例模式,饿汉式,线程安全
 */
public static class Singleton1{
    private final static  Singleton instance = new Singleton();
    private Singleton1(){

    }
    public static Singleton getInstance(){
        return instance;
    }

方式2 懒汉,不安全

可能会出现线程安全问题,因为new一个对象在JVM底层做了如下工作:

1 给 instance 分配内存

2 调用 Singleton 的构造函数来初始化成员变量

3 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

显然是不能保证原子性的。

 

单线程的时候工作正常,但在多线程的情况下就有问题了。设想如果两个线程同时运行到判断instance是否为null的 if语句,并且instance的确没有创建时,那么两个线程都会创建一个实例,此时类型Singletonl就不再满足单例模式的要求了。为了保证在多线程环境下我们还只能得到类型的一个实例,需要加上一个同步锁。把 Singletonl稍作修改

/**
 * 单例模式,懒汉式,线程不安全
 */
public static class Singleton2 {
    private static Singleton2 instance = null;
    private Singleton2() {

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

        return instance;
    }
}

方式3 加锁的懒汉,性能低

我们还是假设有两个线程同时想创建一个实例。由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁时,第二个线程只能等待。当第一个线程发现实例还没有创建时,它创建出一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,并运行接下来的代码。这时候由于实例己经被第一个线程创建出来了,第二个线程就不会重复创

建实例了,这样就保证了我们在多线程环境中也只能得到一个实例。但是类型Singleton2 还 是 很 完 美 。我们每次通过属性Instance得到Singleton2 的实例,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。

/**
     * 单例模式,懒汉式,线程安全,多线程环境下效率不高
     */
    public static class Singleton3 {
        private static Singleton3 instance = null;
 
        private Singleton3() {
 
        }
 
        public static synchronized Singleton3 getInstance() {
            if (instance == null) {
                instance = new Singleton3();
            }
 
            return instance;
        }
    }

方式4 -静态块,可以

参考地址https://www.cnblogs.com/fengzheng/p/9103227.html

当第一次引用getInstance()方法的时候,访问静态内部类中的静态成员变量,此时该内部类需要调用static代码块(因为首次访问该类)。而后再次访问getInstance()方***直接返回instace引用。这种做法相对于传统做法更加巧妙。

/**
     * 单例模式,懒汉式,变种,线程安全
     */
    public static class Singleton4 {
        private static Singleton4 instance = null;
 
        static {
            instance = new Singleton4();
        }
 
        private Singleton4() {
 
        }
 
        public static Singleton4 getInstance() {
            return instance;
        }
}

方式5 静态内部类 推荐

定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。

优点:达到了lazy loading的效果,即按需创建实例。

/**
     * 单例模式,使用静态内部类,线程安全【推荐】
     */
    public static class Singleton5 {
        private final static class SingletonHolder {
            private static final Singleton5 INSTANCE = new Singleton5();
        }
 
        private Singleton5() {
 
        }
 
        public static Singleton5 getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }

虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的
 

方式 6枚举,推荐

/**
 * 静态内部类,使用枚举方式,线程安全【推荐】
 */
public enum Singleton06{
     INSTANCE;
}

在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。

很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:

1 枚举类实现其实省略了private类型的构造函数

2枚举类的域(field)其实是相应的enum类型的一个实例对象

对于第一点实际上enum内部是如下代码:
public enum Singleton {
    INSTANCE;
    // 这里隐藏了一个空的私有构造方法
    private Singleton () {}
}

比较清楚的写法是:

public class SingletonExample5 {
    private SingletonExample5(){}

    public static SingletonExample5 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton{
        INSTANCE;

        private SingletonExample5 singleton;

        //JVM保证这个方法绝对只调用一次
        Singleton(){
            singleton = new SingletonExample5();
        }

        public SingletonExample5 getInstance(){
            return singleton;
        }
    }
}

为什么能保证线程安全

https://www.cnblogs.com/z00377750/p/9177097.html 

方式7 . 双重校验锁 推荐

思想:先判断一下是不是null,然后加锁,再判断一下是否为null。如果还是null,则可以放心地new。

还是不大明白为什么要判断两次null?

 第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

/**
     * 静态内部类,使用双重校验锁,线程安全【推荐】
     */
    public static class Singleton7 {
        private volatile static Singleton7 instance = null;
 
        private Singleton7() {
 
        }
 
        public static Singleton7 getInstance() {
            if (instance == null) {
                synchronized (Singleton7.class) {
                    if (instance == null) {
                        instance = new Singleton7();
                    }
                }
            }
 
            return instance;
        }
    }

 执行过程
双重校验锁方式的执行过程如下:

1.线程A进入 getInstance() 方法。

2.由于 singleton为 null,线程A在 //1 处进入 synchronized 块。

3.线程A被线程B预占。

4.线程B进入 getInstance() 方法。

5.由于 singleton仍旧为 null,线程B试图获取 //1 处的锁。然而,由于线程A已经持有该锁,线程B在 //1 处阻塞。

6.线程B被线程A预占。

7.线程A执行,由于在 //2 处实例仍旧为 null,线程A还创建一个 Singleton 对象并将其引用赋值给 instance。

8.线程A退出 synchronized 块并从 getInstance() 方法返回实例。

9.线程A被线程B预占。

10.线程B获取 //1 处的锁并检查 instance 是否为 null。

11.由于 singleton是非 null 的,并没有创建第二个 Singleton 对象,由线程A所创建的对象被返回。
 

主函数

public static void main(String[] args) {
        System.out.println(Singleton.getInstance() == Singleton.getInstance());
        System.out.println(Singleton2.getInstance() == Singleton2.getInstance());
        System.out.println(Singleton3.getInstance() == Singleton3.getInstance());
        System.out.println(Singleton4.getInstance() == Singleton4.getInstance());
        System.out.println(Singleton5.getInstance() == Singleton5.getInstance());
        System.out.println(Singleton6.INSTANCE == Singleton6.INSTANCE);
        System.out.println(Singleton7.getInstance() == Singleton7.getInstance());
    }

8 拓展问题: 定义一个表示总统的类型President,可以从改类型继承出FrechPresident和AmericanPresident, 这些派生类只能产生一个实例。

/**
 * 问题拓展: 定义一个表示总统的类型President,可以从改类型继承出FrechPresident和AmericanPresident,
 * 这些派生类只能产生一个实例。
 */
class President {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}


class FrenchPresident extends President {
    private FrenchPresident() {
    }

    private static class Nested {
        private final static FrenchPresident instance = new FrenchPresident();
    }

    public static FrenchPresident getInstance() {
        return Nested.instance;
    }

    public static void main(String[] args) {
        FrenchPresident s1 = FrenchPresident.getInstance();
        FrenchPresident s2 = FrenchPresident.getInstance();

        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());

    }
}

class AmericanPresident {
    private AmericanPresident() {
    }

    public static AmericanPresident getInstance() {
        return Nested.instance;
    }

    private static class Nested {

        public static final AmericanPresident instance = new AmericanPresident();
    }

    public static void main(String[] args) {
        AmericanPresident s1 = AmericanPresident.getInstance();
        AmericanPresident s2 = AmericanPresident.getInstance();

        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());

    }

}

在上述的代码中,我们在内部定义了一个私有类型Nested。当第一次用到这个嵌套类型的时候,会调用静态构造函数创建FrenchPresident 以及AmericanPresident的实例instance。类型Nested只在属性FrenchPresident.Instance与AmericanPresident.Instance中被用到,由于其私有属性,他人无法使用Nested类型。因此,当我们第一次试图通过属性 FrenchPresident.Instance与AmericanPresident.Instance得到FrenchPresident和AmericanPresident的实例时,会自动调用Nested的静态构造函数创建实例instance。如果我们不调用属性Singleton5.Instance,就不会调用Nested, 也不会创建实例,这样就真正做到了按需创建。.