- 定义:保证一个类仅有一个实例,并提供一个全局访问点
适用场景:
- 想确保任何情况下只有一个实例
优点:
- 在内存里只有一个实例,减少了内存开销
- 可以避免对资源的多重占用
- 设置了全局访问点,严格控制访问
缺点:
- 扩展困难
重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列号和反序列化安全
- 反射攻击
示例代码:
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton==null){
lazySingleton=new LazySingleton();
}
return lazySingleton;
}
}
存在问题:线程安全,如果存在多线程使用这个单例的话,可能重复创建。多线程测试代码
public class TestThread implements Runnable{
public void run() {
LazySingleton lazySingleton=LazySingleton.getInstance();
System.out.println(Thread.currentThread()+""+lazySingleton);
}
}
Test类
public class Test {
public static void main(String[] args) {
Thread t1=new Thread(new TestThread());
Thread t2=new Thread(new TestThread());
t1.start();
t2.start();
System.out.println("end.........");
}
}
运用多线程Debug模式 控制线程的执行,可复现出创建两个实例的情况,比如线程1执行到了laySigleton=new LazySingleton()时,如果此时控制Thread2执行,2执行到if判断时,判断为空,此时也会执行new的代码,从而造成了多个实例。
解决方案一:同步锁,在getInstance上加上synchronized关键字,在static方法上加上关键字相当于锁住了整个class。
public synchronized static LazySingleton getInstance(){
if(lazySingleton==null){
lazySingleton=new LazySingleton();
}
return lazySingleton;
}
但是同步锁有加锁和解锁的开销,比较消耗性能。是否有更演进的方法呢
解决方案二:双重检查
首先想到可以给 lazySingleton=new LazySingleton();这句加锁,演变为
public class DoubleCheckLazySingleton {
private static DoubleCheckLazySingleton doubleCheckLazySingleton;
private DoubleCheckLazySingleton(){
}
public static DoubleCheckLazySingleton getInstance(){
if(doubleCheckLazySingleton==null){
synchronized(DoubleCheckLazySingleton.class){
doubleCheckLazySingleton=new DoubleCheckLazySingleton();
}
}
return doubleCheckLazySingleton;
}
}
然而仅仅这样是不行的,仍存在线程1走到doubleCheckLazySingleton=new DoubleCheckLazySingleton();这句的时候,线程2对if判断为true,然后走到synchronized,等线程1完成创建,释放锁后,线程2此时获取锁,也会去new实例,所以需要添加双重判断,实现如下
public class DoubleCheckLazySingleton {
private static DoubleCheckLazySingleton doubleCheckLazySingleton;
private DoubleCheckLazySingleton(){
}
public static DoubleCheckLazySingleton getInstance(){
if(doubleCheckLazySingleton==null){
synchronized(DoubleCheckLazySingleton.class){
if(doubleCheckLazySingleton==null){
doubleCheckLazySingleton=new DoubleCheckLazySingleton();
}
}
}
return doubleCheckLazySingleton;
}
}
理论上是木有问题的但是因为由于JVM为了使得处理器内部的运算单元能充分利用,处理器可能会对输入代码进行乱序执行(Out Of Order Execute)优化,处理器会在计算之后将乱序执行的结果进行重组,保证该结果与顺序执行的结果是一样的,但并不保证程序中各个语句计算的先后顺序与输入的代码顺序一致。所以我们可以使用volatile来阻止程序的乱序执行,因为volatile会给程序添加内存屏障从而阻止编译器或者处理器对代码的乱序执行,从而使双重检验锁在多线程下具有正确执行。
public class DoubleCheckLazySingleton {
private volatile static DoubleCheckLazySingleton doubleCheckLazySingleton;
private DoubleCheckLazySingleton(){
}
public static DoubleCheckLazySingleton getInstance(){
if(doubleCheckLazySingleton==null){
synchronized(DoubleCheckLazySingleton.class){
if(doubleCheckLazySingleton==null){
doubleCheckLazySingleton=new DoubleCheckLazySingleton();
}
}
}
return doubleCheckLazySingleton;
}
}
解决方案三:通过静态内部类来解决
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){
}
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton=new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}
在getInstance是的时候都是拿的静态的staticInnerClassSingleton,那么如何保证new StaticInnerClassSingleton()这句是线程安全的呢。在《深入理解JAVA虚拟机》有写到
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
以上都是单例模式的懒汉模式
下面看饿汉模式,饿汉模式在类的初始化的时候就构建好了对象,不存在线程安全问题。但是在不使用的时候,同样耗内存
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton=new HungrySingleton();
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private HungrySingleton(){
}
}
序列化破坏单例模式原理解析及解决方案
示例代码
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Thread t1=new Thread(new TestThread());
// Thread t2=new Thread(new TestThread());
// t1.start();
// t2.start();
// System.out.println("end.........");
HungrySingleton hungrySingleton=HungrySingleton.getInstance();
File file=new File("singlton");
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(hungrySingleton);
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(file));
HungrySingleton newHungrySingleton= (HungrySingleton) objectInputStream.readObject();
System.out.println(hungrySingleton);
System.out.println(newHungrySingleton);
System.out.println(newHungrySingleton==hungrySingleton);
}
}
以上代码的输出不是同一个对象,也就是说破坏了单例模式,通过序列化和反序列化方法。如何避免这种情况,饿汉模式改造如下,readResolve方法能达到返回的对象是同一个对象,但是实际在代码中还是被创建出了新的对象。
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton=new HungrySingleton();
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private HungrySingleton(){
}
private Object readResolve(){
return hungrySingleton;
}
}
反射破坏单例模式原理解析及解决方案
示例代码:可以看到输出的不是同一个对象,也就是实现了利用反射破坏了单例模式
Class objectClass=HungrySingleton.class;
Constructor constructor=objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton hungrySingleton=HungrySingleton.getInstance();
HungrySingleton newHungrySingleton= (HungrySingleton) constructor.newInstance();
System.out.println(hungrySingleton);
System.out.println(newHungrySingleton);
System.out.println(newHungrySingleton==hungrySingleton);
改造方案
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton=new HungrySingleton();
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private HungrySingleton(){
if(hungrySingleton!=null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
private Object readResolve(){
return hungrySingleton;
}
}
那么懒汉模式可以禁止这样反射调用吗?是做不到的,因为懒汉并没有在类初始化的时候构造,所以不为空的逻辑加到懒汉模式中,也不能阻止对单例的破坏。如下代码,依然会输出两个不同的对象
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton(){
if(lazySingleton!=null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton==null){
lazySingleton=new LazySingleton();
}
return lazySingleton;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class objectClass=LazySingleton.class;
Constructor constructor=objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newLazySingleton= (LazySingleton) constructor.newInstance();
LazySingleton lazySingleton=LazySingleton.getInstance();
System.out.println(lazySingleton);
System.out.println(newLazySingleton);
System.out.println(newLazySingleton==lazySingleton);
}
}
所以懒汉模式是防止不了反射攻击的