单例模式

常见应用场景:

1、Windows的Task Manager(任务管理器)就是很典型的单例模式,windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
2、项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
3、网站的计数器,一般也是采用单例模式实现,否则难以同步。
4、应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作否则内容不好追加。
5、数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
6、操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
7、在servlet编程中,每个Servlet也是单例,Application 也是单例的典型应用(Servlet编程中会涉及到)
8、在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理,在spring MVC框架/struts1框架中,控制器对象也是单例
9、一个产品注册了一个商标,那么它就是单例的

单例优缺点

优点:
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
5…避免对共享资源的多重占用。
缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出。

单例创建方式

  1. 饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。

  2. 懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。每次调用getInstance()方法都要同步,并发效率较低,线程不安全

  3. 静态内部方式:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
    优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
    劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建。

  4. 枚举单例: 使用枚举实现单例模式 优点:线程安全(当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承)、实现简单、调用效率高,枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点没有延迟加载。
    单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文:

Enum constants are serialized differently than ordinary serializable or externalizable objects. 
The serialized form of an enum constant consists solely of its name; 
field values of the constant are not present in the form. 
To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. 
To deserialize an enum constant, ObjectInputStream reads the constant name from the stream;
 the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method,
 passing the constant’s enum type along with the received constant name as arguments. 
Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.
 The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, 
and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or 
serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. 
Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
        }  

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。

所以,JVM对序列化有保证。

  1. 双重检测锁方式,由于JVM底层内部模型原因,偶尔会出问题。不建议使用
    问题出在 Instance = new SingleTon();这句话在执行的过程。首先应该进行对象的创建操作大体可以分为三步:
     (1)分配内存空间。
     (2)初始化对象即执行构造方法。
     (3)设置 Instance 引用指向该内存空间。
     那么如果有指令重排的前提下,这三部的执行顺序将有可能发生变化:  
     (1)分配内存空间。
     (2)设置 Instance 引用指向该内存空间。
     (3)初始化对象即执行构造方法。
    如果按照上述的语义去执行,单看线程 A 中的操作虽然指令重排了,但是返回结果并不影响。但是这样造成的问题也显而易见,b 线程将返回一个空的 Instance,可怕的是我们认为这一切是正常执行的。
    为了解决上述问题我们可以从两个方面去考虑:
  2. 避免指令重排
  3. 让 A 线程完成对象初始化后,B 再去判断 instance == null
//饿汉式
public class SingletonDemo01 {
	// 类初始化时,会立即加载该对象,线程天生安全,调用效率高
	private static final SingletonDemo01 singletonDemo01 = new SingletonDemo01();

	private SingletonDemo01() {
		System.out.println("SingletonDemo01初始化");
	}

	public static SingletonDemo01 getInstance() {
		System.out.println("getInstance");
		return singletonDemo01;
	}

	public static void main(String[] args) {
		SingletonDemo01 s1 = SingletonDemo01.getInstance();
		SingletonDemo01 s2 = SingletonDemo01.getInstance();
		System.out.println(s1 == s2);
	}

}

//懒汉式
public class SingletonDemo02 {

	//类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
//private static finalSingletonDemo02 singletonDemo02;错误,因为不可声明为 final ,final修饰要初始化,编译不通过
//private static final SingletonDemo02 singletonDemo02 = null;错误,因为singletonDemo02 = new SingletonDemo02();不可赋值给final
	private static SingletonDemo02 singletonDemo02;

	private SingletonDemo02() {
   
	}

	public synchronized static SingletonDemo02 getInstance() {
		if (singletonDemo02 == null) {
			singletonDemo02 = new SingletonDemo02();
		}
		return singletonDemo02;
	}

	public static void main(String[] args) {
		SingletonDemo02 s1 = SingletonDemo02.getInstance();
		SingletonDemo02 s2 = SingletonDemo02.getInstance();
		System.out.println(s1 == s2);
	}

}

// 静态内部类方式
public class SingletonDemo03 {
	private SingletonDemo03() {
           System.out.println("初始化..");
	}

	private static class SingletonClassInstance {
		private static final SingletonDemo03 singletonDemo03 = new SingletonDemo03();
	}

	// 方法没有同步
	public static SingletonDemo03 getInstance() {
		System.out.println("getInstance");
		return SingletonClassInstance.singletonDemo03;
	}

