懒汉式
public class LazySingleton{
private LazySingleton instance;
private LazySingleton{}//构造方法私有化
public static LazySingleton getInstance{
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
之所以称之为“懒汉式”,是因为在需要该实例的时候才会去进行创建的操作。对于多线程开发,上面的单例模式就不能够保证该类对象是单例了,因为多线程可能会同时进入if语句内,从而都会去创建这个实例。
public class LazySingleton{
private LazySingleton instance;
private LazySingleton(){};
public static synchronized LazySingleton getInstance{
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
我们通过给getInstance方法添加synchronized关键字(也就是加锁),保证该方法一次只能被一个线程调用,从而避免了上述的多线程破坏单例的情况。但是这个方法也存在缺点,就是当instance已经被初始化后再调用时每次都需要去做加锁的操作,非常影响效率。
public class LazySingleton{
private LazySingleton instance;
private LazySingleton(){};
public static LazySingleton getInstance(){
if(instance == null){
synchronized(LazySingleton.class){
instance = new LazySingleton();
}
}
return instance;
}
}
我们直接将锁从方法挪到实例化对象中去了,这样的话当instance不等于空的时候不用处理锁,直接进行返回。但是这种写法仍然会有问题,当多个线程同时进入if语句时,都会排队获取锁并进行实例化对象,从而又破坏了单例模式。那我们应该怎么做呢?
public class LazySingleton{
private LazySingleton instance;
private LazySingleton(){};
public static LazySingleton getInstance(){
if(instance == null){ // 注释1
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();//注释2
}
}
}
return instance;
}
}
我们可以在实例化之前再做一次判断,这样第一次获得所的线程实例化之后,后面获得锁的线程发现instance不为空,直接返回对象。但是这个方式仍然存在弊端,有可能会造成该对象不为空,但是不能被使用的情况。原因主要是因为实例化对象的时候会有以下三个步骤:
- 在堆内存中开辟空间;
- 初始化对象;
- 将该对象指向对应的引用,引用赋值
而这3个步骤的顺序不是固定的,有可能是1-2-3,也有可能是1-3-2,我们称之为指令重排。那试想一下,第一个线程获得锁后,实例化对象的顺序为1-3-2,当刚执行完3还没有来得及执行2的时候,第二个线程执行第一个if语句,会发现instance此时不为空直接返回。但是这时instance还没有被初始化,所以用instance进行操作就会出现问题。那该如何解决呢,答案就是添加volatile关键字禁止这种指令重排,代码如下:
public class LazySingleton{
private volatile LazySingleton instance;
private LazySingleton(){};
public static LazySingleton getInstance(){
if(instance == null){ // 注释1
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();//注释2
}
}
}
return instance;
}
}
饿汉式
public class HungrySingleton{
private static HungrySingleton instance = new HungrySingleton{};
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return instance;
}
}
当调用HungrySingleton类的getInstance方法时,JVM将HungrySingleton类加载到内存中,通过类加载机制确保instance的唯一性。但是饿汉式存在一个问题,就是当类一旦被加载,instance实例就会被创建,会造成不必要的内存开销。
静态内部类
public class InnerClassSingleton{
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){}
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
静态内部类的这个方式相当于是结合了懒汉式和饿汉式两者的优点。通过饿汉式一样的类加载机制实现对象的唯一性,程序只有在调用getInstance方法时才会去加载InnerClassHolder类从而创建实例,避免加载InnerClassSingleton类时就创建对象,造成不必要的内存开销。
反射攻击
上面的饿汉式和静态内部类实现的单例模式碰到反射机制就变得无效了,具体代码如下:
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Test1 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class objClass = InnerClassSingleton.class;
//获取类的构造器
Constructor constructor = objClass.getDeclaredConstructor();
//把构造器私有权限放开
constructor.setAccessible(true);
//正常的获取实例方式
InnerClassSingleton staticInnerClass = InnerClassSingleton.getInstance();
//反射创建实例
InnerClassSingleton newStaticInnerClass = (InnerClassSingleton) constructor.newInstance();
System.out.println(staticInnerClass);
System.out.println(newStaticInnerClass);
System.out.println(staticInnerClass == newStaticInnerClass);
}
}
上面的结果返回的是false,说明出现了两个不同的实例,这就违反了我们的单例原则,不能保证只有一个实例了。那如何解决呢?
public class InnerClassSingleton{
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){
if(InnerClassHolder.instance != null){
throw IllegalStateException();//在这里判断instance是否为空,不为空直接抛出异常
}
}
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;
}
}
对于通过类加载时创建对象实例的这种单例模式,静态内部类、饿汉式,可以通过上面的方式来防止反射攻击。反之如果不是通过类加载时创建实例的方式就没有效果了。举个例子:
public class LazySingleton{
private volatile LazySingleton instance;
private LazySingleton(){
if(instance != null){
throw IllegalStateException();
}
};
public static LazySingleton getInstance(){
if(instance == null){ // 注释1
synchronized(LazySingleton.class){
if(instance == null){
instance = new LazySingleton();//注释2
}
}
}
return instance;
}
}
其实很好理解了,假设我们先通过反射创建对象,走到构造方法的时候instance对象为null,因此会正常创建对象并不会抛出异常。然后通过调用getInstance方法时,instance会重新实例化一次,从而就出现了两个不同的实例了。
其中对象的序列化也能够破坏单例模式,具体代码如下:
public class SerSingleton implements Serializable {
2 private volatile static SerSingleton uniqueInstance;
3 private String content;
4 public String getContent() {
5 return content;
6 }
7
8 public void setContent(String content) {
9 this.content = content;
10 }
11 private SerSingleton() {
12 }
13
14 public static SerSingleton getInstance() {
15 if (uniqueInstance == null) {
16 synchronized (SerSingleton.class) {
17 if (uniqueInstance == null) {
18 uniqueInstance = new SerSingleton();
19 }
20 }
21 }
22 return uniqueInstance;
23 }
24
25
26 public static void main(String[] args) throws IOException, ClassNotFoundException {
27 SerSingleton s = SerSingleton.getInstance();
28 s.setContent("单例序列化");
29 System.out.println("序列化前读取其中的内容:"+s.getContent());
30 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
31 oos.writeObject(s);
32 oos.flush();
33 oos.close();
34
35 FileInputStream fis = new FileInputStream("SerSingleton.obj");
36 ObjectInputStream ois = new ObjectInputStream(fis);
37 SerSingleton s1 = (SerSingleton)ois.readObject();
38 ois.close();
39 System.out.println(s+"\n"+s1);
40 System.out.println("序列化后读取其中的内容:"+s1.getContent());
41 System.out.println("序列化前后两个是否同一个:"+(s==s1));
42 }
43
44 }
得到的结果为false,是两个不同的实例,因此通过序列化对象的方式会破坏单例模式。那有什么办法可以即保证单例、避免反射攻击又能避免序列化呢? 答案就是枚举了,代码如下:
public enum SerEnumSingleton implements Serializable {
2 INSTANCE;
3 private String content;
4 public String getContent() {
5 return content;
6 }
7 public void setContent(String content) {
8 this.content = content;
9 }
10 private SerEnumSingleton() {
11 }
12
13 public static void main(String[] args) throws IOException, ClassNotFoundException {
14 SerEnumSingleton s = SerEnumSingleton.INSTANCE;
15 s.setContent("枚举单例序列化");
16 System.out.println("枚举序列化前读取其中的内容:"+s.getContent());
17 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
18 oos.writeObject(s);
19 oos.flush();
20 oos.close();
21
22 FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
23 ObjectInputStream ois = new ObjectInputStream(fis);
24 SerEnumSingleton s1 = (SerEnumSingleton)ois.readObject();
25 ois.close();
26 System.out.println(s+"\n"+s1);
27 System.out.println("枚举序列化后读取其中的内容:"+s1.getContent());
28 System.out.println("枚举序列化前后两个是否同一个:"+(s==s1));
29 }
30 }
至此,我们常见的单例模式就说完了,包括饿汉式、懒汉式、静态内部类和枚举4种方式。谢谢大家的支持!!!