单例模式
保证一个类只有一个实例,并且提供一个全局访问点
适用场景
确保任何情况下都绝对只有一个实例
单服务情况下网站计数器可以用单例,数据库连接池,数据库线程池
优点
在内存中只有一个实例减少了内存开销,在对象频繁需要创建销毁时,并且创建销毁的性能无法优化,这时候单例模式有很明显的优势
可以避免对资源的多重占用(such as :要对一个文件进行写操作,由于只有一个对象可以避免同时写操作)
设置了全局访问点严格控制访问(对外不能new出来对象,只能通过方法来创建单例对象)
缺点
没有接口,扩展困难(如果要扩展的话就得修改代码.)
重点
私有构造器
禁止从外部调用构造函数,创建对象
线程安全
延迟加载
使用到的时候在创建,需要延迟加载
序列化和反序列化安全
单例对象一旦序列化和反序列化,就会破坏单例
反射
懒汉单例模式(代码笔记)
package com.sun.lazy;
public class LazySingLeton {
//声明静态的单例对象
//在初始化的时候没有创建,而是做延迟加载
private static LazySingLeton lazySingLeton = null;
//私有构造器 不让外部new
private LazySingLeton(){
}
//得到实例
//这种方式是线程不安全的
//在单线程的时候这种写法是没有问题的
//============================================
//note:当第一个线程准备创建对象但是没有执行时,第二个线程去判断结果为true,
//第二个线程也会在new一个对象,这就创建了两个对象,返回最后执行创建的对象
//加上一个synchronized使方法变为同步方法,因为是static方法相当于锁是类的class文件
//如果不是static方法相当于锁是堆内存中生成的一个对象
//使用同步多线程后对象在第一个线程中已经生成,第二个线程将会直接返回生成的对象
public /*synchronized*/ static LazySingLeton getInstance(){
synchronized (LazySingLeton.class) {
if (lazySingLeton == null) {
lazySingLeton = new LazySingLeton();
}
}
return lazySingLeton;
}
}
由于 synchronized同步锁会耗费很多资源,并且同步静态方法等同于同步类文件,耗费更大,下面使用双重检查的方法来设计单例模式
双重检查(double check)
减少了性能开销,减少了 synchronized的锁定范围
new对象的步骤
- 分配内存对象
- 初始化对象
- 设置对象指向刚分配的内存空间
注意:在进行到2和3的时候会经历重排序,他的顺序可能会被颠倒
在多线程的时候,线程1在运行到指向内存空间的时候线程2进行判断,结果不为null然后将还没有初始化的对象返回
package com.sun.doublecheck;
public class DoubleCheck {
//使用volatile可以禁止重排序
//在加了volatile后线程就都能看到共享内存的最新状态
//会将当前内存缓存好的数据写回到系统内存,会使其他内存缓存了该数据无效,因为
//无效,又会从共享内存同步数据(缓存一致性协议),多cpu可能会读到其他cpu过期的内存
private volatile static DoubleCheck doubleCheck = null;
private DoubleCheck(){
}
public static DoubleCheck getInstance(){
if (doubleCheck==null){
synchronized (DoubleCheck.class){
if (doubleCheck==null){
doubleCheck=new DoubleCheck();
}
}
}
return doubleCheck;
}
}
上面的方法是通过禁止重排序来实现,也可以让其他线程看不到本线程的重排序
基于类初始化的延迟加载(静态内部类单例)
在刚开始的时候jvm会初始化类,在执行初始化类的期间,jvm会获取一个锁(只有一个线程可以获得这个锁),这个锁可以同步多个线程对一个类的初始化,基于这个特性可以实现基于线程安全的静态内部类延迟初始化方案
非构造函数看不到;类的初始化
- 初始化一个类
- 执行这个类的静态初始化
package com.sun.staticInnerClass;
public class StaticInner {
private static class InnerClass{
private static StaticInner staticInner=new StaticInner();
}
public static StaticInner getInstance(){
return InnerClass.staticInner;
}
//一定要声明私有构造器
private StaticInner(){
}
}
饿汉模式
在类加载的时候就完成实例化,这也是饿汉和懒汉最大的区别
因为在类加载的时候就加载好所以多线程下也没有问题
package com.sun.staticInnerClass;
public class StaticInner {
private final static StaticInner s;
static {
s=new StaticInner();
}
private StaticInner(){
}
public static StaticInner getInstance(){
return s;
}
}
序列化和反序列化破坏单例模式
将对象序列化到文件中,在读出来是两个不同的对象
只需要在实现序列化的类中加上readResolve方法就行
反射攻击破坏单例
通过反射的方式,使用setaccessible将构造器置为true,这样可以实例化出对象
解决
这个方法只对静态单例有效
package com.sun.staticInnerClass;
public class StaticInner {
private final static StaticInner s;
static {
s=new StaticInner();
}
private StaticInner(){
if (s!=null){
throw new RuntimeException("单例禁止反射调用")
}
}
public static StaticInner getInstance(){
return s;
}
}
可以通过改造构造器来禁止反射()
//是true的时候代表可以实例化
private boolean fl = true;
private StaticInner(){
if (fl){
//第一次实例化后等于false
fl=false;
}else{
throw RuntimeException("单例模式不能使用反射创建");
}
}
这个方法仍然可以通过反射来修改字段从而完成反射攻击
Enum单例
这种方式既可以防御反射,又可以不被序列化破坏