设计模式-单例模式
2019-04-26 18:36 QQ~sunshine 阅读(250) 评论(0) 编辑 收藏 举报单例,顾名思义全局只能有一个实例对象。
基本原理就是
1. 使构造方法私有化(不能随便的通过构造方法创建实例来保证单例);
2. 有一个可以获取实例的静态方法(因为自己不可以创建实例,所以该方法必须是静态的,否则无法调用);
3. 有一个静态私有的局部变量指向自己(私有是因为不可外部调用,静态是因为2中的静态方法需要引用)。
单例模式又有饿汉式和懒汉式之分
按名字理解,饿汉式比较急,不管用没用到直接先创建一个实例,好处是快,在需要的时候直接用,坏处是占用资源,因为如果没有用到,它也创建好在那放着:
1 //单例模式-饿汉式-线程安全 2 public class Singleton{ 3 //静态私有的全局变量 4 private static Singleton singleton = new Singleton(); 5 //构造方法私有化,不允许外部创建 6 private Singleton(){} 7 //获取实例的静态方法,直接调用即可 8 public static Singleton getInstance(){ 9 return singleton; 10 } 11 }
懒汉式比较懒,等到用的时候才会创建出一个实例,好处是不会浪费资源,坏处是初次调用有一个创建的时间:
1 //单例模式-懒汉式-非线程安全 2 public class Singleton{ 3 //静态私有的全局变量 4 private static Singleton singleton; 5 //构造方法私有化,不允许外部创建 6 private Singleton(){} 7 //获取实例的静态方法,直接调用即可 8 public static Singleton getInstance(){ 9 //判断实例是否为空,如果为空也就是第一次调用则创建一个实例对象 10 if(singleton == null){ 11 singleton = new Singleton(); 12 } 13 return singleton; 14 } 15 }
饿汉式是线程安全的,因为它是在类加载时直接初始化了一个实例,多个线程同时调用也只是这一个实例;而懒汉式则是非线程安全的,因为它是调用的时候才会创建实例,这样就有一个问题,当多个线程假如同时到了判断为空的逻辑时,就会每个线程创建一个实例,这样就违背了单例的规则。
为了保证线程安全我们可以使用synchronized关键字进行方法同步或代码块,建议只同步判断为空的代码块,因为如果同步方法同步的作用域会比较大,锁的粒度比较粗,效率会低。下面同步方法的代码已经注释掉,保留了同步代码块的形式,这样即可保证线程安全。
1 //单例模式-懒汉式-线程安全 2 public class Singleton{ 3 //静态私有的全局变量 4 private static Singleton singleton; 5 //构造方法私有化,不允许外部创建 6 private Singleton(){} 7 //获取实例的静态方法,直接调用即可 8 /*public static synchronized Singleton getInstance(){ 9 //判断实例是否为空,如果为空也就是第一次调用则创建一个实例对象 10 if(singleton == null){ 11 singleton = new Singleton(); 12 } 13 return singleton; 14 }*/ 15 16 //获取实例的静态方法,直接调用即可 17 public static Singleton getInstance(){ 18 synchronized (Singleton.class) { 19 //判断实例是否为空,如果为空也就是第一次调用则创建一个实例对象 20 if (singleton == null) { 21 singleton = new Singleton(); 22 } 23 } 24 return singleton; 25 } 26 }
但这样因为有同步代码块效率其实还是低,我们可以使用内部类的方式实现:
1 //单例模式-懒汉式-线程安全 2 public class Singleton{ 3 //私有内部类,需要时进行加载 4 private static class Inner { 5 private static Singleton singleton = new Singleton(); 6 } 7 //构造方法私有化,不允许外部创建 8 private Singleton(){} 9 //获取实例的静态方法,直接调用即可 10 public static Singleton getInstance(){ 11 return Inner.singleton; 12 } 13 }
这样既保证了效率(因为是在使用时才进行的实例初始化),也保证了线程安全问题(同饿汉式相似,是在内部类加载时直接进行的初始化)。
还有一种,使用双重检查的方式创建单例,也可以提高运行效率:
1 //单例模式-懒汉式-双重检查-线程安全 2 public class Singleton{ 3 //静态私有的局部变量,使用volatile关键字防止重排序,因为new Singleton()的操作不是原子的,可能会创建一个不完整的实例 4 private static volatile Singleton singleton; 5 //构造方法私有化,不允许外部创建 6 private Singleton(){} 7 //获取实例的静态方法,直接调用即可 8 public static Singleton getInstance(){ 9 //Double-Check 10 if(singleton == null){ 11 synchronized (Singleton.class){ 12 //只需要在第一次创建实例时进行同步 13 if(singleton == null){ 14 singleton = new Singleton(); 15 } 16 } 17 } 18 return singleton; 19 } 20 }
如上所示,为了保证单例的条件下提高效率,我们对唯一实例进行了二次检查,这样只有第一次创建实例时会进入同步块,但是需要注意,这种方式必须使用volatile修饰实例。
因为创建实例的过程不是原子的(1.分配内存空间;2.初始化对象;3.使实例指向刚分配的内存地址),这个过程可能发生无序写入(改变指令顺序),假如有两个线程,第一个首次调用无疑会创建实例,在创建实例的时候指令顺序变成了1 3 2,在执行完第三步而没有初始化对象的时候,另一个线程到了第一个判断为空的逻辑,这时候实例不为空会直接返回,但是也没有进行初始化,这样就会导致程序出现问题。而volatile关键字正好可以解决这个问题。
总结以上实现单例模式一共六种:
1. 传统饿汉式,线程安全;
2. 传统懒汉式,非线程安全;
3. 使用synchronized关键字修饰方法的懒汉式,线程安全;
4. 使用synchronized关键字修饰代码块的懒汉式,线程安全;
5. 使用内部类延迟加载的懒汉式,线程安全;
6. 使用双重检查的懒汉式,线程安全;
到这里单例模式基本已经介绍完,但是需要注意的是:单例模式如果使用反射的方式依然是可以创建多个实例的,因为反射可以拿到私有的构造方法,就可以自己创造实例,所以我们使用单例模式要禁止使用反射进行对象的创建。