设计模式——单例的几种实现

饿汉单例:类加载的时候就直接初始化

优点:简单、安全

缺点:有时候不需要类加载的时候就初始化,希望延迟加载

代码

public class Singleton{

    //构造器私有

    private Singleton(){};

    

    //静态成员

    private static final Singleton singleton = new Singleton();

    public static Singleton getInstance(){

        return singleton;

    }

}

懒汉单例:真正使用的时候才创建单例

1. 单线程简单单例

代码

public class Singleton{

    //构造器私有

    private Singleton(){};

    

    //静态成员

    private static Singleton singleton;

    public static Singleton getInstance(){

        //第一次使用的时候创建

        if(singleton == null){ 

            singleton = new Singleton();

        }

        //随后直接返回

        return singleton;

    }

}

多线程下存在的问题:多个线程同时进入if条件,创建多个实例,违背了单例初衷。

2. 使用内置锁保护:使用synchronized内置锁确保只有一个线程可以进入临界区

代码

public class Singleton{

    //构造器私有

    private Singleton(){};

    

    //静态成员

    private static Singleton singleton;

    //使用synchronized锁住临界区

    public static synchronized Singleton getInstance(){

        //第一次使用的时候创建

        if(singleton == null){ 

            singleton = new Singleton();

        }

        //随后直接返回

        return singleton;

    }

}

缺点在于每次获取单例都要获取锁,高并发下内置锁会升级成重量级锁,开销大、性能差

3. 双重检查锁:只在第一次创建的时候进行加锁,所以,先判断单例是否已被初始化,如果没有,加锁后再初始化。

代码

public class Singleton{

    //构造器私有

    private Singleton(){};

    

    //静态成员

    private static Singleton singleton;

    public static Singleton getInstance(){

        if(singleton == null){ //一重检查

            synchronized(Singleton.class){ //加锁

                if(singleton == null){ //二重检查

                    singleton = new Singleton();

                }

            }

        }

        //随后直接返回

        return singleton;

    }

}

在进行第一重检查的时候不需要加锁,多个线程可以同时进入,但是只有一个线程能获取到锁并创建单例,其余线程随后可能获取到锁,但是无法通过第二重检查,所以直接返回单例。相比锁住整个getInstance方法,锁的粒度更小了,性能更好。

4. 双重检查锁+volatile: 表面上看双重检查锁天衣无缝了,但是需要注意:初始化单例的语句:

singleton = new Singleton();

并不是一个原子操作,经过汇编之后大致会被分成三个步骤:

1)分配一块内存M.

2) 在内存M上初始化Singleton对象

3)把M的地址赋给singleton变量

编译器、CPU都可能会对没有内存屏障和数据依赖关系的操作进行重排序,上述三个指令可能被优化成1)3)2)。这就可能导致线程A先进入临界区并且分配了内存,把地址赋给了singleton变量,但是还没有初始化对象,这时候线程B调用getInstance方法,发现singleton != null,直接返回一个未初始化的对象。

解决方法是用volatile关键字禁止指令重排。

代码

public class Singleton{

    //构造器私有

    private Singleton(){};

    

    //静态成员,使用volatile保持可见性,禁止指令重排

    private static volatile Singleton singleton;

    public static Singleton getInstance(){

        if(singleton == null){ //一重检查

            synchronized(Singleton.class){ //加锁

                if(singleton == null){ //二重检查

                    singleton = new Singleton();

                }

            }

        }

        //随后直接返回

        return singleton;

    }

}

 

实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

静态内部类实现单例:虽然双重检查锁能实现高性能、线程安全的单例模式,但是写法繁琐,利用静态内部类可以实现简单且安全的单例。

代码

public class Singleton{

    //构造器私有

    private Singleton(){};

    

    //静态内部类

    private static class LazyHolder{

        //通过final保障初始化的线程安全

        private static final Singleton singleton = new Singleton();

    }

    

    public static final Singleton getInstance(){

        return LazyHolder.singleton;

    }

}

在外部类被加载的时候并不会创建内部类的实例对象,只有getInstance()被调用的时候才会去加载内部类并且初始化单例。

但是静态内部类有一个致命的缺点:外部无法传入参数

猜你喜欢

转载自blog.csdn.net/weixin_42371376/article/details/132116233