Singleton设计模式

参考
Efficetive Java page14
深入浅出单实例Singleton设计模式
如何正确地写出单例模式

Singleton 1.0版本

public class Singleton {
    private static Singleton instance =null;
    private Singleton()
    {
        System.out.println("Singleton 实例化");
    }
    public static Singleton getInstance()
    {
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

Singleton的特点

  1. 私有(private)的构造函数,表明这个类是不可能形成实例了。这主要是怕这个类会有多个实例。
  2. 即然这个类是不可能形成实例,那么,我们需要一个静态的方式让其形成实例:getInstance()。注意这个方法是在new自己,因为其可以访问私有的构造函数,所以他是可以保证实例被创建出来的。
  3. 在getInstance()中,先做判断是否已形成实例,如果已形成则直接返回,否则创建实例。
  4. 所形成的实例保存在自己类中的私有成员中。
  5. 我们取实例时,只需要使用Singleton.getInstance()就行了。

单线程下

测试代码:

public class Tests {
    
    public static void main(String[] args) {
        for(int i=1;i<=1000;i++)
        {
            //Object相同则hashCode一样
            System.out.println(Singleton.getInstance().hashCode());
        }
    }
}

测试结果
2016-04-19_132745

多线程下

测试代码:

public class Tests {
    
    public static void main(String[] args) {
        TestSingletonThread T[]=new TestSingletonThread[101];
        for(int i=1;i<=100;i++)
        {
            T[i] = new TestSingletonThread();
            T[i].start();
         }
     }
}
    class TestSingletonThread extends Thread
    {
        @Override
        public void run()
        {
            System.out.println(Singleton.getInstance().hashCode());
        }
}

测试结果:
2016-04-19_133750

可以发现在多线程下会实例化多个实例

Singleton 1.1版本

因为在多线程下会实例化多个实例,所以加入synchronized关键字

public class Singleton {
    private static Singleton instance =null;
    private Singleton()
    {
        System.out.println("Singleton 实例化");
    }
    public static  synchronized Singleton getInstance()
    {
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

但是此时在多线程下,当已经实例化一份实例后,之后的线程再去获得实例应该是并行的,而加上synchronized关键字后变成并行 影响程序性能
因为自己电脑为4线程所以开4个线程去获取Singleton的实例,来展示性能差异
代码

public class Tests {
    
    public static void main(String[] args) throws InterruptedException {
        TestSingletonThread T[]=new TestSingletonThread[5];
        long start,end;
        start = System.nanoTime();
        for(int i=1;i<=4;i++)
        {
            T[i] = new TestSingletonThread();
            T[i].start();
        }
        for(int i=1;i<=4;i++)
        {
            T[i].join();
        }
        end = System.nanoTime();
        System.out.println("运行时间"+(end-start)/1000.0+"微秒");
    }
}
class TestSingletonThread extends Thread
{
    @Override
    public void run()
    {
        System.out.println(Singleton.getInstance().hashCode());
    }
}

不加synchronized关键字 但会实例化多个

QQ截图20160419214859

QQ截图20160419215011

加synchronized关键字 不会实例化多个 但会损耗性能

QQ截图20160419214736

QQ截图20160419214826

Singleton1.2版本

为了改善性能同时又不实例化多个Singleton实例,可以使用双重检测(Double-Check)

代码

public class Singleton {
    private static Singleton instance =null;
    private Singleton()
    {
        System.out.println("Singleton 实例化");
    }
    public static Singleton getInstance()
    {
        
        if(instance==null){
            synchronized(Singleton.class){
                if(instance == null)
                    instance = new Singleton();
            }       
        }   
        return instance;
    }
}

说明
1. 第一个条件是说,如果实例创建了,那就不需要同步了,直接返回就好了。
2. 不然,我们就开始同步线程。
3. 第二个条件是说,如果被同步的线程中,有一个线程创建了对象,那么别的线程就不用再创建了。
但是,如果你认为这个版本大攻告成,你就错了。

主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 singleton 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
  3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

对此,我们只需要把singleton声明成 volatile 就可以了。下面是1.3版:

Singleton 1.3版本

代码

public class Singleton {
    private volatile static Singleton instance =null;
    private Singleton()
    {
        System.out.println("Singleton 实例化");
    }
    public static Singleton getInstance()
    {
        
        if(instance==null){
            synchronized(Singleton.class){
                if(instance == null)
                    instance = new Singleton();
            }       
        }   
        return instance;
    }
}

使用 volatile 有两个功用:

1)这个变量不会在多个线程中存在复本,直接从内存读取。

2)这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。

但是,这个事情仅在Java 1.5版后有用,1.5版之前用这个变量也有问题,因为老版本的Java的内存模型是有缺陷的。

Singleton 1.4版本

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

 public class Singleton {
    private  static final Singleton instance = new Singleton();
    private Singleton()
    {
        System.out.println("Singleton 实例化");
    }
    public static Singleton getInstance()
    {   
        return instance;
    }
}

但是,这种玩法的最大问题是——当这个类被加载的时候,new Singleton() 这句话就会被执行,就算是getInstance()没有被调用,类也被初始化了。

于是,这个可能会与我们想要的行为不一样,比如,我的类的构造函数中,有一些事可能需要依赖于别的类干的一些事(比如某个配置文件,或是某个被其它类创建的资源),我们希望他能在我第一次getInstance()时才被真正的创建。这样,我们可以控制真正的类创建的时刻,而不是把类的创建委托给了类装载器。

Singleton 1.5版本

静态内部类

public class Singleton {
    
    private static class SingletonHolder
    {
        private  static final Singleton instance = new Singleton();
    }
    private Singleton()
    {
        System.out.println("Singleton 实例化");
    }
    public static final Singleton getInstance()
    {   
        return SingletonHolder.instance;
    }
}

上面这种方式,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

Singleton 1.6 枚举 Enum

public enum Singleton {
    INSTANCE;
    public  void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }
}

我们可以通过Singleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。

#其它问题(参考链接1)

Class Loader

序例化 (声明所有实例域都是瞬时的(transient)的 并提供一个readResolve方法

Effective Java 77条

多个Java虚拟机

volatile变量

关于代码重用

发表评论

电子邮件地址不会被公开。 必填项已用*标注