单例模式
Posted hhhshct
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式相关的知识,希望对你有一定的参考价值。
在学习单例模式前,我们首先要了解两个问题。
1、单例模式有哪些作用
第一、控制资源的使用,通过线程同步来控制资源的并发访问;第二、控制实例产生的数量,达到节约资源的目的。第三、作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。
2、什么时候需要使用单例
一个类在应用中如果有两个或者两个以上的实例会引起错误,或者某个类在整个应用中,同一时刻,有且只能有一种状态,则需要被设计为单例。在日常应用中使用的比较多就是配置类,还有我们常用的数据库连接池等都是使用了单例模式。
java中单例模式的几种的方式
1、饿汉式
package singleton; public class HungerSingleton { private static HungerSingleton simpleSingleton = new HungerSingleton(); private HungerSingleton(){}; public static HungerSingleton getSingleton() { return simpleSingleton; } }
优点:线程安全,访问时速度快,因为项目启动时已经创建好;缺点:项目启动慢,初始化需要占用空间即便没有用到
2、简单懒汉式
package singleton; public class SimpleSingleton { private static SimpleSingleton simpleSingleton; private SimpleSingleton(){}; public static SimpleSingleton getSingleton() { if(simpleSingleton== null) { simpleSingleton = new SimpleSingleton(); } return simpleSingleton; } }
优点:使用时才会创建,节省空间,启动速度快,缺点:多线程下容易创建多个实例,造成莫名的错误,通过下面一个简单的例子可以验证
package singleton; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Client { private volatile boolean flag; public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } public static void main(String[] args) throws InterruptedException { Set<String> set = new HashSet<>(); ExecutorService executorService = Executors.newCachedThreadPool(); Client client = new Client(); client.setFlag(false); for(int i=0;i<100;i++) { executorService.execute(new Runnable() { @Override public void run() { while(true) { if(client.isFlag()) { SimpleSingleton s = SimpleSingleton.getSingleton(); set.add(s.toString()); break; } } } }); } Thread.sleep(5000); client.setFlag(true); Thread.sleep(5000); System.out.println("并发情况下获取的实例情况"); for (String string : set) { System.out.println(string); } executorService.shutdown(); } }
运行结果如下:
3、同步懒汉式
package singleton; public class SynSingleton { private static SynSingleton simpleSingleton; private SynSingleton(){}; public static synchronized SynSingleton getSingleton() { if(simpleSingleton== null) { simpleSingleton = new SynSingleton(); } return simpleSingleton; } }
优点:线程安全,启动快,初始化时不占用空间,缺点:同步代码较多,容易造成线程等待
4、双重检查懒汉式
package singleton; public class DoubleCheckSingleton { private static DoubleCheckSingleton simpleSingleton; private DoubleCheckSingleton(){}; public static DoubleCheckSingleton getSingleton() { if(simpleSingleton== null) { synchronized(DoubleCheckSingleton.class) { if(simpleSingleton== null) { simpleSingleton = new DoubleCheckSingleton(); } } } return simpleSingleton; } }
优点:线程安全,启动快,节省初始化空间,效率高,缺点:代码复杂,可能出现未知错误,至于为什么会出现未知错误,那我们就要先了解jvm创建对象的过程。
jvm创建对象分为三个步骤:1.分配内存2.初始化构造器3.将对象指向分配的内存的地址,这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了,因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给synchronizedSingleton,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为synchronizedSingleton对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了synchronizedSingleton,就会产生莫名的错误。
5、双重检查 + volatile
这种就是在双重检查的基础上给 simpleSingleton 属性加上volatile关键字,这样便可以禁止jvm指定重排序,即避免上双重检查出现未知错误的情况。
6、最常用的单例模式,静态内部类
package singleton; public class StandardSingleton { private StandardSingleton(){}; public static StandardSingleton getSingleton() { return InnerSingleton.standardSingleton; } private static class InnerSingleton{ static StandardSingleton standardSingleton = new StandardSingleton(); } }
关于这种方式,有人会问会不会和双重检查有一样的问题,答案是不会,因为jvm在加载静态属性是内部实现了同步,所以不用考虑多线程下的问题。
以上是关于单例模式的主要内容,如果未能解决你的问题,请参考以下文章