如何创建多线程环境下的可序列化单例模式

本文从以下几个点讲解:

  1. 实现单例的两种方式(预加载&懒加载)
  2. 多线程环境下的单例
  3. 可序列化的单例
  4. 对以上代码的重构
  5. 单例与枚举

1. 实现单例的两种方式

预加载:

public class Foo {
    private static final Foo INSTANCE = new Foo();

    private Foo() {
        if(INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return INSTANCE;
    }
}

懒加载:

class Foo {
    private static Foo INSTANCE = null;

    private Foo() {
        if(INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new Foo();
        }
        return INSTANCE;
    }
}

2. 多线程环境下的单例

上面的代码在单线程的情况下可以满足需要,如果在多线程环境下,则需要进行修改:

class Foo {
    // 请注意volatile关键字
    private static volatile Foo INSTANCE = null;

    private Foo() {
        if(INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        if(INSTANCE == null) {          // check 1
            synchronized(Foo.class) {
                if(INSTANCE == null) {  // check 2
                    INSTANCE = new Foo();
                }
            }
        }
        return INSTANCE;
    }
}

3. 可序列化的单例

解决了多线程环境下的单例,可以进一步的思考如何实现可序列化的单例。反序列化可以不通过构造函数直接生成一个对象,所以反序列化时,我们需要保证其不再创建新的对象。
关于序列化的详细文章:click me

class Foo implements Serializable {
    private static final long serialVersionUID = 1L;

    private static volatile Foo INSTANCE = null;

    private Foo() {
        if(INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        if(INSTANCE == null) {
            synchronized(Foo.class) {
                if(INSTANCE == null) {
                    INSTANCE = new Foo();
                }
            }
        }
        return INSTANCE;
    }

    @SuppressWarnings("unused")
    private Foo readResolve() {
        return INSTANCE;
    }
}

readResolve方法可以保证,即使程序在上一次运行时序列化过此单例,也只会返回全局唯一的单例。

4. 对以上代码重构

public class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    // 使用内部静态class实现懒加载
    private static class FooLoader {
        // 保证在多线程下无差错运行
        private static Foo INSTANCE = new Foo();
    }

    private Foo() {
        throw new UnsupportedOperationException("can not construct this Foo");
    }

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }

    @SuppressWarnings("unused")
    private Foo readResolve() {
        return FooLoader.INSTANCE;
    }
}

5. 单例与枚举

最后提供一种更加简洁的方法:

public enum Foo {
    INSTANCE;
}

08年 google 开发者年会中,Joshua Bloch Joshua Bloch 在 高效 Java 话题中解释了这种方法,视频请戳 这里,在他 演讲的ppt 30-32 页提到:

// 实现单例正确的方式如下:
public enum Elvis {
    INSTANCE;

    private final String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

Effective Java 线上部分 说到:

上述实现单例的方式,其实等同于将 INSTANCE 设置为 public static final 的方式。
不同之处在于,使用枚举的方式显得更为简洁,而且枚举默认提供了序列化机制,也保证了多线程访问的安全
虽然这种单例的实现方式还没有被广泛使用,但是实现单例的最好方式就是使用一个单元素的枚举。

为什么可以这么简洁?

因为 Java 中每一个枚举类型都默认继承了 java.lang.Enum,而 Enum 实现了 Serializable 接口,所以枚举类型对象都是默认可被序列化的。
通过反编译,也可以知道枚举常量本质上是一个pubilc static final xxx

关于序列化的详细文章:click me

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值