本文从以下几个点讲解:
- 实现单例的两种方式(预加载&懒加载)
- 多线程环境下的单例
- 可序列化的单例
- 对以上代码的重构
- 单例与枚举
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