设计模式——01单例模式:Singleton
/**
* 单例:把构造方法设置为私有的,别人(其他类里)new不了,而且全局只有这一个实例,在本类里可以,因为private的性质
* 因为实例定义是final 且 static的,每次调用getSingleton()返回的就是这一个;
* 所以是每次返回的都是同一个实例。这里需要注意的是实例化操作(new Singleton();)在声明其变量时就要操作;
* 否则你放getSingleton()里,那么每次都new一个新的,那就没意义了。
*/
1、饱汉式:
示例1:
package class01; public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getSingleton() { return INSTANCE; } public void print() { System.out.println("this is Singleton"); } public static void main(String[] args) { Singleton singleton1 = new Singleton(); Singleton singleton2 = new Singleton(); System.out.println(singleton1 == singleton2); } }
输出为:
false
解释:
这里把new的操作放在了main中,自然是两个对象,在这里可以new,是因为在同一个类中,参见private和public的属性。
示例2:
package class01; public class Main { public static void main(String[] args) { //Singleton singleton = new Singleton();//不能直接new Singleton singleton = Singleton.getSingleton(); Singleton singleton2 = Singleton.getSingleton(); System.out.println(singleton == singleton2); } }
输出:
true
解释:
返回的是同一个对象。
类加载到内存后,就实例化一个单例,JVM保证线程安全;
唯一缺点:不管是否用到,类装载时就完成实例化(话说自己不用干嘛要实例化,是吧?)
* 为了解决上面的问题:提出了懒汉式实例化,lazy loading,即用到的时候在new;
* 虽然达到了按需初始化的目的,但是却带来了线程不安全的问题。
* 变量为非final的,有final必须初始化.
package class01; public class Singleton2 { private static Singleton2 INSTANCE; private Singleton2() {} public static Singleton2 getSingleton() { if (INSTANCE == null) { INSTANCE = new Singleton2(); } return INSTANCE; } public void print() { System.out.println("this is Singleton"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ System.out.println(Singleton2.getSingleton().hashCode()); }).start(); } } }
但是这样就会造成线程不安全。编写如上main方法,来揭示在多线程下得到的实例是不是同一个对象,为了区分明显,我们可以在getSingleton(){}方法内的 if (){} 里写入睡眠语句,如下:
public static Singleton2 getSingleton() { if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } INSTANCE = new Singleton2(); } return INSTANCE; }
运行后,输出:
512585151 945515189 945515189 76872907 76872907 945515189 945515189 945515189 945515189 945515189
即发现hashcode并不完全一致(虽然完全一致并不代表是同一个对象,但是不一致肯定不是同一个对象)。
so...........
为了改善线程不安全,我们可以使用synchronized来给方法加锁。
但是这样就效率低下了。
public static synchronized Singleton2 getSingleton() { if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } INSTANCE = new Singleton2(); } return INSTANCE; }
为了改进这个问题,我们可以使用代码块加锁的方式,只对方法里不安全的代码进行加锁。
1 public static Singleton2 getSingleton() { 2 if (INSTANCE == null) { 3 synchronized (Singleton2.class) { 4 try { 5 Thread.sleep(1); 6 } catch (InterruptedException e) { 7 // TODO Auto-generated catch block 8 e.printStackTrace(); 9 } 10 INSTANCE = new Singleton2(); 11 } 12 } 13 return INSTANCE; 14 }
执行结果为:
945515189 945515189 945515189 945515189 945515189 945515189 512585151 512585151 512585151 76872907
我们发现这样做,依然不是线程安全的。
为什么呢?
原因在于:当线程A 进入到第2 行时,此时A 暂停,线程B 也进入第2 行,之后B 线程取得锁,B 线程继续执行new 操作完毕,释放锁。
之后才轮到A 去执行,此时拿到锁,这里就出现问题了,下一步A 仍旧会去再执行new 操作。这样就new 了两次。
为了避免面这个问题。特此引入双重判空结构。
而且,变量INSTANCE要设置成: private static volatile Singleton2 INSTANCE; ,需要加上关键词 volatile,关于该关键词的用法及详解大家可以参考我这篇文档,Java之先行发生原则与volatile关键字详解。
public static Singleton2 getSingleton() { if (INSTANCE == null) { synchronized (Singleton2.class) { if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } INSTANCE = new Singleton2(); } } } return INSTANCE; }
输出:
512585151 512585151 512585151 512585151 512585151 512585151 512585151 512585151 512585151 512585151
hashcode都一致。
静态内部类方式
JVM保证单例
加载外部类时不会加载静态内部类,这样可以实现懒加载。
package class01; public class Singleton3 { /** * 静态内部类方式 * JVM保证单例 * 加载外部类时不会加载内部类,这样可以实现懒加载。 */ private Singleton3() {} private static class SingletonHolder { private static final Singleton3 INSTANCE = new Singleton3(); } public static Singleton3 getSingleton() { return SingletonHolder.INSTANCE; } public void print() { System.out.println("this is Singleton"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ System.out.println(Singleton3.getSingleton().hashCode()); }).start(); } } }
输出:
1172810421 1172810421 1172810421 1172810421 1172810421 1172810421 1172810421 1172810421 1172810421 1172810421
hashcode都一致。
这里的线程安全是JVM保证的。
最后一种是最完美的写法。
但是,JAVA的创始人不乐意了,干出了一个更完美的写法:在《Effective Java》书中给出了这样的例子:
package class01; public enum Singleton4 { /** * 不仅可以解决线程同步问题,还可以防止反序列化 */ INSTANCE; public void m() {} public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()->{ System.out.println(Singleton4.INSTANCE.hashCode()); }) .start(); } } }
大家注意:这里是enum 枚举,而不是class。另外枚举不需要构造函数
输出:
76872907
76872907
76872907
76872907
76872907
76872907
76872907
76872907
76872907
...
总结
一般情况下是用第一种和第四种的,因为比较简单。
Over....