设计模式(二)-创建者模式(1)-单例模式
概述
为何需要单例模式(Singleton Pattern)?
在程序运行当中,我们只希望一个类只能创建一个对象,在多个地方可以公用这个唯一的对象。
特点: 必须保证类只能创建一个对象。
单例模式可分为:饿汉式和懒汉式
1)饿汉式
特点: 类加载时(程序一开始运行时),该单例对象就被创建。
(所谓饿,饿肚子就马上干饭,所以程序一运行,就会被创建。)
优点: 没有出现线程安全问题。
缺点: 浪费内存空间。如果创建一个非常大的实例,到程序结束都没有被使用到,就浪费了资源。同时也会导致程序运行很慢,尤其在程序启动的时候。
2)懒汉式:
特点: 类加载不会创建单例对象,而是在程序运行中首次使用到单例对象时,才会开始被创建。
(所谓懒,不需要时不会创建。需要时才去检查有没有实例化,如果有就会返回实例对象,没有则会创建一个。)
缺点: 线程不安全。在程序运行中,可能会存在多个线程轮询干活。如果有多个线程同时判断是否有单例对象,如果都判断没有,就同时创建了多个对象。
1、饿汉式(静态变量) |
public class Singleton
{
private Singleton() { }
private static Singleton instance = new Singleton();
public static Singleton getInstance(){ return instance;}
}
2、饿汉式(静态代码块) |
//通过静态代码块(C# 不支持)
public class Singleton
{
private Singleton() { }
private static Singleton instance;
static { instance = new Singleton();
public static Singleton geetInstance() { return instance; }
}
3、懒汉式(线程不安全) |
public class Singleeton
{
private Singleeton() { }
private static Singleeton instance;
public static Singleeton getInstance
{
get
{
if (instance == null)
{
instance = new Singleeton();
}
return instance;
}
}
缺点: 如果有多个线程同时访问单例对象,会可能存在上一个线程还没有创建好对象时,而下一个线程因为判断该对象为空就重复创建一个对象。
4、懒汉式(线程安全) |
public class Singleeton
{
private Singleeton() { }
private static Singleeton instance;
private static object lockobj = new object();
public static Singleeton getInstance()
{
//加锁的作用:保证创建唯一的单例对象
//缺点:当单例对象创建好后,仍会导致其他线程进行等待抢锁,而不是直接返回该对象。
lock(lockobj)
{
if (instance == null)
{
instance = new Singleeton();
}
}
return instance;
}
}
优点: 解决了多个线程重复创建对象的问题。因为在单例对象判断是否存在前加了把锁,说明需要等待上一个线程创建好单例对象了,才能把执行权分配给下一个线程去执行 if 代码块。
缺点: 影响性能。如果单例对象已经创建好,其他线程为了使用该单例对象时,仍然会进行排队抢锁,从而增加了抢锁的时间,而不是直接返回该对象。
5、懒汉式(双重检查锁) |
public class Singleeton
{
private Singleeton() { }
private static Singleeton instance;
private static object lockobj = new object();
public static Singleeton getInstance()
{
//如果存在单例对象就直接返回该对象,避免浪费排队强锁的时间。
if (instance == null)
{
lock (lockobj)
{
if (instance == null)
{
instance = new Singleeton();
}
}
}
return instance;
}
}
还存在缺点: 在代码执行过程中,会存在指令的执行顺序问题。即指令的执行顺序并不一定会按照我们编写的顺序执行。也就说,系统可能会存在对指令重排序问题。所以这会导致程序不一定能及时拿到单例对象变量(instance)的最新值。这样即使能拿到不为空的 instance 变量,也不确保这个变量是否完全被创建好。
6、懒汉式(双重检查锁改进) |
public class Singleeton
{
private Singleeton() { }
//改进:在双重检查锁的代码中,只在 instance 变量中加了修饰符 volatile.
private static volatile Singleeton instance;
private static object lockobj = new object();
//....
}
为何 instance 变量 需要使用 volatile 修饰符?
如果 instance 变量去掉修饰符 volatile 的话,就不能保证该代码执行的正确性。因为instance = new SIngleton();这行代码并不是原子操作。(原子操作:不会被线程调度机制打断的操作,中间的执行不会切换到另一个线程来操作,也就是new 单例变量和单例对象具体创建的过程并不是由同一个线程执行的)。
打个比方:骑手送外卖。他一般工作的顺序就是送餐到达地点后,然后联系顾客,最后把餐丢到顾客嘴里。但是有时候呢,他可能因为其他事情如赶时间等原因,还没到送达点,就先联系顾客说外卖到了。结果就是顾客开门没看到外卖,就一直张着嘴,结果吃的是西北风。
如果顾客在APP上设置了通知功能,当骑手真的到送达点了就马上通知顾客。
这个例子,就相当于:
- 多线程:顾客拿餐、骑手送外卖过程、外卖中途有其他事情等。
- instance = new instance():顾客用餐。
- 创建Instance:骑手送餐、联系顾客,送达这一过程。
- 指令重排序:骑手中途先联系顾客,再到送达点。
- volatile:一旦骑手到了送达点,APP就会马上通知顾客。否则,就继续等待用餐。
初始化对象的顺序问题
Java 有JVM,同样的C#也会存在相似的运行环境,即 CLR。
在运行环境中,对类对象创建时会存在三个阶段执行:
- 1)为变量分配内存
- 2)初始化变量
- 3)将变量指向分配的内存空间
如果系统存在重排序,就可能会出现执行顺序问题。也就是说,系统可能存在先执行第三步后执行第二步,也可能会正常按顺序执行。前者的执行顺序,会导致变量还没有初始化完成,其他线程就已经判断了该变量值不为空,然后返回一个没有初始化完成的单例对象。
volatile的作用
(1)保证并发编程的可见性(不保证原子性)
单线程时重排序无影响,但是多线程就有可能会读取到脏数据,这就需要用 volatile。使用 volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值。
(2)禁止指令重排序
用 volatile 修饰保证执行顺序。