java单例设计模式总结

说到设计模式,比较常见的是单例设计模式!

单例模式的特点:每一次实例化的对象都是同一个;

单例模式有两种实现方式:1、饿汉式2、懒汉式

饿汉式,就是还没一开始使用便初始化好。

public class Singleton {
	private static final Singleton singleton = new Singleton();
	private Singleton(){}
	public static Singleton getInstance() {
		return singleton;
	}
}
优点:预加载,避免了线程安全问题。

缺点:还没使用便加载好了,占用内存。


如果想等到程序一加载后再初始化,可以使用懒汉式。

public class Singleton {
	private static Singleton singleton = null;
	private Singleton(){}
	public static Singleton getInstance() {
		if (singleton == null) {
			singleton = new Singleton();
		}
		return singleton;
	}
}
优点:懒加载,一开始不占用资源。

缺点:会有线程安全问题。


考虑到线程安全问题,可以使用synchronized关键字修饰。

public class Singleton {
	private static Singleton singleton = null;
	private Singleton(){}
	public synchronized static Singleton getInstance() {
		if (singleton == null) {
			singleton = new Singleton();
		}
		return singleton;
	}
}
这样就解决了线程安全问题,但是由于同步的原因,性能不高,可以做如下优化

public class Singleton {
	private static Singleton singleton = null;
	private Singleton(){}
	public static Singleton getInstance() {
		if (singleton == null) {
			synchronized (Singleton.class) {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}
这样从代码的从代码的角度上看就问题,但是从JVM上分析,创建一个对象需要申请分配一块内存初始化构造函数;将指针指向分配的内存。这两个操作的先后顺序,JVM并没有规定好,特别是java编译后会将字节码进行重排序。所以可能会出现这样的情况:线程A先进来创建对象,将指针指向分配的内存,但是未完成初始化完成,线程B进来的时候,由于指针有引用singleton!=null,会直接未完成初始化好的对象,则会出现异常。

为了解决如上问题,可以考虑使用一个临时变量引用,来确保先初始化好再将指针指向内存的顺序,代码如下

public class Singleton {
	private static Singleton singleton = null;
	private Singleton(){}
	public static Singleton getInstance() {
		if (singleton == null) {
			Singleton temp;
			synchronized (Singleton.class) {
				temp = singleton;
				if (temp == null) {
					synchronized(Singleton.class) {
						if (temp == null) {
							temp = new Singleton();
						}
					}
					singleton = temp;
				}
			}
		}
		return singleton;
	}
}
问题其实还没完成解决,因为JVM编译优化可能将改变代码的顺序,如singleto = temp;放到同步代码块里面,所以需要重新做处理。

针对以上问题,终极解决方案有两种:

1、使用volatile

在JDK1.5的时候,出现volatile关键字,在并发的时候,当线程A的数据发生变化时,会通知其他线程修改数据这样能有效的避免JVM编译导致的重排序问题,即使出现线程A还没初始化完对象,线程B就进来的情况,也会通过volatile让线程A初始化好后通知线程B变更数据,代码如下:

public class Singleton {
	private volatile static Singleton singleton = null;
	private Singleton(){}
	public static Singleton getInstance() {
		if (singleton == null) {
			synchronized (Singleton.class) {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}
}

2、使用静态匿名内部类

public class Singleton {
	private Singleton(){}
	private static class SingletonInstance {
		private static final Singleton singleton = new Singleton();
	}
	public static Singleton getInstance() {
		return SingletonInstance.singleton;
	}
}
由于代码没有static的属性,所以当Singleton被加载的时,内部类不会初始化,只有当getInstance()被调用时才会加载SingletonInstance,由于使用类加载的形式,并用final修饰,初始化对象的时候具有原子性,所以不要考虑线程安全,所以不需要加锁。

备注:通过反射我们可以强制生成多个对象,这些极端现象我们不做考虑,除了这样的方式,我们还可以使用序列化的方式强制生成多个对象,代码如下:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Singleton implements Serializable {
	private static final Singleton singleton = new Singleton();
	private Singleton(){}
	public static Singleton getInstance() {
		return singleton;
	}
	private Object readResolve(){
		return singleton;
	}
	public static void main(String[] args) throws Exception {
		Singleton s1 = null;
		Singleton s2 = getInstance();
		FileOutputStream fos = new FileOutputStream("d:/aa.txt");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(s2);
		oos.flush();
		oos.close();
		FileInputStream fis = new FileInputStream("d:/aa.txt");
		ObjectInputStream ois = new ObjectInputStream(fis);
		s1 = (Singleton) ois.readObject();
		System.out.println(s1.hashCode() == s2.hashCode());
	}
}

 上面的代码页面打印的结果是true,但是注释掉readResolve函数的时候,打印的结果会是false。 
  


大家看完还有什么不懂或是觉得代码哪里有问题,欢迎留言交流。



你可能感兴趣的:(java)