单例模式
单例模式要求类能够有返回对象一个引用(同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)
单例的实现主要是通过以下步骤:
(1)将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
(2)在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。
(3)定义一个静态方法返回这个唯一对象。
注意事项
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
饿汉式
public class Singleton{
private final static Singleton singleton = new Singleton();//在类内部创建实例对象
private Singleton(){} //构造方法私有
public static Singleton getInstance(){//对外提供一个获得该实例对象的公共接口
return singleton;
}
}
由于饿汉模式是不管程序是否需要该实例对象,都会进行创建,比较浪费资源。由此提出了懒汉模式。即就是:当程序中需要使用该实例对象时,才进行创建该实例对象。但此过程会涉及到多线程并发问题。
懒汉式
public class Singleton {
//声明一个全局唯一的实例对象,并未直接创建实例对象
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这种写法起到了懒加载的效果,但只能在单线程下使用。如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
懒汉式(线程安全,同步方法)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对 getInstance() 方法进行了线程同步。
缺点:同步效率低,每个线程在想获得类的实例时候,执行getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。
懒汉式双检锁
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要 Singleton 类被装载就会实例化,没有懒加载的作用,而静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
缺点:这样还不行,因为
singleton = new Singleton();//不是一个原子操作
/*
1.分配内存空间
2.执行构造方法
3.把这个对象指向这个空间
JVM存在指令重排
123
132 此时singleton还没有完成构造
*/
再加入volatile关键字,防止指令重排就可以
public class Singleton {
private static volatile Singleton singleton;//防止指令重排
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
懒汉式三重检查
public class LazyMan {
//设置标志位,防止反射通过无参构造新建对象
private static boolean flag=false;
private LazyMan(){
synchronized (LazyMan.class){
if(flag==false){
flag=true;
} else {
throw new RuntimeException("不要试图用反射破解单例");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
//如果获取了该标志位的属性,还是可以通过反射篡改该private属性
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true); //篡改属性
Constructor<LazyMan> declaredConstructor= LazyMan.class.getDeclaredConstructor(); //获取无参构造
declaredConstructor.setAccessible(true);
LazyMan l1 = declaredConstructor.newInstance(); // 通过newInstance方法新建对象
flag.set(l1,false);
System.out.println(l1.hashCode()); //后台输出该对象的哈希值
LazyMan l2 = LazyMan.getInstance(); //通过传统的方式创建,不通过反射创建
System.out.println(l2.hashCode()); //后台输出正常创建对象的哈希值,输出结果为两者不一致
}
}
这种方式可以避免通过反获取该对象的无参构造函数,但是,只需要获取设置标志位的字段名,通过getDeclaredField方法就可以获得该字段,通过setAccessible方法就可以篡改该字段属性,一样来拿无参数构造就行,无视标志位,直接创建新对象,此时发生单例模式对象并不单一。