单例模式
一、什么是单例模式
单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。
二、单例模式和静态类的区别
首先理解一下什么是静态类,静态类就是一个类里面都是静态方法和静态field,构造器被private修饰,因此不能被实例化。Math类就是一个静态类。
知道了什么是静态类后,来说一下他们两者之间的区别:
- 首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;
- 单例模式的灵活性更高,方法可以被override,因为静态类都是静态方法,所以不能被override;
- 如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;
- 那么时候时候应该用静态类,什么时候应该用单例模式呢?首先如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用单例模式。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。
三、实现单例模式
这里先放一下测试主程序
每个对象的hashCode相同,即每个对象相同;hashCode不同,即创建了多个对象,每个对象不同
class MyThread implements Runnable {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
// 饿汉模式 线程安全
//System.out.println(currentThread.getName() + SingletonHungry.getInstance().hashCode());
// 懒汉模式 线程不安全
//System.out.println(currentThread.getName() + SingletonLazy.getInstance().hashCode());
// 懒汉模式 同步方法 线程安全
//System.out.println(currentThread.getName() + SingletonLazySyncMethod.getInstance().hashCode());
// 懒汉模式 同步代码块 线程安全
//System.out.println(currentThread.getName() + SingletonLazySyncBlock.getInstance().hashCode());
// 懒汉模式 缩小同步代码块 线程不安全
//System.out.println(currentThread.getName() + SingletonLazySyncSmallBlock.getInstance().hashCode());
// 懒汉模式 双重锁机制 线程安全
//System.out.println(currentThread.getName() + SingletonLazyDoubleCheck.getInstance().hashCode());
}
}
public class SingletonPatternTest {
public static void main(String[] args) {
// 开启10个线程
for (int i = 0; i < 10; i++) {
MyThread myThread = new MyThread();
// 开启线程
new Thread(myThread, "线程" + i + ":").start();
}
}
}
1、饿汉模式
所谓饿汉模式就是立即加载,一般情况下再调用getInstance方法之前就已经产生了实例,也就是在类加载的时候已经产生了。这种模式的缺点很明显,就是占用资源,当单例类很大的时候,其实我们是想使用的时候再产生实例。因此这种方式适合占用资源少,在初始化的时候就会被用到的类。
// 饿汉模式 线程安全
public class SingletonHungry {
private static SingletonHungry singletonHungry = new SingletonHungry();
// 私有化构造方法
private SingletonHungry() {}
// 提供返回该对象的静态方法
public static SingletonHungry getInstance() {
return singletonHungry;
}
}
测试结果
2、懒汉模式1
懒汉模式就是延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。针对懒汉模式,这里给出了几种实现方式,但有些实现方式是线程不安全的,也就是说在多线程并发的环境下可能出现资源同步问题。
// 懒汉模式 线程不安全
public class SingletonLazy {
private static SingletonLazy singletonLazy = null;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (null == singletonLazy) {
try {
// 模拟对象创建之前的准备工作
Thread.sleep(1000);
singletonLazy = new SingletonLazy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singletonLazy;
}
}
测试结果
3、懒汉模式2
因为上述懒汉模式1是线程不安全的,我们可以考虑用synchronized修饰getInstance方法进行同步实现线程安全
// 懒汉模式 线程安全
public class SingletonLazySyncMethod {
private static SingletonLazySyncMethod singletonLazySyncMethod = null;
private SingletonLazySyncMethod() {}
public static synchronized SingletonLazySyncMethod getInstance() {
if (null == singletonLazySyncMethod) {
try {
// 模拟对象创建之前的准备工作
Thread.sleep(1000);
singletonLazySyncMethod = new SingletonLazySyncMethod();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singletonLazySyncMethod;
}
}
测试结果
这种方法虽然实现了线程安全,但是由于是同步运行的,下个线程想要取得对象,就必须要等到上一个线程释放,才可以继续执行,所以效率很低
4、懒汉模式3
上述懒汉模式2效率太低,我们可以将它稍微改进,不用同步方法实现线程安全,可以在方法内部采用同步代码块实现线程安全,像这样
// 懒汉模式 线程安全
public class SingletonLazySyncBlock {
private static SingletonLazySyncBlock singletonLazySyncBlock = null;
private SingletonLazySyncBlock() {}
public static SingletonLazySyncBlock getInstance() {
synchronized (SingletonLazySyncBlock.class) {
if (null == singletonLazySyncBlock) {
try {
// 模拟对象创建之前的准备工作
Thread.sleep(1000);
singletonLazySyncBlock = new SingletonLazySyncBlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singletonLazySyncBlock;
}
}
}
测试结果
这种方式虽然也实现了线程安全,但是效率还是很低
5、懒汉模式4
上述懒汉模式3使用同步代码块实现了线程安全,但是效率还是较为低下。接下来。我们试着再次缩小同步代码块的范围
// 懒汉模式 线程不安全
public class SingletonLazySyncSmallBlock {
private static SingletonLazySyncSmallBlock singletonLazySyncSmallBlock = null;
private SingletonLazySyncSmallBlock() {}
public static SingletonLazySyncSmallBlock getInstance() {
if (null == singletonLazySyncSmallBlock) {
try {
// 模拟对象创建之前的准备工作
Thread.sleep(1000);
synchronized (SingletonLazySyncSmallBlock.class) {
singletonLazySyncSmallBlock = new SingletonLazySyncSmallBlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singletonLazySyncSmallBlock;
}
}
测试结果
上述测试结果,我们可以看到,很不辛失败了,缩小了同步代码块的范围不能保证线程安全了。分析一下原因:我们假设有两个线程A、B同时走进了if代码块中,由于线程A比较牛批,首先抢到了锁,就去创建对象了;而线程B只能等待,线程A创建完成对象之后,释放锁线程B拿到锁也会去创建对象,这样就创建了两个对象,所以多线程环境下就不能保证单例了。
6、懒汉模式5
上述懒汉模式4,为什么出现线程不安全,我们已经分析过了。接下来我们考虑如何去解决线程不安全。还拿上述线程A、B的例子来说,线程B之所以会再次创建对象,是因为线程B已经闯过了if条件的判断,那么我们此时考虑在线程B拿到锁之后,再让线程B去进行一次if判断,也就是在synchronized代码块中再加一层if判断,好的,问题解决。这样,既缩小了同步代码块的范围,又保证了多线程环境下的线程安全。而且这种条件下,不像上述的懒汉模式2和懒汉模式3,懒汉模式2的同步方法是每次都要执行的,懒汉模式3的同步代码块也是每次都要执行的,而这里的懒汉模式5同步代码块的执行次数与第一次同时闯过第一层if判断的线程数相同,而后续线程过第一层if判断时,在第一层if判断就被拦截了。所以大大提高了程序的效率。这种方式就叫做双重检查锁机制
// 懒汉模式 线程安全
public class SingletonLazyDoubleCheck {
// volatile修饰
private volatile static SingletonLazyDoubleCheck singletonLazyDoubleCheck = null;
private SingletonLazyDoubleCheck() {}
public static SingletonLazyDoubleCheck getInstance() {
if (null == singletonLazyDoubleCheck) {
// 模拟对象创建之前的准备工作
try {
Thread.sleep(1000);
synchronized (SingletonLazyDoubleCheck.class) {
if(null == singletonLazyDoubleCheck) {
singletonLazyDoubleCheck = new SingletonLazyDoubleCheck();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singletonLazyDoubleCheck;
}
}
测试结果