单例模式(思维导图)
图1 Java单例模式【点击查看大图】
单例模式:【核心】只存在一个实例化对象。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。
(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)
1,饿汉式
1 package com.cnblogs.mufasa.Demo3; 2 3 public class Singleton { 4 private static Singleton instance=new Singleton(); 5 private Singleton(){ 6 System.out.println("饿汉式 无参构造"); 7 } 8 public static Singleton getInstance(){ 9 return instance; 10 } 11 public void print(){ 12 System.out.println("饿汉式【单例设计模式】"); 13 } 14 }
调用就直接进行实例化
单例调用测试:
1 package com.cnblogs.mufasa.Demo3; 2 3 public class Demo { 4 public static void main(String[] args) throws Exception { 5 long startTime = System.currentTimeMillis(); 6 //多线程情况下 7 for(int x=0;x<1000;x++){ 8 new Thread(()->{ 9 Singleton.getInstance().print(); 10 },"单例消费端-"+x).start(); 11 } 12 13 long endTime = System.currentTimeMillis(); 14 long usedTime = (endTime-startTime); 15 System.out.println("耗时:"+usedTime);//143 16 17 } 18 }
2,普通懒汉式【非线程安全】
1 package com.cnblogs.mufasa.Demo3; 2 //普通懒汉式,多线程会出现多个实例化 3 //多线程情况下,就不是单例模式了,没有进行synchronized 4 public class Singleton1 { 5 private static Singleton1 instance=null; 6 private String str=""; 7 private Singleton1(){ 8 System.out.println("【"+Thread.currentThread().getName()+"】单线程下的单例模式"); 9 } 10 public static Singleton1 getInstance(){ 11 if(instance==null){ 12 instance=new Singleton1(); 13 } 14 return instance; 15 } 16 public void print(){ 17 System.out.println(str+"懒汉式单例模式"); 18 } 19 20 public String getStr() { 21 return str; 22 } 23 24 public void setStr(String str) { 25 this.str = str; 26 } 27 }
需要的时候才进行实例化,多线程的时候会出现实例化多个对象的情况!!!
3,初级同步懒汉式
1 package com.cnblogs.mufasa.Demo3; 2 //代价很大,以前的并发变成串行,效率降低 3 public class Singleton2 { 4 private static Singleton2 instance = null; 5 private Singleton2(){ 6 System.out.println("【"+Thread.currentThread().getName()+"】单线程下的单例模式"); 7 } 8 public static synchronized Singleton2 getInstance(){ 9 if(instance==null){ 10 instance=new Singleton2(); 11 } 12 return instance; 13 } 14 public void print(){ 15 System.out.println("懒汉式【单例设计模式】添加getInstance同步修饰"); 16 } 17 }
对getInstance进行synchronized修饰,但是这个把原本的并发操作进行串行转换,低效
4,高级同步懒汉式【无volatile修饰】
1 package com.cnblogs.mufasa.Demo3; 2 //双重效验锁 3 //只是在处理空对象时,才进行同步操作,只有一次是同步其他全部是并发非同步处理 4 public class Singleton3 { 5 // private static volatile Singleton3 instance=null; 6 private static Singleton3 instance = null;//添加volatile修饰符 7 private Singleton3(){ 8 System.out.println("【"+Thread.currentThread().getName()+"】*** 单线程下的【单例模式】 ***"); 9 } 10 public static Singleton3 getInstance(){//还是并发处理 11 if(instance==null){ 12 synchronized (Singleton3.class){ 13 if(instance==null){ 14 instance=new Singleton3(); 15 } 16 } 17 } 18 return instance; 19 } 20 public void print(){ 21 System.out.println("懒汉式【单例设计模式】"); 22 } 23 }
也叫做双重效验锁,进行两次判断,并且第二次之前进行同步操作处理
接下来我解释一下在并发时,双重校验锁法会有怎样的情景:
STEP 1. 线程A访问getInstance()方法,因为单例还没有实例化,所以进入了锁定块。
STEP 2. 线程B访问getInstance()方法,因为单例还没有实例化,得以访问接下来代码块,而接下来代码块已经被线程1锁定。
STEP 3. 线程A进入下一判断,因为单例还没有实例化,所以进行单例实例化,成功实例化后退出代码块,解除锁定。
STEP 4. 线程B进入接下来代码块,锁定线程,进入下一判断,因为已经实例化,退出代码块,解除锁定。
STEP 5. 线程A获取到了单例实例并返回,线程B没有获取到单例并返回Null。
5,volatile修饰懒汉式
1 package com.cnblogs.mufasa.Demo3; 2 //双重效验锁 3 //只是在处理空对象时,才进行同步操作,只有一次是同步其他全部是并发非同步处理 4 public class Singleton3 { 5 private static volatile Singleton3 instance=null; 6 // private static Singleton3 instance = null;//添加volatile修饰符 7 private Singleton3(){ 8 System.out.println("【"+Thread.currentThread().getName()+"】*** 单线程下的【单例模式】 ***"); 9 } 10 public static Singleton3 getInstance(){//还是并发处理 11 if(instance==null){ 12 synchronized (Singleton3.class){ 13 if(instance==null){ 14 instance=new Singleton3(); 15 } 16 } 17 } 18 return instance; 19 } 20 public void print(){ 21 System.out.println("懒汉式【单例设计模式】"); 22 } 23 }
保持内存可见性和防止指令重排序,直接跳过副本对内存进行操作
6,Holder式
1 package com.cnblogs.mufasa.Demo3; 2 3 public class Singleton4 { 4 private static class SingletonHolder{ 5 private static Singleton4 instance=new Singleton4(); 6 } 7 private Singleton4(){ 8 System.out.println("无参构造"); 9 } 10 public static Singleton4 getInstance(){ 11 return SingletonHolder.instance; 12 } 13 public void print(){ 14 System.out.println("holder模式实现的线程安全单例"+Thread.currentThread().getName()); 15 } 16 }
静态类内部加载,通过调用内部静态类——内部静态中的静态属性初始化——来创建对象
7,枚举法实现
1 package com.cnblogs.mufasa.Demo3; 2 3 enum Singleton5 { 4 INSTANCE; 5 public void print(){ 6 System.out.println("枚举法实现的线程安全单例模式"+Thread.currentThread().getName()); 7 } 8 }
利用枚举本身特性实现,(1)自由串行化。(2)保证只有一个实例。(3)线程安全。
枚举与其他的单例调用方式不太一样:
1 package com.cnblogs.mufasa.Demo3; 2 3 public class Demo5 { 4 public static void main(String[] args) { 5 long startTime = System.currentTimeMillis(); 6 //多线程情况下 7 for(int x=0;x<1000;x++){ 8 new Thread(()->{ 9 Singleton5.INSTANCE.print(); 10 },"单例消费端-"+x).start(); 11 } 12 long endTime = System.currentTimeMillis(); 13 long usedTime = (endTime-startTime); 14 15 System.out.println("耗时:"+usedTime);//144 16 } 17 }
8,ThreadLocal实现
1 package com.cnblogs.mufasa.Demo3; 2 //使用ThreadLocal实现单例模式(线程安全) 3 public class Singleton6 { 4 private Singleton6(){} 5 private static final ThreadLocal<Singleton6> tlSingleton=new ThreadLocal<>(){ 6 @Override 7 protected Singleton6 initialValue(){ 8 return new Singleton6(); 9 } 10 }; 11 12 public static Singleton6 getInstance(){ 13 return tlSingleton.get(); 14 } 15 16 public static void print(){ 17 System.out.println("ThreadLocal实现单例模式"); 18 } 19 20 }
每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突
通过牺牲空间换取时间——原始数据是同样的但是我们不使用原始数据,我们使用它的拷贝副本,避免各个线程争抢资源【以前有一个球大家都抢来抢去,现在给大家伙每人发一个,大家都不抢了】!有点像原型模式的策略!
2022.01.05 更新:使用的ThreadLocal方式,会给每一个线程都创建一个新对象。ThreadLocal只能确保线程安全。(本质来讲不是单例!)
9,CAS锁实现
1 package com.cnblogs.mufasa.Demo3; 2 3 import java.util.concurrent.atomic.AtomicReference; 4 5 //使用CAS锁实现(线程安全) 6 public class Singleton7 { 7 private static final AtomicReference<Singleton7> INSTANCE = new AtomicReference<Singleton7>(); 8 private Singleton7(){ 9 } 10 public static final Singleton7 getInstance(){ 11 for(;;){ 12 Singleton7 current=INSTANCE.get(); 13 if(current!=null){ 14 return current; 15 } 16 current=new Singleton7(); 17 if(INSTANCE.compareAndSet(null,current)){ 18 System.out.println("同步锁比较"); 19 return current; 20 } 21 } 22 } 23 public static void print(){ 24 System.out.println("CAS锁实现单例模式"); 25 } 26 27 }
通过编程的原子操作来进行同步
(1)使用总线锁保证原子性【一个CPU操作共享变量内存地址的缓存的时候,锁住总线其他CPU不能插手——变成串行化操作,一个个的来】
(2)使用缓存锁保证原子性【内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效】
(3)Java使用循环CAS实现原子操作
https://blog.csdn.net/zxx901221/article/details/83033998
10,登记式单例
Spring的IOC-实现中就使用的在这种策略,Definition中缓存的的实例,下次再使用的时候直接调用即可。
package com.cnblogs.mufasa.Demo3; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; public class Singleton8 { private static Map<String,Singleton8> map=new HashMap<String, Singleton8>(); static { Singleton8 singleton=new Singleton8(); map.put(singleton.getClass().getName(),singleton); } protected Singleton8(){} public static Singleton8 getInstance(String name){ if(name==null){ name=Singleton8.class.getName(); System.out.println("name == null"+"--->name="+name); } if(map.get(name)==null){ try { map.put(name,(Singleton8) Class.forName(name).getDeclaredConstructor().newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return map.get(name); } public void print(){ System.out.println("利用反射&类名注册机制实现单例"); } }
利用反射和类名注册实现的单例模式
package com.cnblogs.mufasa.Demo3; public class Demo8 { public static void main(String[] args) { long startTime = System.currentTimeMillis(); //多线程情况下 for(int x=0;x<1000;x++){ new Thread(()->{ Singleton8.getInstance("com.cnblogs.mufasa.Demo3.Singleton8").print(); },"单例消费端-"+x).start(); } long endTime = System.currentTimeMillis(); long usedTime = (endTime-startTime); System.out.println("耗时:"+usedTime);//143 } }
11,总结
请编写单例设计模式
·【100%】直接编写一个饿汉式的单例模式,并且实现构造方法私有化;
·【120%】在Java中哪里使用到单例设计模式?Runtime类、Pattern类、Spring框架;
·【200%】懒汉式单例设计模式问题?多线程情况下,可能产生多个实例化对象,违法了其单例的初衷!
12,如何破解单例模式?
单例模式是只能产生一个实例化对象,构造方法私有化,不能通过普通的方法进行实例化。
如果想要获取新的实例化对象,要怎么办呢?
①直接跳过无视私有化构造:反射机制
②我压根不新建立一个实例化对象,跳过私有化构造,我直接进行开辟新空间的数据深拷贝:原型模式
以上两种方法都可以无视单例模式,获取多个实例化对象。
13,参考链接
链接1:https://blog.csdn.net/jason0539/article/details/23297037
链接2:https://blog.csdn.net/qq_35860138/article/details/86477538