版权声明:本文来自kid_2412的csdn博客,欢迎转载! https://blog.csdn.net/kid_2412/article/details/53404827
正常情况下构造单例模式主要分为两种,饿汉和懒汉,即懒加载(延迟加载)和直接加载。
饿汉方式如下:
public class Singleton{
private static final Singleton INSTANCE=new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
饿汉的方式是线程安全的,因为在类加载的时候就产生了Singleton的实例对象INSTANCE,初始化的时候已经用私有构造函数防止了new操作。虽然是线程安全的,但是通过反射攻击是可以破坏私有构造函数的。对于如何反射攻击和如何防止反射攻击,说完了懒汉模式再看,因为懒汉模式一样容易出现反射攻击。
懒汉模式:
懒汉模式有多种,当然是线程安全的和非线程的安全写法,当然我们最好还是保证线程安全。
先说线程不安全的:
public class Singleton{
private static Singleton INSTANCE=null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){ //这里是线程不安全的,多个线程同时调用,会创建多个INSTANCE
INSTANCE=new Singleton();
}
}
}
接下来保证线程安全:
public class Singleton{
private static Singleton INSTANCE=null;
private Singleton(){}
public synchronized static Singleton getInstance(){
//加了synchronized线程安全了,但是会同步整个方法里的所有代码,显然有点得不偿失
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
改进synchronized:
public class Singleton{
private static Singleton INSTANCE=null;
private Singleton(){}
public static Singleton getInstance(){
synchronized(INSTANCE){//显然这里判断实例null需要进行竞争,也是得不偿失
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
}
再改进synchronized:
public class Singleton{
private static Singleton INSTANCE=null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){//这种双重校验虽然避免了上面的问题,但是对于-server模式下,会产生指令重排,会导致双重判断失效,依然不同步,于是再改进
synchronized(INSTANCE){
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
}
}
再再改进synchronized:
public class Singleton{
private volatile static Singleton INSTANCE=null; //volatile保证了指令顺序,不会重排
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){ //虽然保证了指令重排,但是这么写法为何感觉这么操蛋?于是再改进
synchronized(INSTANCE){
if(INSTANCE==null){
INSTANCE=new Singleton();
}
}
}
}
}
一个貌似很完善的单例,静态内部类:
public class Singleton{
private Singleton(){}
private static final class SingletonHandler{
private static final Singleton INSTANCE=new Singleton();
}
public static Singleton getInstance(){ //借助了内部类天生的对线程安全的特性创建实例,不用同步锁了,也不用检查实例是否为null了
return SingletonHandler.INSTANCE;
}
}
为何说貌似很完善的单例,因为上面从饿汉到懒汉都可以用反射破坏私有构造函数封装性,然后用构造函数创建对象,如下代码:
public class SingletonTest{
public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor constructor = Singleton.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o = constructor.newInstance();
System.out.println(Singleton.getInstance() == o);//这里会输出false,如下图
}
}
这并不好,我们的单例不再是单例了!
避免被反射攻击的有两种方法,第一种:在私有构造函数里检查标记,像这样:
public class Singleton {
private static boolean flag=false;
private Singleton() {
synchronized (Singleton.class){
if(flag==false){//检查标记,如果为false,但是由于执行了改代码,构造函数肯定被调用了,于是让标记变为true。但是这个方法加锁了!
flag=!flag;
}else{//如果标记为true,证明被调用构造函数,抛出异常
throw new RuntimeException("singleton is bad!");
}
}
}
public synchronized static Singleton getInstance() {
return UserUtilHandler.INSTANCE;
}
private static final class UserUtilHandler {
private static final Singleton INSTANCE = new Singleton();
}
}
再改进一下,就彻底完美了,产生了第二种:使用枚举做单例,枚举天生线程安全,同时不会产生构造函数,代码如下:
//这个世界瞬间清净了许多!
public enum Singleton {
INSTANCE;
public void test(){
System.out.print("i am a very very very safety singleton!");
}
}
来看看枚举的暴力反射:
public class SingletonTest{
public static void main(String args[]) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor constructor = Singleton.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o = constructor.newInstance();
Singleton.INSTANCE.test();//调用实例方法
System.out.println(Singleton.INSTANCE == o);
}
}
可以看到,通过枚举构成的单例是最安全的。但是需要注意的是枚举构造单例在jdk1.5之前是不支持的!那么可以在jdk1.5之后使用枚举方式,1.5之前使用静态内部里+私有构造标记抛出异常的方式。