前言:课上并未对单例模式进行详细的介绍,因此本篇博客介绍一下关于单例模式的一些内容。
单例模式(Singleton Pattern)
目的(Q&A)
Q:我们为什么需要这个模式?
A:因为有时候我们需要的是一个独一无二的对象,这个对象只有一个实例,我们在不同时段调用这个实例时希望这个实例具有“记忆”(个人看法)。
Q:那么哪些对象是需要具有独一无二这种特性的呢?
A:有很多,比如缓存,注册表,在现实生活中比如太阳、月亮(个人看法)。
实现
那么我们要如何实现这个模式呢?下面用代码加注释的形式来加以阐述。
- 首先,我们需要使用静态工厂方法。(注意静态工厂方法与工厂方法的区别,两者是不同的,前者是一个类方法,而后者是一个实例方法,两者都能将对象延迟实例化)
public Singleton{
//rep
private static Singleton uniqueInstance;
public static Singleton getInstance(){
if(uniqueInstance == null)
uniqueInstance = new Singleton();//这里并没有急着将UniqueInstance实例化,而是在需要的时候才会实例化这个变量(延迟实例化,减少内存消耗)
}
}
Q:为什么这里需要用静态工厂方法而不是工厂方法呢?
A:看了第二部就明白了,假设可以使用工厂方法,那么意味着已经可以通过Creator来获得实例了,这违背于我们最初的目的。
- 第二步,我们需要将Creator 私有化,原因很简单,我们不能让外部调用这个Creator。
public Singleton{
private Singleton(){//private将这个方法的可视范围限定在这个类内部
...
}
}
- 这个模式只需要两步就完成啦,下面把这个模式的代码全貌展示一下。
public Singleton{
//rep
private static Singleton uniqueInstance;
//other reps
private Singleton(){//构造器私有化
//delegation or other actions
}
//Static factory method
public static Singleton getInstance(){
if(uniqueInstance == null)
uniqueInstance = new Singleton();//延迟实例化
return uniqueInstance;
}
//other methods
}
线程安全
Q:这个模式它线程安全吗?
A:我们可以看到这个方法并没有将rep暴露出去(private),同时将构造器私有化了,看似非常安全,实际上还是会出现线程不安全的情况。
如上图所示,如果出现两个线程同时进行到红框中的这一步判断,由于操作并不原子,因此可以出现overleaving,导致最后产生了两个实例,而这恰恰违反了我们的spec。
下面是我的测试代码
public class Sun{
//rep
private static Sun uniqueSun;
//other reps
public static int number = 1;
private Sun(){//构造器私有化
//delegation or other actions
}
//Static factory method
public static Sun getInstance(){
if(uniqueSun == null) {
uniqueSun = new Sun();//延迟实例化
number++;
}
return uniqueSun;
}
public static void main(String[] args) {
Thread th1 = new Thread(new Runnable(){
@Override
public void run()
{
Sun sun = Sun.getInstance();
System.out.println(Sun.number);
}
});
Thread th2 = new Thread(new Runnable(){
@Override
public void run()
{
Sun sun = Sun.getInstance();
System.out.println(Sun.number);
}
});
th1.start();
th2.start();
}
}
结果:
3
3
案例来说,这个条件判断的语句应该只能执行一次,可是这里却执行了两次,说明确实存在线程不安全的情况。
如何解决?
我们可以使用锁的机制,将这个方法改成一个同步方法。
改动如下:
public synchronized static Sun getInstance(){
if(uniqueSun == null) {
uniqueSun = new Sun();//延迟实例化
number++;
}
return uniqueSun;
}
我们将这个静态工厂方法改进成一个同步方法。由于这里是类同步方法,因此这把锁是区别与对象的一个特殊的锁,通过锁机制,我们得到如下结果。
这说明我们已经将将线程不安全变成一个线程安全的了。
Q:按照缩小影响范围的原则,我们是不是还可以对那一部分条件判断的代码块进行加锁从而提高一些性能呢?
A当然可以,这里的显式调用的那把锁可以是Sun.class这个对象。具体代码如下:
public static Sun getInstance(){
synchronized(Sun.class) {
if(uniqueSun == null) {
uniqueSun = new Sun();//延迟实例化
number++;
}
return uniqueSun;
}
}
结果如下: