单例模式的6种实现方式
一、单例模式的定义
**定义:**确保一个类只有一个实例,并提供该实例的全局访问点。
这样做的好处是:有些实例,全局只需要一个就够了,适用单例模式就可以避免一个全局适用的类,频繁的创建和销毁,耗费系统资源。
二、单例模式的设计要素
一个私有构造函数(确保只能单例类自己创建实例)
一个私有静态变量(确保只有一个实例)
一个公有静态函数(给使用者提供调用方法)
简单来说就是,单例类的构造方法不让其他人修改和适用;并且单例类自己只创建了一个实例,这个实例,其他人也无法修改和直接适用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例。
三、单例模式的6种实现及各种实现的优缺点
(一) 懒汉式(线程不安全)
实现
public class Singleton{
private static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getUniqueInstance(){
if(uniqueInstance==null){
uniqueInstance=new Singleton();
}
return uniqueInstance;
}
}
说明:先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
优点: 延迟了实例化,如果不需要适用该类,就不会被实例化,节约了系统资源。
缺点:线程不安全,多线程环境下,如果多个线程同时进入了if(uniqueInstancenull),若此时还未实例化,也就是uniqueInstancenull,那么就会有多个线程执行uniqueInstance=new Singleton(0; ,就会实例化多个实例;
(二)、饿汉式(线程安全)
实现:
public class Singleton{
private static Singleton uniqueInstance =new Singleton();
private Singleton(){
}
public static Singleton getUniqueInstance(){
return uniqueInstance;
}
}
**说明:**先不管需不需要适用这个实例,直接先实例化好实例(饿死鬼一样,所以称为饿汉式),然后当需要适用的时候,直接调用方法就可以适用了。
优点: 提前实例化好了一个实例,避免了线程不安全问题的出现。
**缺点:**直接实例化好了实例,不再延迟实例化;若系统没有适用这个实例,或者系统运行很久之后才需要适用这个实例,都会造成操作系统的浪费。
(三) 懒汉式(线程安全)
实现:
public class Singleton{
private static Singleton uniqueInstance;
private static singleton(){}
private static synchronized Singleton getUniqueInstance(){
if(uniqueInstance==null){
uniqueInstance=new Singleton();
}
return uniqueInstance;
}
}
**说明:**实现和线程不安全的懒汉式几乎一样,唯一不同的点是,在get方法上加了一把锁。如此一来,多个线程访问,每次只有拿到锁的线程才能够进入该方法,避免了多线程不安全问题的出现。
优点: 延迟实例化,节约了资源,并且是线程安全的。
缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使已经实例化了,即后续不会再出现线程问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法使线程阻塞,等待时间过长。
(四)双重检查锁实现(线程安全)
实现:
pblic class Singleton{
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getUniqueInstance(){
if(uniqueInstance==null){
synchronized(Singleton.class){
if(uniqueInstance==null){
uniqueInstance=new Singleton();
}
}
}
return uniqueInstance;
}
}
**说明:**双重检查锁相当于是改进了线程安全的懒汉式。线程安全的懒汉式的缺点是性能降低了,造成的原因是因为即使实例化已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会有线程阻塞的问题。
为什么适用volatile 关键字修饰了uniqueInstance实例变量?
uniqueInstance=new Singleton(); 这段代码执行时分为三步:
1.为uniqueInstance 分配内存空间
2.初始化uniqueInstance
3.将uniqueInstance 指向分配的内存地址
正常的执行顺序当然时1>2>3, 但是由于JVM具有指令多重的特性,执行顺序有可能变成1>3>2 .
单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
例如:线程A只执行了1和3,此时线程B来调用getUniqueInstance(),发现uniqueInstance 不为空,便获取uniqueInstance 实例,但是其实此时的uniqueInstance 还没有初始化。
解决办法就是加一个volatile关键字来修饰uniqueInstance ,volatile 会禁止JVM 的指令重排,就可以保证多线程环境下的安全运行。
优点: 延迟实例化,节约了资源;线程安全;并且相对于线程安全的懒汉式,性能提高了。
**缺点:**volatile 关键字,对性能也有一些影响。
(五) 静态内部类实现(线程安全)
实现:
public class Singleton{
private Singleton(){}
private static class SingletonHolder{
private static final Singleton INSTANCE=new Singleton();
}
public staic Singleton getUniqueInstance(){
return SingletonHolder.INSTANCE;
}
}
**说明:**首先,当外部类 Singleton 被加载时,静态内部类SingletonHolder 并没有被加载进内存。当调用getUniqueInstance() 方法时,会运行return SingletonHolder.INSTANCE; ,触发了SingletonHoder.INSTANCE,此时静态内部类SingletonHolder 才会被加载进内存,并且初始化INSTANCE 实例,并且JVM会确保 INSTANCE只被实例化一次。
(六)、枚举实现(线程安全)
public enum Singleton{
INSTANCE;
//添加自己需要的操作
public void doSomeThing(){
}
}
**说明:**默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。
**优点:**写法简单,线程安全,天然防止反射和反序列化调用。
防止反序列化
**序列化:**把java对象转换为字节序列的过程;
**反序列化:**通过这些字节序列在内存种新建java对象的过程。
**说明:**反序列化将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。我们要防止反序列化,避免得到多个实例。
枚举类天然防止序列化。
其他单例模式可以通过重写readResolve()方法,从而防止反序列化,实例唯一重写readResolve():
private Object readResolve() throws ObjectStreamException{
return singleton;
}
四、单例模式的应用场景
应用场景举例:
网站计数器。
应用程序的日志应用。
Web项目中的配置对象的读取。
数据库连接池。
多线程池。
…
适用场景总结:
频繁实例化然后又销毁的对象,使用单例模式可以提高性能。
经常使用的对象,但实例化时耗费时间或者资源多,如数据库连接,使用单例模式,可以提高性能,降低资源耗费。
使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信。