虽然写了很多年代码,但是说真的对设计模式不是很熟练,虽然平时也会用到一些,但是都没有深入研究过,所以趁现在有空练下手
这章主要讲单例模式,也是最简单的一种模式,但是因为spring中bean的广泛应用,所以现在单例模式在应用中其实很少会手动实现
单例模式是一种常见的设计模式,适用于以下情况:
首先是最简单实用的饿汉模式
直接上代码(建议使用)
public class EagerSingleton {
// 在类加载时就创建实例,并初始化为静态变量
private static final EagerSingleton instance = new EagerSingleton();
// 私有化构造方法,防止外部实例化
private EagerSingleton() {}
// 获取单例实例的方法
public static EagerSingleton getInstance() {
try {
// 模拟处理业务逻辑耗时
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return instance;
}
public static void main(String[] args) {
for (int i= 0; i < 100; i++) {
new Thread(() -> {
System.out.println(EagerSingleton.getInstance().hashCode());
}).start();
}
}
}
执行一下
可以看到hashCode都是一样的,这里有人可能会说hashCode一样并不代表对象一样,我只能说你的确是对的,但这不在本章讲解范围内
然后我们再来看一下懒汉模式
直接上代码(不建议使用)
public class LazySingleton {
private static LazySingleton instance;
// 私有化构造方法,防止外部实例化
private LazySingleton() {}
// 获取单例实例的方法
public static LazySingleton getInstance() {
// 在第一次调用时才创建实例
if (instance == null) {
try {
// 模拟处理业务逻辑耗时
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new LazySingleton();
}
return instance;
}
public static void main(String[] args) {
for (int i= 0; i < 100; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance().hashCode());
}).start();
}
}
}
执行一下
发现没有,hashCode都不一样,虽然hashCode相同不能证明对象是同一个,但是hashCode不相同肯定不是同一个对象,这说明其实是线程不安全的,因此这种写法其实是被淘汰了的
上面的那种写法虽然不推荐使用,但是提供了一种思路,就是只在需要的时候才加载,其主要目的还是为了节省资源(现在的硬件其实都很强大,这点资源省不省问题其实不大,这也让我想起了很多年前我刚入行还在写C++,当时问我师父说这个指针要是忘了释放怎么办,他跟我说没关系的,现在电脑都很牛逼,这点资源浪费根本影响不了什么)
好了下面我们来完善一下懒汉模式,最简单的方法就是使用synchronized关键字来保证线程的安全,当然同时也就伴随着性能的损耗(不推荐使用)
这里直接用synchronized关键字锁整个方法
public class LazySingleton {
// 注意:volatile关键字是必须的,防止指令重排序
private static volatile LazySingleton instance;
// 私有化构造方法,防止外部实例化
private LazySingleton() {}
// 获取单例实例的方法
public static synchronized LazySingleton getInstance() {
// 在第一次调用时才创建实例
if (instance == null) {
try {
// 模拟处理业务逻辑耗时
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new LazySingleton();
}
return instance;
}
public static void main(String[] args) {
for (int i= 0; i < 100; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance().hashCode());
}).start();
}
}
}
执行一下
上面那个示例虽然保证了一个实例,但是性能上还是不如意,如是再优化一下就出现了下面这种(不是很推荐,因为看起来很复杂)
这里只在需要的地方加synchronized,就不再锁整个方法,性能上提升了一丢丢(注意这里其实是双重判断的懒汉模式,还有一个只有一层判断,因为和一开始的那个一样存在线程安全问题这里不做展示)
public class LazySingleton {
// 注意:volatile关键字是必须的,防止指令重排序
private static volatile LazySingleton instance;
// 私有化构造方法,防止外部实例化
private LazySingleton() {
}
// 获取单例实例的方法
public static LazySingleton getInstance() {
// 在第一次调用时才创建实例
if (instance == null) {
synchronized (LazySingleton.class) {
// 注意这里使用的是双重判断,防止多线程并发时重复创建实例
// 如果不加下面这个判断,多线程并发时,可能会创建多个实例
if (instance == null) {
try {
// 模拟处理业务逻辑耗时
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
instance = new LazySingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(LazySingleton.getInstance().hashCode());
}).start();
}
}
}
通过比较可以发现,饿汉模式是不管需不需要都会创建一个实例,有点浪费资源,然后在程序启动的时候会拖慢一点速度。懒汉模式虽然是在需要的时候才创建实例,但是因为使用了synchronized关键字,所以在使用的时候也会有性能问题。虽然问题都不大,但是有些完美主义可能就接受不了,所以下面我们再优化一下。
直接上代码(建议使用,目前来看应该是最完美的实现方式,唯一的缺点就是不能反序列化)
由于静态内部类只有在被使用的时候才会被加载,所以单例实例的创建会延迟到 getInstance() 方法被调用的时候。而且由于类加载过程是线程安全的,所以这种方式也是线程安全的。
public class Singleton {
// 私有化构造方法,防止外部实例化
private Singleton() {}
// 静态内部类持有单例实例
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 获取单例实例的方法
public static Singleton getInstance() {
try {
// 模拟处理业务逻辑耗时
Thread.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return SingletonHolder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton.getInstance().hashCode());
}).start();
}
}
}
执行一下
那能不能写一个更完美的,让它能反序列化呢?答案当然是可以的!
直接上代码(虽然看起来很牛逼,用起来也很牛逼,但是不建议使用,违背Java代码设计原则)
public enum EnumSingleton {
INSTANCE;
// 注意枚举不是类没有构造方法
// 这里可以用来处理业务逻辑
public void doSomething() {
System.out.println("do something");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(EnumSingleton.INSTANCE.hashCode());
}).start();
}
}
}
执行一下
还有些其它的方式就不讲了,这几种基本就是最常见的,大家根据实际业务情况自行选择就好