前言
最近工作中有这么一个需求,我们系统出单后,需要同步数据到合作方,合作方对数据接收并解析反馈结果文件给我们,根据结果文件状态判断合作方系统是否解析成功,对于失败的单子,需要邮件通知相关负责人。所以这里我们需要用到邮件发送工具,在使用时如果每次都new 邮件工具那么就比较耗费堆内存空间,所以这里我们使用单例模式,在整个系统运行使用过程中,只需要new 一次即可。
单例模式有几种用法,需要根据具体的业务场景来制定,如:饿汉模式、懒汉模式、线程安全版的懒汉模式,下面来看下具体的使用方法。
正文
饿汉单例模式
代码示例
public class MyEmail { private static final EmailService expose=new EmailService(); public static EmailService getInstance(){ return expose; } }
使用场景
什么时候使用它呢?我个人理解,如果服务器内存空间够大,可以使用这种方式,因为服务在启动过程中会有大量的类被实例化,如果系统中很多采用这种饿汉写法,如果堆栈空间不够的话,可能会导致内存溢出。
优缺点
优点
饿汉版的单例模式可以保证线程安全,因为类加载到内存后就会进行实例化,JVM可以保证其线程安全,这个版本比较简单实用。
缺点
饿汉版版单例模式唯一的缺点就是不管系统是否有使用到,都会进行实例化。
懒汉单例模式
代码示例
public class MyEmail { private static EmailService expose=null; public static EmailService getInstance(){ if (expose==null){ expose=new EmailService(); } return expose; } }
使用场景
我个人理解系统启动实例化时,需要大量的堆栈空间,而非急切用到的类实例,我们可以将其放到使用时在进行实例化,这样可以提高系统的稳定性;
优缺点
优点
延迟加载可以提高项目启动时速度及稳定性
缺点
虽然达到了按需初始化的目的,但却带来线程不安全的问题,下面看下这个测试用例:
public class MyEmail { private static EmailService expose=null; public static EmailService getInstance(){ if (expose==null){ try { //提高CPU让出当前线程执行其它线程的概率 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } expose=new EmailService(); } return expose; } public static void main(String[] args) { for(int i=0; i<100; i++) { new Thread(()-> System.out.println(MyEmail.getInstance().hashCode()) ).start(); } } }
由上面的测试用例,可以看到多线程的情况下存在实例被实例化多次的情况。
懒汉单例模式(线程安全版)
代码示例1
public class MyEmail2 { private static EmailService expose=null; public static synchronized EmailService getInstance(){ if (expose==null){ expose=new EmailService(); } return expose; } }
直接在方法上加synchronized,这种方式比较简单粗暴。但是效率比较低,因为锁的是整个方法,如果该方法体里面的代码比较多或者说执行时间比较长,那其它线程只能这样干等着,这会导致CPU的效率低下。
代码示例2
代码示例1中直接加锁的方式效率低下,那么我们可以采用细化锁来提高效率。
步骤1:
public class MyEmail3 { private static EmailService expose=null; public static EmailService getInstance(){ if (expose==null){ synchronized (MyEmail3.class){ expose=new EmailService(); } } return expose; } }
大家觉得这种写法是否可以实现线程安全呢?口说无凭,我们写个测试用例来校验一下;
public class MyEmail3 { private static EmailService expose=null; public static EmailService getInstance(){ if (expose==null){ try { //提高CPU让出当前线程执行其它线程的概率 Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (MyEmail3.class){ expose=new EmailService(); } } return expose; } public static void main(String[] args) { for(int i=0; i<100; i++) { new Thread(()-> System.out.println(MyEmail3.getInstance().hashCode()) ).start(); } } }
这种方式显然是有问题的,在if (expose==null)判断的时候:
线程1符合条件进入了,但是还没有执行synchronized代码,未抢占锁
线程2获得了CPU的使用权,此时expose还是为null,所以也进入了该逻辑
线程2进行synchronized代码块实例化了实例,并释放了锁
线程1获得锁,进入synchronized代码块进行实例化
基于以上几点,这种方式会存在多次创建实例的情况
步骤2:
基于步骤1的缺点,我们在synchronized中再加上一层If判断。
public static EmailService getInstance(){ if (expose==null){ synchronized (MyEmail3.class){ if (expose==null){ expose=new EmailService(); } } } return expose; }
我们来运行测试用例:
public class MyEmail3 { private static EmailService expose=null; public static EmailService getInstance(){ if (expose==null){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (MyEmail3.class){ if (expose==null){ expose=new EmailService(); } } } return expose; } public static void main(String[] args) { for(int i=0; i<100; i++) { new Thread(()-> System.out.println(MyEmail3.getInstance().hashCode()) ).start(); } } }
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/5a5dd94476af43eaa50285c32c38eac7.png = 800x)我们可以看到结果符合我们的预期,但是大家不要高兴得太早,在极端情况下这种也会存在问题。
我们需要了解下创建实例的过程(个人理解,要求严谨可自查资料):
当new之后,会在堆内存中开启一块内存空间,此时内存空间为Null
调用构造器后,会往开启的内存空间填充数据
将内存空间地址引用赋值给栈中的变量,也就是变量名称
由于此步骤中步骤2和步骤3是没有关联性的,所以在CPU执行指令的时候可能先执行3再执行2,如果是这种情况的话,那么由于栈有了引用了,但是还未填充数据,导致此时该实例的属性值为默认值(int为0,引用类型为null),那么其它线程拿到该单例的时候,对属性进行读取或者修改就会出现问题。
步骤3:
public class MyEmail3 { private static volatile EmailService expose=null; public static EmailService getInstance(){ if (expose==null){ synchronized (MyEmail3.class){ if (expose==null){ expose=new EmailService(); } } } return expose; } }
使用volatile关键字修改,可以避免指令重排的情况;
总结
工作中具体使用哪种单例模式,需要根据业务场景综合考虑,没有最好只有适合。