设计模式——单例模式

单例模式可以说是所有设计模式中最简单的一种,但是简单并不意味着它不实用,如果你是一个Java Web的开发者,没有用过Spring就很low了,而Spring的基础就是根据单例模式来设计的。

其实对于这种模式一开始感觉它并没有什么用,但是如果有一些开发经验会知道,在某种情况下一些类的对象是只有一个的,比如说线程池和日志对象等,如果多于一个对象可能会出现问题,所以说这时我们就

  • 需要确保这个类的实例只有一个,即只能被实例化一次;
  • 但是可以提供一个全局访问点来获取这个实例;

1. 经典单例模式

关于单例模式最经典的实现方式莫过于下面这种了:

public class Singleton{
    private static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

关于这种经典的方式有如下几点说明:

  • 构造器为私有,所以在此类的外部是不可以实例化该类的,这样就在类的外部保证了类的唯一性;
  • 通过提供一个全局的访问方法来获取这个唯一的实例;
  • 如果该类的实例一直没有使用,则该类并不会实例化,可以避免实例化带来的资源浪费;

2. 多线程下的单例模式

但是以为这样就足够了吗?too native!!看下面的实验如下:

声明SingleRunnable如下:

public class SingleRunnable implements Runnable {

    public void run() {
        System.out.println(Singleton.getInstance());
    }
}

main方法如下:

public static void main( String[] args )
{
    for(int i=0; i<100; i++){
        Thread thread = new Thread(new SingleRunnable());
        thread.start();
    }
}

看看在多线程情况下会发生什么?这里要说的是我们遇到的问题试验了多次才会出现,所以说这还是需要点运气的,本屌丝的运气向来不赖,试了6、7次问题就出来了,如下:

这里写图片描述

从上图中可以看到在多线程环境下使用经典的单例模式是会出问题的,产生的实例可能不止一个。为什么会出现这样的问题,只要对线程稍微明白一点的就不难理解,几个线程可能同时执行到if(singleton == null),然后发现singleton还没有实例化,于是每个线程都会产生一个新的实例,所以会出现多个实例。

那么怎么避免这种问题的出现?根据经验来说,当然是同步啊!但这并不是首选,能避免这种操作尽量避免,因为这样会使执行效率下降很多,那么如何使单例模式能够在多线程环境下也能正常运行呢?有如下几种方法:

2.1 避免延迟实例化

前面在经典单例模式中,实例不是立即被初始化的,而是在第一次使用时才会进行初始化,也就是在这个时候,多线程的环境下会初始化多个实例,但是如果在类层次进行初始化,也就是说当类被加载时就会初始化实例那么就会避免以上的问题,所以修改单例模式如下:

public class StaticSingleton {
    private static StaticSingleton singleton = new StaticSingleton();

    private StaticSingleton(){}

    public static StaticSingleton getInstance(){
        return singleton;
    }
}

但是如果这种实例的初始化开销很大,这种方法就不是很适合。

2.2 使用synchronized关键字

我们只要保证在一个时间点上只有一个线程执行到if(singleton == null)即可。

2.2.1 只使用synchronized关键字

我们将单例模式修改如下:

public class SynchronizedSingleton {
    private static SynchronizedSingleton singleton;

    private SynchronizedSingleton(){}

    public static synchronized SynchronizedSingleton getInstance(){
        if(singleton == null){
            singleton = new SynchronizedSingleton();
        }
        return singleton;
    }
}

在静态同步方法中getInstance(…)中,使用SynchronizedSingleton来调用这个方法时都会进行同步调用,也就是说同时执行if(singleton == null)的线程只会有一个。

但是这样真的是完美的吗?too native!!这种被synchronized修饰的方法不管是什么时候调用都会同步,但是从上文来看我们需要只是在第一次调用时同步,之后实例生成后就不需要同步了。

2.2.2 双重检查枷锁

为了避免每次都需要执行同步的方法,我们设定只在第一次调用时执行同步的方法,修改如下:

public class RepeatCheckSynchronizedSingleton {
    private static RepeatCheckSynchronizedSingleton singleton;

    private RepeatCheckSynchronizedSingleton(){}

    public static RepeatCheckSynchronizedSingleton getInstance(){
        if(singleton == null){
            synchronized (RepeatCheckSynchronizedSingleton.class){
                if(singleton == null){
                    singleton = new RepeatCheckSynchronizedSingleton();
                }
            }
        }
        return singleton;
    }
}

关于上面的修正的说明:

  • 当第一次调用getInstance(…)的时候才会执行第一个if(singleton == null),此时才会去执行同步代码块,这样就保证了之后调用该方法的时候不会再执行该同步代码块;
  • 为什么要检查两次if(singleton == null)?只有这样才能够确保只生成一个实例,例如当有两个线程A,B都执行完第一个检查if(singleton == null)之后,假设线程A抢到了资源进入同步代码块,线程B就需要等待线程A执行同步代码块完毕才能够进入,假设同步代码块中没有第二个检查if(singleton == null),那么进入的线程就会直接进行类的实例化,这是线程A就会产生一个实例;之后线程B进入同步代码块,但是由于线程B已经执行完第一个if(singleton == null),如果没有第二个检查的限制,也会生成新的实例,所以这时也要重新判断if(singleton == null),这样才会保证是否实例唯一;

我开始自己写单例模式能想到的也就到这里了,但是这样真的是最终的结果吗?然而并不是,我们不知道的是类的实例化并不是一个原子操作,它也会进行细分,所以在类的实例化时也会出现问题,这里面就涉及JVM的问题了,具体的原因参考相关文章。

关于上面这种方式我本想截一张图出来的,奈何运气较差,一直不出现多实例的问题,只好作罢!

所以为了避免这种问题,我们还需要使用volatile关键字来完善我们的代码,如下:

public class VolatileRepeatCheckSynchronizedSingleton {
    private volatile static VolatileRepeatCheckSynchronizedSingleton singleton;

    private VolatileRepeatCheckSynchronizedSingleton(){}

    public static VolatileRepeatCheckSynchronizedSingleton getInstance(){
        if(singleton == null){
            synchronized (RepeatCheckSynchronizedSingleton.class){
                if(singleton == null){
                    singleton = new VolatileRepeatCheckSynchronizedSingleton();
                }
            }
        }
        return singleton;
    }
}

2.3 使用ReentrantLock

如果你厌倦了使用synchronized关键字(这个字也太不好写了!!),也许可以试试ReentrantLock(呃呃,也不是很好写!!),这是通过使用显式锁来完成,这个相对于synchronized有明显的特点,使用synchronized当一个线程在执行完同步代码之后退出同步区后,另外任何一个线程会进入该同步区,这个线程可以和之前的线程是一样的,这样就会存在一个问题,在线程等待队列中的线程可能会有一部分总也不会执行,而同样的线程可能会执行多次,而使用ReentrantLock则会“公平”看待这些线程,它会从等待队列中找出等待时间最长的线程来执行,来避免有的线程不会执行的问题。

示例代码如下:

public class LockRepeatCheckSynchronizedSingleton {
    private volatile static LockRepeatCheckSynchronizedSingleton singleton;

    private static final Lock lock = new ReentrantLock();

    private LockRepeatCheckSynchronizedSingleton(){}

    public static LockRepeatCheckSynchronizedSingleton getInstance(){
        if(singleton == null){
            lock.lock();
            try{
                if(singleton == null){
                    singleton = new LockRepeatCheckSynchronizedSingleton();
                }
            }
            finally {
                lock.unlock();
            }
        }

        return singleton;
    }
}

相关文章:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值