好久没继续了…
单例模式
单例模式是一种常用的软件设计模式,属于创建模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。1
入门版
我们先看下最接地气的版本:
/**
* 单例模式1,超简单,饿汉
*/
public class Singleton1 {
private static final Logger logger = LoggerFactory.getLogger(Singleton1.class);
private static final Singleton1 INTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INTANCE;
}
public static void main(String[] args) {
LocalDateTime begin = LocalDateTime.now();
int count = 1000;
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch lock = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
Thread thread = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton1 instance = Singleton1.getInstance();
logger.info("{}", instance.hashCode());
lock.countDown();
}, "" + i);
thread.start();
}
countDownLatch.countDown();
try {
lock.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("全部单例获取完毕,耗时:[{}]", Duration.between(begin, LocalDateTime.now()));
}
}
上面的代码中通过private static final Singleton1 INTANCE = new Singleton1();
就实现了单例的功能。这是利用了jvm中一个类只会被加载一次的特性。通过上面的main方法测试单例的效果会发现打印出来的单例对象的hash都是一样的。
其实这个也算是相当好的版本了,但是,有那么一丢丢的逼格不足。作为一个讲逼格的程序员,怎么能只满足到这里呢。
双重校验版
这个算是写起来比较复杂的版本:
/**
* 单例模式2,双重校验版本
*/
public class Singleton2 {
private static final Logger logger = LoggerFactory.getLogger(Singleton2.class);
private static volatile Singleton2 INTANCE;
private Singleton2() {
}
public static Singleton2 getInstance() {
if (INTANCE == null) {
synchronized (Singleton2.class) {
if (INTANCE == null) {
INTANCE = new Singleton2();
}
}
}
return INTANCE;
}
public static void main(String[] args) {
LocalDateTime begin = LocalDateTime.now();
int count = 1000;
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch lock = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
Thread thread = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton2 instance = Singleton2.getInstance();
logger.info("{}", instance.hashCode());
lock.countDown();
}, "" + i);
thread.start();
}
countDownLatch.countDown();
try {
lock.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("全部单例获取完毕,耗时:[{}]", Duration.between(begin, LocalDateTime.now()));
}
}
在这个版本中,使用synchronized+volatile保证在多线程情况下依旧只有一个单例。怎么说呢,属于费力不讨好的写法吧,但是对于学习多线程还是挺有帮助的
holder写法
这个写法是个人比较喜欢的一种写法,有逼格但又不复杂
/**
* 单例模式3,holder
*/
public class Singleton3 {
private static final Logger logger = LoggerFactory.getLogger(Singleton3.class);
private Singleton3() {
}
public static Singleton3 getInstance() {
return Singleton3Holder.INTANCE;
}
public static void main(String[] args) {
LocalDateTime begin = LocalDateTime.now();
int count = 1000;
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch lock = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
Thread thread = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton3 instance = Singleton3.getInstance();
logger.info("{}", instance.hashCode());
lock.countDown();
}, "" + i);
thread.start();
}
countDownLatch.countDown();
try {
lock.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("全部单例获取完毕,耗时:[{}]", Duration.between(begin, LocalDateTime.now()));
}
private static class Singleton3Holder {
private static final Singleton3 INTANCE = new Singleton3();
}
}
在holder的写法中,由于静态内部类的加载不依赖外部类,所以当Singleton3被加载时,Singleton3Holder还没有被加载,所以只要不调用Singleton3.getInstance()方法,Singleton3Holder就不会被加载,Singleton3的实例也就不会被创建,堪称典范。
神级写法
使用枚举来实现单例。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
/**
* 单例模式4,枚举型
*/
public enum Singleton4 {
/**
* i'm Singleton
*/
INTANCE;
private static final Logger logger = LoggerFactory.getLogger(Singleton4.class);
public static void main(String[] args) {
LocalDateTime begin = LocalDateTime.now();
int count = 1000;
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch lock = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
Thread thread = new Thread(() -> {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton4 instance = Singleton4.INTANCE;
logger.info("{}", instance.hashCode());
lock.countDown();
}, "" + i);
thread.start();
}
countDownLatch.countDown();
try {
lock.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("全部单例获取完毕,耗时:[{}]", Duration.between(begin, LocalDateTime.now()));
}
}
总结
不管哪种写法,第一步都得是保证构造器私有,不然的话,就没法单例了。真要说绝对单例,那只有通过枚举实现的单例能实现绝对单例。因为枚举类是没有构造方法的,别人就算通过映射的方式,也无法创建第二个实例。除了枚举这种方式之外的所有方式都可以通过反射的方式被人创建多例。但是,谁会这么欠儿欠儿的呢…
效率评测
剧透一下:效率上基本没区别,大家可以放心使用,详情见:压力测试-Benchmark_wx的博客-优快云博客