单例模式
单例模式指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
单例模式有 3 个特点:
单例类只有一个实例对象;
该单例对象必须由单例类自行创建;
单例类对外提供一个访问该单例的全局访问点;
优点:
内存中只有一个实例,减少了内存开销;
可以避免对资源的多重占用;
设置全局访问点,严格控制访问。
缺点:
没有接口,拓展困难。
单例模式的主要角色有:
单例类:包含一个实例且能自行创建这个实例的类。
访问类:使用单例的类。
1.懒汉模式
懒汉模式下的单例写法是最简单的,但它是线程不安全的:
public class LazyMode {
//定义一个静态实例
private static LazyMode lazyMode=null;
public LazyMode() {
}
public static LazyMode getInstance(){
if(lazyMode==null){
lazyMode=new LazyMode();
}
return lazyMode;
}
}
2.同步锁单例模式
加上一个同步锁解决下线程安全问题:
//同步锁单例模式,加同步锁,解决线程安全
public class LazyMode2 {
//定义一个静态实例
private static LazyMode2 lazyMode=null;
public LazyMode2() {
}
public static LazyMode2 getInstance(){
synchronized (LazyMode2.class){//加上同步锁
if(lazyMode==null){
lazyMode=new LazyMode2();
}
}
return lazyMode;
}
}
但是这个同步锁锁的是整个类,比较消耗资源,并且即使运行内存中已经存在LazyMode2,调用其getInstance还是会上锁。
3.双重同步锁单例模式
我们再进行优化一下,看下面的例子,在同步锁上再加上一层判断,变成双重同步锁,当LazyMode3 实例创建好后,后续再调用其getInstance方法不会上锁。
要注意的是虽然弄了双重同步锁,但它可能还是线程不安全的。虽然不会出现多次初始化LazyMode3 实例的情况,但是由于指令重排的原因,某些线程可能会获取到空对象,后续对该对象的操作将触发空指针异常。要修复这个问题,只需要阻止指令重排即可,所以可以给LazyMode3 属性加上volatile关键字,来确保线程安全。
volatile关键字修饰的成员变量具有两大特性:保证了该成员变量在不同线程之间的可见性;禁止对该成员变量进行重排序,也就保证了其有序性。但是volatile修饰的成员变量并不具有原子性,在并发下对它的修改是线程不安全的
//懒汉模式,加上双重同步锁
public class LazyMode3 {
//定义一个静态实例
private volatile static LazyMode3 lazyMode=null;//加上volatile关键字阻止指令重排,通过volatile修饰的成员变量 会添加内存屏障来阻止JVM进行指令重排优化。
public LazyMode3() {
}
public static LazyMode3 getInstance(){
if(lazyMode==null) {//再加上一层判断,防止多次加锁
synchronized (LazyMode3.class) {//加上同步锁
if (lazyMode == null) {
lazyMode = new LazyMode3();
}
}
}
return lazyMode;
}
}
4.静态内部类单例模式
JVM在类的初始化阶段会加Class对象初始化同步锁,同步多个线程对该类的初始化操作;
静态内部类InnerClass的静态成员变量lazyStaticMode在方法区中只会有一个实例。
在Java规范中,当以下这些情况首次发生时,class类将会立刻被初始化:
1.class类型实例被创建;
2.class类中声明的静态方法被调用;
3.class类中的静态成员变量被赋值;
4.class类中的静态成员被使用(非常量);
//静态内部类单例模式
public class LazyStaticMode {
public LazyStaticMode() {
}
public static class InnerClass{//在内部类中定义一个静态实例
private static LazyStaticMode lazyStaticMode=new LazyStaticMode();
}
public static LazyStaticMode getInstance(){
return InnerClass.lazyStaticMode;//通过内部类去获取实例
}
}
5.饿汉单例模式
该模式是在类加载的时候就初始化:
//饿汉单例模式
public class HungaryMode {
private static HungaryMode hungaryMode=new HungaryMode();//直接创建实例
public HungaryMode(){
}
public static HungaryMode getInstance(){
return hungaryMode;
}
}
这种模式在类加载的时候就完成了初始化,所以并不存在线程安全性问题;但由于不是懒加载,饿汉模式不管需不需要用到实例都要去创建实例,如果创建了不使用,则会造成内存浪费。
而且这个模式容易被序列化和反射破坏,下面做个序列化和反射的破坏例子
6.序列化破坏单例模式
1.让前面的饿汉单例模式,,实现序列化接口
//序列化破坏单例模式,实现序列化接口
public class HungaryMode2 implements Serializable{
private static final long serialVersionUID = -9086351862017233122L;
private static HungaryMode2 hungaryMode=new HungaryMode2();
public HungaryMode2(){
}
public static HungaryMode2 getInstance(){
return hungaryMode;
}
}
2.测试输出
public class Test {
public static void main(String[]args) throws IOException, ClassNotFoundException {
HungaryMode2 hungaryMode2=HungaryMode2.getInstance();
//对象输出
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
outputStream.writeObject(hungaryMode2);
//对象输入
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
HungaryMode2 hungaryMode3 = (HungaryMode2) inputStream.readObject();
System.out.println(hungaryMode2);
System.out.println(hungaryMode3);
System.out.println(hungaryMode2==hungaryMode3);
}
}
//com.wfg.design.mode5.HungaryMode2@4a574795
//com.wfg.design.mode5.HungaryMode2@f6f4d33
//false
从输出可以看到,即使它是单例模式,也成功创建出了两个不一样的实例,单例遭到了破坏。
要让反序列化后的对象和序列化前的对象是同一个对象的话,可以在HungaryMode2 里加上readResolve方法:
这种方式反序列化过程内部还是会重新创建HungaryMode2 实例,但是因为HungaryMode2 类定义了readResolve方法(方法内部返回了hungaryMode对象引用),反序列化过程中会判断目标类是否定义了readResolve该方法,是的话则会通过反射调用该方法,所以最终获取到的还是HungaryMode2
//序列化破坏单例模式,实现序列化接口
public class HungaryMode2 implements Serializable{
private static final long serialVersionUID = -9086351862017233122L;
private static HungaryMode2 hungaryMode=new HungaryMode2();
public HungaryMode2(){
}
public static HungaryMode2 getInstance(){
return hungaryMode;
}
public Object readResolve(){//添加上readResolve方法
return hungaryMode;
}
}
测试输出
public class Test {
public static void main(String[]args) throws IOException, ClassNotFoundException {
HungaryMode2 hungaryMode2=HungaryMode2.getInstance();
//对象输出
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
outputStream.writeObject(hungaryMode2);
//对象输入
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
HungaryMode2 hungaryMode3 = (HungaryMode2) inputStream.readObject();
System.out.println(hungaryMode2);
System.out.println(hungaryMode3);
System.out.println(hungaryMode2==hungaryMode3);
}
}
//com.wfg.design.mode5.HungaryMode2@4a574795
//com.wfg.design.mode5.HungaryMode2@4a574795
//true
7.反射破坏单例模式
public static void main(String[]args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
HungaryMode2 hungaryMode2=HungaryMode2.getInstance();
//通过反射创建了HungaryMode2实例
Class<HungaryMode2> cs=HungaryMode2.class;
//获取HungaryMode2里的构造
Constructor<HungaryMode2> ct=cs.getConstructor();
//打开构造权限
ct.setAccessible(true);
HungaryMode2 hungaryMode3=ct.newInstance();
System.out.println(hungaryMode2);
System.out.println(hungaryMode3);
System.out.println(hungaryMode2==hungaryMode3);
}
//com.wfg.design.mode5.HungaryMode2@27716f4
//com.wfg.design.mode5.HungaryMode2@8efb846
//false
从上面输出可以看到,可以通过反射破坏了私有构造器权限,成功创建了新的实例。
对于这种情况,饿汉模式下的例子可以在构造器中添加判断逻辑来防御(懒汉模式的就没有办法了)
//序列化破坏单例模式,实现序列化接口
public class HungaryMode2 implements Serializable{
private static final long serialVersionUID = -9086351862017233122L;
private static HungaryMode2 hungaryMode=new HungaryMode2();
public HungaryMode2(){
if(hungaryMode==null){//通过在这里加上判断来阻止反射破坏
throw new RuntimeException("不允许通过这个构造器来生成实例");
}
}
public static HungaryMode2 getInstance(){
return hungaryMode;
}
public Object readResolve(){
return hungaryMode;
}
}
再次模拟反射,发现抛出我们的自己定义的错误,阻止反射破坏实例
Exception in thread "main" java.lang.ExceptionInInitializerError
at com.wfg.design.mode5.Test.main(Test.java:23)
Caused by: java.lang.RuntimeException: 不允许通过这个构造器来生成实例
at com.wfg.design.mode5.HungaryMode2.<init>(HungaryMode2.java:11)
at com.wfg.design.mode5.HungaryMode2.<clinit>(HungaryMode2.java:8)
... 1 more
8.枚举单例模式
枚举单例模式是推荐的单例模式,它不仅可以防御序列化破坏,也可以防御反射破坏
1.定义一个枚举类,里面设置想要的实例对象
public enum EnumMode {
ENUMMODE;
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public static EnumMode getInstance(){
return ENUMMODE;
}
}
2.测试是否是单例的
public static void main(String[]args) {
EnumMode enumMode=EnumMode.getInstance();
enumMode.setObj(new Object());
EnumMode enumMode2=EnumMode.getInstance();
System.out.println(enumMode.getObj());
System.out.println(enumMode2.getObj());
System.out.println(enumMode==enumMode2);
}
//java.lang.Object@27716f4
//java.lang.Object@27716f4
//true
从输出上看到,是单例的,还是同一个对象
3.测试序列化破坏
//序列化破坏
public static void main(String[]args) throws IOException, ClassNotFoundException {
EnumMode enumMode=EnumMode.getInstance();
enumMode.setObj(new Object());
//对象输出
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
outputStream.writeObject(enumMode);
//对象输入
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("file"));
EnumMode enumMode2 = (EnumMode) inputStream.readObject();
System.out.println(enumMode.getObj());
System.out.println(enumMode2.getObj());
System.out.println(enumMode==enumMode2);
}
//java.lang.Object@4a574795
//java.lang.Object@4a574795
//true
从输出上看到,创建的实例对象,也没有被序列化破坏,还是同一个
//反射破坏
public static void main(String[]args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumMode enumMode=EnumMode.getInstance();
enumMode.setObj(new Object());
//通过反射创建了HungaryMode2实例
Class<EnumMode> cs=EnumMode.class;
// 枚举类只包含一个(String,int)类型构造器
Constructor<EnumMode> ct=cs.getDeclaredConstructor(String.class,int.class);
//打开构造权限
ct.setAccessible(true);
EnumMode enumMode2=ct.newInstance("aa",1);
System.out.println(enumMode);
System.out.println(enumMode2);
System.out.println(enumMode==enumMode2);
}
抛出异常
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.wfg.design.mode5.Test2.main(Test2.java:42)
由于Java禁止通过反射创建枚举对象,抛出了错误。正是因为枚举类型拥有这些先天的优势,所以用它创建单例也是不错的选择,
下一篇讲讲建造者模式