	public static void main(String[] args) {
		SingletonDemo03 s1 = SingletonDemo03.getInstance();
		SingletonDemo03 s2 = SingletonDemo03.getInstance();
		System.out.println(s1 == s2);
	}
}

/**
 * 使用枚举方法实现单例模式
 */
public enum ClassD {
    //定义一个枚举的元素,它就代表了Singleton的一个实例。
    INSTANCE;
    //对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public void otherMethod(){
        //功能处理
    }
    
    //测试
    public static void main(String[] args) {
        ClassD a = ClassD.INSTANCE;
        ClassD b = ClassD.INSTANCE;
        System.out.println(a==b);
    }
}

//使用枚举实现单例模式 优点:实现简单、枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞 缺点没有延迟加载
public class User {
	public static User getInstance() {
		return SingletonDemo04.INSTANCE.getInstance();
	}
	private User(){
        
    }

	private static enum SingletonDemo04 {
		INSTANCE;
		// 枚举元素为单例
		private User user;

		private SingletonDemo04() {
			System.out.println("SingletonDemo04");
			user = new User();
		}

		public User getInstance() {
			return user;
		}
	}

	public static void main(String[] args) {
		User u1 = User.getInstance();
		User u2 = User.getInstance();
		System.out.println(u1 == u2);
	}
}

//双重检测锁
public class SingletonDemo04 {
	private volatile static SingletonDemo04 singletonDemo04 = null;

	private SingletonDemo04() {

	}

	Public static SingletonDemo04 getInstance() {
	//这次判空是避免了多线程争抢锁等待锁,减少内存的开销
if (singletonDemo04 == null) {
			synchronized (SingletonDemo04.class) {
				if (singletonDemo04 == null) {//这次判空,避免已有线程创建了实例,重复创建问题
					singletonDemo04 = new SingletonDemo04();
				}
			}
		}
		return singletonDemo04;
	}

}

防止反射漏洞攻击

在jdk中ObjectInputStream的类中有readUnshared()方法,上面详细解释了原因。我简单描述一下,那就是如果被反序列化的对象的类存在readResolve这个方法,他会调用这个方法来返回一个“array”,然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。

public class SingletonDemo7 implements Serializable{
	
	private static class SingletonClassInstance {
		private static final SingletonDemo7 instance = new SingletonDemo7();
	}
	
	// 方法没有同步,调用效率高
	public static SingletonDemo7 getInstance() {
		return SingletonClassInstance.instance;
	}
	
	// 防止反射获取多个对象的漏洞
	private SingletonDemo7() {
		if (null != SingletonClassInstance.instance)
			throw new RuntimeException();
	}
	
	// 防止反序列化获取多个对象的漏洞
// 防止反序列化获取多个对象的漏洞。
	// 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
	// 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象
	private Object readResolve() throws ObjectStreamException {  
		return SingletonClassInstance.instance;
	}
	}

其他不常用单例创建方式

使用ThreadLocal实现

线程安全,ThreadLocal采用以空间换时间的方式,为每一个线程都提供一份变量,因此可以同时访问而互不影响。代码如下:

/**
 * 使用ThreadLocal实现单例模式
 */
public class ClassF {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassF(){ }
    //2.在类的内部创建一个类的实例
    private static final ThreadLocal<ClassF> tls = new ThreadLocal<ClassF>(){
        @Override
        protected ClassF initialValue(){
            return new ClassF();
        }
    };
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public static ClassF  getInstance(){
        return tls.get();
    }

    //测试
    public static void main(String[] args) {
        ClassF a = ClassF.getInstance();
        ClassF b = ClassF.getInstance();
        System.out.println(a==b);
    }
}


使用CAS锁来实现

(CAS锁(Compare and Swap):比较并交换,是一种有名的无锁算法,属于乐观锁)。用CAS锁来实现单例模式是线程安全的,代码如下:

/**
 * 使用CAS锁来实现单例模式
 */
public class ClassG {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassG(){ }
    //2.在类的内部创建一个类的实例
    private static final AtomicReference<ClassG> instance = new AtomicReference<ClassG>(); 
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public static final ClassG getInstance(){
        for(;;){
            ClassG current = instance.get();
            if(current != null){
                return current;
            }
            current = new ClassG();
            if(instance.compareAndSet(null,current)){
                return current;
            }
        }
    }

    //测试
    public static void main(String[] args) {
        ClassG a = ClassG.getInstance();
        ClassG b = ClassG.getInstance();
        System.out.println(a==b);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值