1、什么叫单例模式?
答:单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
2、单例模式的特点?
答:(1)单例类只能有一个实例。
(2)单例类必须自己创建自己的唯一实例。
(3)单例类必须给所有其他对象提供这一实例。
3、单例模式设计意图?
答:通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
4、使用的场景?
答:在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免生出多个实例。
5、如何实现单例模式?或者设计的思想是什么?
答:①一个类之所以能够创建出实例是因为有构造方法的存在,只要我们把构造方法的访问修饰符改成私有(private),外界就不能通过new来创建该类的实例。
②在单例类中自身new出一个对象,因为要被外界访问,我们可以把它静态化(static),以便外界访问(类型.对象)。
③有时候我们需要控制这个对象,也处于安全起见,我们可以把继续私有化(private),然后提供一个getter方法以便外界访问。
6、什么是线程安全?
答:如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行同一段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
单例模式种类:饿汉方式、懒汉方式、登记方式。
一、懒汉式(延迟加载),三种方式
1、线程不安全
/*** 懒汉单例 非线程安全*/
public class SingleTest2 { private static SingleTest2 singleTest2; public static int vertifyCode; //标记是否会产生多个单例 private SingleTest2() { } public static SingleTest2 getInstance(){ System.out.println("开始======"); if(singleTest2==null){ System.out.println("进入======"); singleTest2=new SingleTest2(); vertifyCode++; System.out.println("vertifyCode==="+vertifyCode); } System.out.println("结束======"); return singleTest2; } }
测试:
public class MyThreadTest2 extends Thread{ public void run(){ //方式一:不安全 System.out.println(SingleTest2.getInstance().hashCode()); } public static void main(String[] args) { int i=0; while (SingleTest2.vertifyCode!=2) { for (int j = 0; j < 10; j++) { new MyThreadTest2().start(); } i++; } System.out.println(i+"次后,生成第二个单例对象"); } }
说明:(1)最基本的单例模式,但是不支持于多线程。在多线程下,不安全。
2、线程安全
/*** 懒汉单例——线程安全*/
public class SingleTest1 { private static SingleTest1 singleTest1; public static int vertifyCode; //标记是否会产生多个单例 private SingleTest1() { } public synchronized static SingleTest1 getInstance(){ System.out.println("开始======"); if(singleTest1==null){ System.out.println("进入======"); singleTest1=new SingleTest1(); vertifyCode++; System.out.println("vertifyCode==="+vertifyCode); } System.out.println("结束======"); return singleTest1; } }
测试:
public class MyThreadTest2 extends Thread{ public void run(){//方式二:安全 System.out.println(SingleTest1.getInstance().hashCode()); } public static void main(String[] args) { int i=0; while (SingleTest2.vertifyCode!=2) { for (int j = 0; j < 10; j++) { new MyThreadTest2().start(); } i++; } System.out.println(i+"次后,生成第二个单例对象"); } }
说明:(1)在方法体上,加上synchronized,属于线程安全,支持多线程。
(2)每次实例化对象,都需要先获取同步锁,影响效率,但是99% 情况下不需要同步。所以,该方法基本不使用。
3、双重检验同步延迟加载
/*** 双检验单例模式*/
public class SingleTest { private volatile static SingleTest singleTest; public static int identifyCode; //插入一个验证码验证时是否生成了多个对象 private SingleTest(int num){ System.out.println(num+"进入构造方法!!!!"); //code.. 对你组合的对象初始化 } public static SingleTest getInstance(int i){ System.out.println("enter method"+i); if(singleTest==null){ //第一次判断是否为null System.out.println("status 1 thread---"+i); synchronized(SingleTest.class){ //在代码块中加同步锁 如果已经有线程访问 当前线程转为阻塞状态 if(singleTest==null){ //当第二个线程访问时 已经不为null了 那么不再创建对象 System.out.println("status 2 thread---"+i); singleTest=new SingleTest(i); } } } return singleTest; } public void method(){ System.out.println("调用实现方法!!!!"); //code.. } }
测试:
public class MyThreadTest extends Thread{ private int aint; //int没有初始值,默认是0 public void run(){ SingleTest.getInstance(aint); } public MyThreadTest(int i) { this.aint=i; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new MyThreadTest(i).start(); } } }
说明:
(1)采用双锁机制,安全且在多线程情况下能保持高性能。
(2)该方法对实例instance进行多次检查,目的为了避开过多的同步。(只有在第一次创建实例才会同步,一旦创建成功,以后获取实例就不需要再获取同步锁了)。
(3)jdk2及以前版本,双重检验同步的方法会出现锁定失败的情况,原因是java内存模型的设计(而且JDK5.0以后才引入volatile关键字)。
(4)由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题,不建议使用。
4、双重检验同步延迟加载(优化,使用ThreadLocal)
/*** 懒汉单例ThreadLocal—线程安全*/
public class SingleTest3 { //借助ThreadLocal对临界资源(需要同步的资源)进行局部化操作 @SuppressWarnings("rawtypes") private static final ThreadLocal preThreadLoad=new ThreadLocal(); private static SingleTest3 singleTest3; private SingleTest3() { } public static SingleTest3 getInstance(){ if(preThreadLoad.get()==null){ // 每个线程第一次都会调用 createInstance(); } return singleTest3; } @SuppressWarnings("unchecked") private static final void createInstance(){ synchronized (SingleTest3.class) { if(singleTest3==null){ singleTest3=new SingleTest3(); } } preThreadLoad.set(singleTest3); } }
说明:借助于ThreadLocal,将临界资源(需要同步的资源)线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为了线程局部范围内来作。这里的ThreadLocal也只是用作标示而已,用来标示每个线程是否已访问过,如果访问过,则不再需要走同步块,这样就提高了一定的效率。
总结:(1)只有在调用的时候才加载,进行实例化,属于延迟加载。
(2)从资源利用率角度,延迟加载比较好。但是从速度和反应时间来看,就没有饿汉单例快。
二、饿汉单例模式
/*** 饿汉单例_线程安全*/
public class SingleTest4 { private static SingleTest4 single=new SingleTest4(); private SingleTest4() { } public static SingleTest4 getInstance(){ return single; } }
说明:(1)饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。
总结下两种模式的区别:
(1)饿汉式:在加载类的时候比较慢,由于它还要去实例化一个对象并造成内存资源的浪费,但在运行调用中的速度会比较快。
(2)懒汉式,在加载类的时候比较快,由于在加载类的时候不需要去实例化对象,但在运行调用时的速度比较慢,由于还要去做判断。
(3)饿汉模式是属于线程安全,而懒汉模式属于线程不安全,在高并发时会出现问题。
三、静态单例模式
/**
* 静态单例_线程安全
* 静态内部类实现方式(也是一种懒加载方式)
* 这种方式:线程安全,调用效率高,并且实现了延迟加载
*/
public class SingleTest5 { private SingleTest5() { } private static class singleLoad{ private static final SingleTest5 INSTANCE=new SingleTest5(); } public static final SingleTest5 getInstance(){ return singleLoad.INSTANCE; } }
说明:延迟加载,线程安全(java中class加载时互斥的),也减少了内存消耗,推荐使用内部类方式。
四、登记模式
/**
* 登记模式单例_线程安全
* 类似Spring里面的方法,将类名注册,下次从里面直接获取。
*/
public class SingleTest6 { private SingleTest6() { } private static Map<String, SingleTest6> map = new HashMap<String, SingleTest6>(); //静态代码块 static { SingleTest6 single = new SingleTest6(); map.put(single.getClass().getName(), single); } //静态工厂方法,返还此类惟一的实例 public static SingleTest6 getInstance(String name) { if(name == null) { name = SingleTest6.class.getName(); System.out.println("name == null"+"--->name="+name); } if(map.get(name) == null) { try { map.put(name, (SingleTest6) Class.forName(name).newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return map.get(name); } //一个示意性的商业方法 public String about() { return "Hello, I am RegSingleton."; } public static void main(String[] args) { SingleTest6 single3 = SingleTest6.getInstance(null); System.out.println(single3.about()); SingleTest6 single4 = SingleTest6.getInstance(SingleTest6.class.getName()); if(single3==single4){ System.out.println("========="); } } }
登记式实际对一组单例模式进行的维护,主要是在数量上的扩展,通过map我们把单例存进去,这样在调用时,先判断该单例是否已经创建,是的话直接返回,不是的话创建一个登记到map中,再返回。对于数量又分为固定数量和不固定数量的。上面采用的是不固定数量的方式,在getInstance方法中加上参数(string name),然后通过子类继承,重写这个方法将name传进去。
五、枚举模式
/*** 通过枚举实现单例模式(没有延迟加载)
* 线程安全,调用效率高,不能延迟加载。
* 并且可以天然的防止反射和反序列化漏洞
*/
public enum SingleEnum { // 枚举元素,本身就是单例对象 INSTANCE; //实现方法 public void methods(){ System.out.println("执行方法"); } }
说明:
(1)这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化制,绝对防止多次实例化。
(2)线程安全,调用效率高,不能延迟加载,并且可以天然的防止反射和反序列化漏洞.
测试:
/*** 从多线程与效率方面测试*/
public class Test { public static void main(String[] args) throws Exception { long begin = System.currentTimeMillis(); int threadNum = 100; // 100个线程(10个线程的情况下,运行多次有时候耗时为0!所以让线程多一点!) final CountDownLatch countDownLatch = new CountDownLatch(threadNum); //方式一 // for (int i = 0; i < threadNum; i++) { // new Thread(new Runnable() { // @Override // public void run() { // for (int i = 0; i < 100000; i++) { Object obj1 = SingletonDemo1.getInstance(); // 15.饿汉式 Object obj2 = SingletonDemo2.getInstance(); // 156.懒汉式 Object obj3 = SingletonDemo3.getInstance(); // 16.双重检查锁,不要使用! Object obj4 = SingletonDemo4.getInstance(); // 15.静态内部类 // Object obj5 = SingleEnum.INSTANCE; // 16.枚举实现 // } // countDownLatch.countDown(); // } // }).start(); // } //方式二 for (int i = 0; i < threadNum; i++) { new Thread(new MyRunnable(countDownLatch)).start(); } countDownLatch.await(); // main线程阻塞,直到计数器变为0,才会继续往下执行 long end = System.currentTimeMillis(); System.out.println("总耗时:" + (end - begin)); } } //MyRunnable 多线程 public class MyRunnable implements Runnable { private CountDownLatch countDownLatch; public MyRunnable(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; } @Override public void run() { for (int i = 0; i < 100000; i++) { // Object obj1 = SingletonDemo1.getInstance(); // 15.饿汉式 // Object obj2 = SingletonDemo2.getInstance(); // 156.懒汉式 // Object obj3 = SingletonDemo3.getInstance(); // 16.双重检查锁,不要使用! Object obj4_1 = SingleTest5.getInstance(); // 静态 // Object obj4 = SingleTest6.getInstance(SingleTest6.class.getName()); // 31.登记 // Object obj5 = SingleEnum.INSTANCE; // 16.枚举实现 } countDownLatch.countDown(); } }
六、工厂方式实现单例模式
产品类:
/** * 单例类,以工厂方式实现 * Singleton类定义了一个private的无参构造方法,目的是不允许通过new的方式创建对象。 * Singleton类也不自己定义一个Singleton对象了,因为它要通过工厂来获得。 */ public class Singleton { private Singleton(){ } public void dosomthing(){ //具体业务逻辑实现 System.out.println("需要做的一些事情!!!"); } }
工厂单例类:
/** * 单例类,以工厂方式实现 * 通过反射方式 */ public class SingletonFactory { private static Singleton single; static{ try { System.out.println("进入实例开始======="); Class<?> clazz=Class.forName(Singleton.class.getName()); //获取无参构造方法 Constructor<?> constructor=clazz.getDeclaredConstructor(); //设置构造器方法可用 constructor.setAccessible(true); //获取实例对象,强制转换实例对象 single=(Singleton) constructor.newInstance(); System.out.println("进入实例结束======="); } catch (Exception e) { e.printStackTrace(); } } public static Singleton getInstance(){ System.out.println("结束实例======="); return single; } }
测试类:
public class MyThreadTest extends Thread{ private int aint; //int没有初始值,默认是0 public void run(){ // SingleTest.getInstance(aint); //工厂模式实现的单例 System.out.println("实例"+aint+"开始启动:"+SingletonFactory.getInstance().hashCode()); } public MyThreadTest(int i) { this.aint=i; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new MyThreadTest(i).start(); } } //运行结果 /**** * 进入实例开始======= 进入实例结束======= 结束实例======= 实例0开始启动:129126564 结束实例======= * 实例2开始启动:129126564 结束实例======= 实例4开始启动:129126564 结束实例======= * 实例1开始启动:129126564 结束实例======= 实例6开始启动:129126564 结束实例======= * 实例7开始启动:129126564 结束实例======= 实例5开始启动:129126564 结束实例======= * 实例3开始启动:129126564 结束实例======= 实例9开始启动:129126564 结束实例======= * 实例8开始启动:129126564 */ }
结论:
单例对象占用资源少,不需要延迟加载:枚举式好于饿汉式。
单例对象占用资源大,需要延迟加载:静态内部类式好于懒汉式。
常用的两种方式,饿汉式和懒汉式,单例对象占用资源少时,选用饿汉式;反之,用懒汉式。
就效率来说:由于懒汉式需要同步,效率最低。
如果单例对象占用资源少,无需延迟加载,使用饿汉式或枚举式;
如果单例对象占用资源大,需要延迟加载,使用静态内部类;