文章目录
一、什么是单例模式?
单例模式是属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程(线程)中只有一个实例。
这种模式涉及一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,并且这个类提供了访问其唯一对象的方法。
二、单例模式的实现
1.饿汉模式
1.1 饿汉模式的定义
饿汉模式就是在类加载的时候立刻就会实例化对象,并且只会实例化一个对象,后续使用就只会出现一份实例。
1.2 饿汉模式的特点
- 将构造方法私有化
- 在类的内部只定义一个 private static 修饰的实例对象
- 提供一个 getInstance() 获取实例对象的方法
- 饿汉模式天然就是线程安全的
1.3 饿汉模式的实现
class HungrySingleton {
// private static 修饰实例对象
private static HungrySingleton singleton = new HungrySingleton();
//私有化构造方法
private HungrySingleton() {
}
//提供获取该实例对象的方法
public static HungrySingleton getInstance() {
return singleton;
}
}
public class HungrySingletonDemo {
public static void main(String[] args) {
HungrySingleton singleton1 = HungrySingleton.getInstance();
HungrySingleton singleton2 = HungrySingleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
结果展示:
在饿汉模式实现的代码中,将构造方法私有化,能够保证只有在本类中能够创建对象;通过 private static 修饰实例对象,表示这是一个类对象,被该类的所有对象所共有;提供一个 getInstance() 方法,保证通过 getInstance() 获取到的对象是同一个对象。因此在测试类中,无论调用几次 getInstance(),得到的对象都是同一个对象。
1.4 为什么说饿汉模式天然就是线程安全的?
首先在饿汉模式中,实例化对象是随着类的加载而加载的,也就是说类加载完成,实例化对象也就完成了。另外,在饿汉模式中,如果多个线程同时调用 getInstance() 获取实例对象,只是会触发多个线程读取实例化对象,并没有对实例化对象进行修改,因此,不会触发引起线程不安全的五个原因中任何一个,所以说饿汉模式天然就是线程安全的。
2.懒汉模式
2.1 懒汉模式的定义
懒汉模式就是指,在类加载的时候不直接实例化对象,而是在第一次使用的时候才会创建实例化对象。
2.2 懒汉模式的特点
- 构造方法私有化
- 用 private static 修饰实例对象,但是没有直接初始化
- 提供 getInstance() 方法获取实例对象,同时实例化对象采用双重校验锁的方式实现
- 懒汉模式天然不是线程安全的
2.3 线程安全的懒汉模式的实现方式
2.3.1 用 synchronized 修饰获取实例化对象的方法
class SingletonLazy {
//创建对象时,没有立即初始化
private static SingletonLazy singletonLazy = null;
//构造方法私有化
private SingletonLazy() {
}
synchronized public static SingletonLazy getInstance() {
if (singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
}
public class SingletonLazyDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
System.out.println(singletonLazy1);
});
Thread t2 = new Thread(() -> {
SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
System.out.println(singletonLazy2);
});
Thread t3 = new Thread(() -> {
SingletonLazy singletonLazy3 = SingletonLazy.getInstance();
System.out.println(singletonLazy3);
});
t1.start();
t2.start();
t3.start();
}
}
结果展示:
通过 synchronized 修饰获取实例的方法,可以保证每次只有一个线程可以可以调用获取实例化对象的方法,并且只会在第一次调用该方法时完成实例化对象的创建和初始化。
2.3.2 用 volatile 修饰实例化对象,并采用双重锁机制创建实例化对象
class SingletonLazy {
//创建对象时,没有立即初始化
volatile private static SingletonLazy singletonLazy = null;
//构造方法私有化
private SingletonLazy() {
}
//采用双重校验锁机制,提供获取实例对象的方法
public static SingletonLazy getInstance() {
//判断是否需要加锁
if (singletonLazy == null) {
synchronized (SingletonLazy.class) {
//判断是否需要创建对象
if (singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
}
public class SingletonLazyDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
System.out.println(singletonLazy1);
});
Thread t2 = new Thread(() -> {
SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
System.out.println(singletonLazy2);
});
Thread t3 = new Thread(() -> {
SingletonLazy singletonLazy3 = SingletonLazy.getInstance();
System.out.println(singletonLazy3);
});
t1.start();
t2.start();
t3.start();
}
}
结果展示:
- 第一个 if 的作用,是判断是否需要加锁。主要是用于优化代码的执行效率,如果没有这个 if,那么当多个线程执行到这里时,都会阻塞等待,影响程序的执行效率;
- 第二个 if 的作用是判断是否需要实例化对象,主要作用是优化代码的执行效率。
- 虽然加锁确保同一时刻只有一个线程能够调用实例化方法,但是并不能保证线程安全,因为存在内存可见性和指令重排序的问题,会引起线程安全问题,要解决这个问题,就需要使用 volatile 修饰实例化对象。这样就可以保证该方法实现的懒汉模式是线程安全的。
总结
并发编程是一个复杂的过程,要想在实际操作中保证线程的安全,就要搞清楚导致线程安全问题的原因。