赞
踩
1、定义:保证一个类只有一个实例,并提供一个全局访问点
2、类型:创建型
3、适用场景
4、优点:
5、缺点:
6、重点
7、懒汉式和饿汉式的区别
8、UML
懒汉式是延迟加载的,并没有在类加载时就被初始化,如下所示:
//懒汉式
private static LazySingleton lazySingleton = null;
//饿汉式
private static HungrySingleton hungrySingleton = new HungrySingleton();
//创建懒汉类 public class LazySingleton { //声明一个静态的要被单例的对象 private static LazySingleton lazySingleton = null; //懒汉式比较懒,在初始化时是没有创建的,而是做了延迟加载 private LazySingleton() { } //获取LazySingleton对象的方法 public static LazySingleton getInstance() { //如果对象是空的就new一个,否则就直接返回 if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } //这种方式只能用于单线程,它是线程不安全的, // 一个线程到达了lazySingleton = new LazySingleton();而另一个线程到达了if判断那一行...这样会导致多个对象产生 }
//实现Runnable接口来实现多线程
public class T implements Runnable {
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " " + instance);
}
}
public class Test {
public static void main(String[] args) {
// LazySingleton instance = LazySingleton.getInstance(); //单线程的方式
//多线程的方式,run的话得到的对象都是一样的,这边要使用多线程debug
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("end");
}
}
多线程debug方法
设置多线程debug。在这打上断点,对1断点按右键,suspend选Thread挂起方式,选中后能用断点控制线程的执行顺序,如果是all只会debug到本线程。make Default表示以后断点都是你选择的这个方式,但是之前打的断点不会修改。
对于Thread t0 和 t1,如果t1没有拿到对象,而t0拿到对象并且执行完,那么全都执行完时,这两个线程输出的对象的地址是不一样的;两个都未执行完的情况下,t0拿到了对象,t1没拿到对象,等t1拿到对象后,会把t0的对象覆盖,最后程序都执行完后,两个线程拿到的对象都是相同的。
在getInstance
方法上加synchronized
关键字使它变成同步方法
//锁静态方法写法 public synchronized static LazySingleton getInstance() { if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } //第二种写法 public LazySingleton getInstance() { synchronized (LazySingleton.class) { if (lazySingleton == null) { lazySingleton = new LazySingleton(); } } return lazySingleton; } //再进行debug我们可以看到Thread的状态会变为`Monitor`,被阻塞,只有另一个线程走完了才会变为`RUNNING`
缺点:synchronized锁太大,对性能有影响。
这种方式兼顾了性能和线程安全,也是懒加载的.
我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。
public static LazyDoubleCheckSingleton getInstance() { //这边不加锁,如果不为null,直接返回,如果为null,也只有一个线程能够进入内部,保证只有一个线程能创建对象,对象创建好后,以后调用这个方法都不需要加锁,直接返回对象,大大降低synchronized加在方法上带来的开销 if (lazyDoubleCheckSingleton == null) { //锁定这个单例的类 synchronized (LazyDoubleCheckSingleton.class) { //再做一层判断 if (lazyDoubleCheckSingleton == null) { lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); //隐患2 //上面这一行进行了三个操作,在步骤2和3可能进行了重排序,2和3顺序可能被颠倒 //1、分配内存给对象 //2、初始化对象 //3、设置lazyDoubleCheckSingleton指向刚分配的内存地址 } } } return lazyDoubleCheckSingleton; }
但是这个方法也不是无懈可击,问题出在这个new,假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
如果是单线程,没什么问题
如果是多线程的情况,线程0只是设置了指向内存的空间,海内初始化,但是此时线程1到了,看到内存中有instance地址,然后访问了一个尚未初始化的对象。
解决思路是:
1、volatile + double check
不允许步骤2和步骤3(初始化和指向内存空间)进行重排序,即使用volatile
来声明这个instance,这样重排序就会被禁止
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
2、通过静态内部类来解决–基于类初始化的延迟加载
允许线程0中的2和3重排序,但是不允许线程1看到这个重排序
public class StaticInnerClassSingleton {
//在静态内部类中直接new了一个instance的对象
//这个方法的核心在于静态内部类InnerClass的初始化锁被哪个线程拿到,哪个线程就去初始化它
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
//私有构造器是必须的
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
}
原理:jvm在类的初始化阶段期间会获取一个锁,这个锁可以同步多个线程对一个类的初始化(绿色部分),线程0和线程1试图去获取这个锁的时候,肯定只有一个线程能获得锁,假设线程0获得了,由线程0去执行静态内部类的初始化,对于静态内部类,即使步骤2和步骤3存在重排序,但是线程1是无法看到的(因为没有获得锁),只能等待。五种A类被立刻初始化的情况:
1、有一个A类型的实例被创建
2、A类中声明的一个静态方法被调用了
3、A类中声明的一个静态成员被赋值
4、A类中声明的一个静态成员被使用,且这个成员不是常量成员
5、A类是一个顶级类,且类中有嵌套的断言语句
在类加载的时候就完成实例化,我们可以把它设置为final,这样就不可更改了
public class HungrySingleton {
//直接实例化
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
优点是类加载是写法简单,避免了线程同步问题,但缺点是,如果这个类自始至终都没用过,就造成了内存浪费,我们把单例实例化的过程放到静态代码块。
声明为final的变量必须在类加载完成时就已经赋值,第一种方式是直接new出来,第二种方式是放到静态块中去new,这个都能完成在类加载时赋值
public class HungrySingleton {
private static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
测试代码
public class Test { //HungrySingleton实现序列化接口 public static void main(String[] args) throws IOException, ClassNotFoundException { HungrySingleton instance = HungrySingleton.getInstance(); //把instance单例对象序列化到一个文件中,再从文件中取出来,这两个对象还会是同一个对象吗 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(instance); File file = new File("singleton_file"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); HungrySingleton newInstance = (HungrySingleton) ois.readObject(); System.out.println(instance); //HungrySingleton@1d44bcfa System.out.println(newInstance); //HungrySingleton@b4c966a System.out.println(instance == newInstance); //false } }
为什么会这样?序列化会通过反射调用无参数的构造方法创建一个新的对象。
具体分析见https://blog.csdn.net/chy6575/article/details/51063505
如何解决?添加readResolve()
方法并返回单例对象
import java.io.Serializable; public class HungrySingleton implements Serializable { private static HungrySingleton hungrySingleton; static{ hungrySingleton = new HungrySingleton(); } private HungrySingleton() { } public static HungrySingleton getInstance() { return hungrySingleton; } //解决序列化和反序列化破坏单例模式 private Object readResolve() { return hungrySingleton; } } //再run得到 //HungrySingleton@1d44bcfa //HungrySingleton@1d44bcfa //true
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射攻击解决方案
Class objectClass = HungrySingleton.class;
//通过反射把构造器权限打开来获取对象
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true); //打开权限
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance); //HungrySingleton@61bbe9ba
System.out.println(newInstance); //HungrySingleton@610455d6
}
}
通过反射打开了权限,private构造器就没用了。
解决方案是在类加载时构造器内进行判断,只有饿汉式和懒汉式的静态内部类方式能使用,这两个是在类加载时就初始化了对象
public class HungrySingleton implements Serializable { private static HungrySingleton hungrySingleton; static{ hungrySingleton = new HungrySingleton(); } //避免反射攻击 private HungrySingleton() { if (hungrySingleton != null) { throw new RuntimeException("单例构造器禁止反射调用"); } } public static HungrySingleton getInstance() { return hungrySingleton; } //解决序列化和反序列化破坏单例模式 private Object readResolve() { return hungrySingleton; } }
那对于不是在类加载时创建对象的情况怎么解决反射攻击呢?
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazySingleton instance = LazySingleton.getInstance();
Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = constructor.newInstance();
System.out.println(instance); //LazySingleton@61bbe9ba
System.out.println(newInstance); //LazySingleton@610455d6
}
}
通过给LazySingleton
的私有构造器添加判断代码,发现两种不同情况
//我们通过给LazySingleton的私有构造器添加判断代码,发现两种不同情况
先执行LazySingleton instance = LazySingleton.getInstance();
后执行LazySingleton newInstance = constructor.newInstance();
//结果返回true;
先执行LazySingleton newInstance = constructor.newInstance();
后执行LazySingleton instance = LazySingleton.getInstance();
//结果返回false;
//这个和cpu分配有关
结论是:对于Lazy这种情况,一旦多线程,反射攻击是无法避免的
多例模式的关键点在于
getInstance()
方法获取实例多例模式必定是有限多例模式,无限多例只要不停new就行了。如果多例模式的上限是1,那就是单例模式了。
多例模式的特点
/** * 多例模式 * 以围棋只有黑白两种棋子为例 */ public class MultiplePattern { //必须要有容器 private static List<Chess> chessList = new ArrayList<>(); private static final Chess white = new Chess("white"); private static final Chess black = new Chess("black"); private static final int maxCount = 2; static { chessList.add(white); chessList.add(black); } //私有构造方法,避免外部创建实例 private MultiplePattern() { } //随机拿取棋子 public static Chess getInstance() { Random random = new Random(); int crt = random.nextInt(maxCount); return chessList.get(crt); } //指定拿取棋子 public static Chess getInstance(int index) { return chessList.get(index); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。