引子:
相信大部分人对设计模式都不陌生,基本大大小小的面试都会问到 24 种设计模式,有很多人连其中一种都说不出来,想要了解掌握所有的设计模式固然困难,但是我们掌握其中几种常用的设计模式并非难事。设计模式不仅可以提高我们代码的质量,还可以帮助我们更容易的去阅读一些源码实现。
正文:
我们从 24 种设计模式中相对简单且常用的单例模式开始介绍,单例模式需要保证一个类只能被新建一次,不管多少个地方调用,要保证使用的都是同一个对象,并且这个类不能被外部类以 new 形式创建。
单例模式还分为饿汉式、懒汉式的创建方式,下面是一个简单的懒汉式单例模式实现:
public class SingletonFactory {
private static SingletonFactory singletonFactory;
/* 单例模式有⼀个特点就是不允许外部直接创建,也就是 new SingletonFactory() ,因此这⾥在默认的构造函数上添加了私有属性 private. */
private SingletonFactory() {
}
public static SingletonFactory getInstance() {
if(singletonFactory == null) {
singletonFactory = new SingletonFactory();
}
return singletonFactory;
}
}
public static void main(String[] args) {
SingletonFactory singletonFactory = SingletonFactory.getInstance();
System.out.println(singletonFactory); // 输出:com.fjc.demo.singleton.SingletonFactory@2f704a96
}
上面的实现属于懒汉式,在最开始加载的时候并不会创建这个类,调用 getInstance()方法并且该类为null的时候再进行创建;构造方法的修饰符之所以使用private是为了防止该类被new的形式创建。
之所以说这个属于线程不安全的原因我们可以看下面例子:
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("t1:" + SingletonFactory.getInstance());
System.out.println(Thread.currentThread().getName()+":"+i);
}
},"t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("t2:" + SingletonFactory.getInstance());
System.out.println(Thread.currentThread().getName()+":"+i);
}
},"t2");
t1.start();
t2.start();
}
/**
输入结果:
t2:com.fjc.demo.singleton.SingletonFactory@4b588b46
t1:com.fjc.demo.singleton.SingletonFactory@7c9e510c
t2:com.fjc.demo.singleton.SingletonFactory@7c9e510c
t1:com.fjc.demo.singleton.SingletonFactory@7c9e510c
**/
我们不难看出,SingletonFactory并没有保证唯一性,而是被新建了多次。在我们的getInstance方法中,可能会出现两个线程同时进if判断的情况,在这个时候两个线程拿到的singletonFactory都是空的,那么singletonFactory就会被新建两次并且返回给各自的线程。
public static SingletonFactory getInstance() {
if(singletonFactory == null) {
// t1、t2 同时进来
singletonFactory = new SingletonFactory();
}
return singletonFactory;
}
为了解决线程完全问题,我们可以使用synchronized关键词去修饰getInstance方法,加锁以后也就保证了线程安全;
使用饿汉式的方式去实现单例模式,并保证线程安全:
public class SingletonFactory {
private static SingletonFactory singletonFactory = new SingletonFactory();
/* 单例模式有⼀个特点就是不允许外部直接创建,也就是 new SingletonFactory() ,因此这⾥在默认的构造函数上添加了私有属性 private. */
private SingletonFactory() {
}
public static SingletonFactory getInstance() {
return singletonFactory;
}
}
这种方法在刚开始加载的时候就创建了SingletonFactory对象,而被static修饰的属性又只会被加载一次,所以也就保证了线程的安全性。
但是以上两种方法对于性能都有所损耗,
- synchronized:此种模式虽然是安全的,但由于把锁加到⽅法上后,所有的访问都因需要锁占⽤导致资源的浪费。 如果不是特殊情况下,不建议此种⽅式实现单例模式。
- 饿汉式:但此种⽅式并不是懒加载,也就是说⽆论程序中是否⽤到这样的类都会在程序启动之初进⾏创建。
还有一种方式既可以保证懒加载又可以保证线程安全,静态内部类实现单例模式:
public class SingletonFactory {
private static class SingletonInternal {
private static SingletonFactory instance = new SingletonFactory();
}
/* 单例模式有⼀个特点就是不允许外部直接创建,也就是 new SingletonFactory() ,因此这⾥在默认的构造函数上添加了私有属性 private. */
private SingletonFactory() {
}
public static SingletonFactory getInstance() {
return SingletonInternal.instance;
}
}
- 使⽤类的静态内部类实现的单例模式,既保证了线程安全又保证了懒加载,同时不会因为加锁的⽅式耗费性能。
- 也是比较推荐的一种实现单例方式
双重锁校验也可以实现懒汉式并且保证线程安全性,这个方式就是将原本直接在方法上加锁的方式换成了在对应不安全代码块上加锁,缩小了锁的范围。
双重锁校验实现懒汉式单例模式
public class SingletonFactory {
private static SingletonFactory singletonFactory;
/* 单例模式有⼀个特点就是不允许外部直接创建,也就是 new SingletonFactory() ,因此这⾥在默认的构造函数上添加了私有属性 private. */
private SingletonFactory() {
}
public static SingletonFactory getInstance() {
if (singletonFactory != null) return singletonFactory;
synchronized (SingletonFactory.class) {
if (singletonFactory==null) {
singletonFactory = new SingletonFactory();
}
}
return singletonFactory;
}
}
- 双重锁的⽅式是⽅法级锁的优化,减少了部分获取实例的耗时。
- 同时这种⽅式也满⾜了懒加载。
end.