1、前言
说到单例模式,最开始认识的时候是在数据库连接里面,当时也不知道为什么要采用单例模式,就是自学跟着视频里面的讲师敲代码,定义一个字段:private static DBConnectionManager instance;采用一个方法:
- public static synchronized DBConnectionManager getInstance() {
- if (instance == null) {
- instance = new DBConnectionManager();
- }
- return instance;
- }
说这个采用了单例模式,多个数据库连接时返回唯一实例,如果是第一次调用此方法,则创建实例 。其实当时真的是一脸懵逼,不知道为啥,只知道讲师这样做了,我也跟着这样做,然后就这样一步步做了下来,那个是我第一次接触连接数据库的小项目,叫DVD租赁管理系统,在网易云上的课程,大家如果有初学者感兴趣可以去看看。接着说今天的主题:单例模式。
2、什么是单例模式?
单例模式是用以确保一个特定的类只有一个对象被实例化的一种设计模式理念。它一般运用在数据库操作里,数据库在进行操作时要经常创建实例,然后再进行数据库操作,所以就将数据库操作的方法进行封装,采用单例模式进行设计。
为什么要采用这样的设计呢?好处是毋庸置疑的,它一是像概念说的那样,只生成一个实例,当一个对象的产生需要比较多的资源时,可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决;二是可以在系统设置全局的访问点,优化共享资源访问。
3、常见的五种单例模式的写法比较
饿汉式(调用效率高。但是不可以延时加载)【可以使用】
优点:
饿汉式单例模式代码块中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,线程是安全的。
缺点:
如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源的浪费。
案例演示:
public class SingletonDemo01 {
//类初始化时,立即加载这个对象。(没有延时加载的优势)
//加载类时,天然的是线程安全的!
private static SingletonDemo01 instance = new SingletonDemo01();
private SingletonDemo01(){
}
//方法没有同步,调用效率高!
public static SingletonDemo01 getInstance(){
return instance;
}
}
懒汉式(调用效率不高。但是可以延时加载)【单线程可以使用,多线程加锁使用】
优点:
Lazy Load!延时加载,懒加载!真正用的时候才加载。
缺点:
资源利用效率高了。但是每次调用getInstance()方法都要同步,并发效率低。
如果在不加锁的情况下不允许在多线程下使用,是不安全的。因为在多线程下,当一个线程进入了if(s==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。因此多线程情况下不允许不加锁使用。
案例演示:
public class SingletonDemo02 {
//类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
private static SingletonDemo02 instance;
//初始化构造器
private SingletonDemo02(){}
//方法同步,调用效率低
public static synchronized SingletonDemo02 getInstance(){
if(instance == null){
instance = new SingletonDemo02();
}
return instance;
}
}
双重检测锁(由于JVM底层内部模型原因,偶尔会出现问题)【不建议使用】
优点:
这种模式将同步内容放到if内部,提高了执行的效率,不必每次获取对象时都进行同步,只有第一次才同步创建,以后就没必要了。
缺点:
由于编译器优化原因和JVM底层内部模型原因,偶尔会出现问题,不建议使用。
public class SingletonDemo03 {
private static SingletonDemo03 instance;
// 初始化构造器
private SingletonDemo03(){}
public static synchronized SingletonDemo03 getInstance() {
if (instance == null) {
SingletonDemo03 sc;
synchronized (SingletonDemo03.class) {
sc = instance;
if(sc == null){
synchronized(SingletonDemo03.class){
if(sc == null){
sc = new SingletonDemo03();
}
}
instance = sc;
}
}
}
return instance;
}
}
静态内部类实现(调用效率高,而且可以延时加载)【推荐使用】
要点:
外部类没有static属性,则不会像饿汉式那样立即加载对象。
只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的。instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
兼备了并发高效调用和延迟加载的优势。
public class SingletonDemo04 {
//静态内部类
private static class SingletonClassInstance{
private static final SingletonDemo04 instance = new SingletonDemo04();
}
public static SingletonDemo04 getInstance(){
return SingletonClassInstance.instance;
}
private SingletonDemo04(){
}
}
枚举实现单例模式(调用效率高,不能延时加载,但是可以天然的防止反射和反序列化漏洞)【推荐使用】
优点:
实现简单
枚举本身就是单例模式。由于JVM从根本上提供了保障!避免了通过反射和反序列化的漏洞!(如何通过反射和反序列化得到单例对象,请看下一条博客)。
缺点:
无延时加载。
public enum SingletonDemo05 {
//这个枚举元素,本身就是单例对象!
INSTANCE;
//添加自己需要的操作。
public void singletonOperation(){
}
}
4、上面五种单例模式在多线程环境下的效率测试
测试代码:
public class ClientTest02 {
public static void main(String[] args) throws Exception{
long start = System.currentTimeMillis();
//线程数量
int threadNum = 10;
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
//将对象分别更改成类名
Object object = SingletonDemo01.getInstance();
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();//main线程阻塞,直到计数器变为0,才会继续往下执行。
long end = System.currentTimeMillis();
System.out.println(end-start);
}
}
测试结果:(效率请参照相对值)
饿汉式 | 10 |
懒汉式 | 145 |
双重检查锁 | 126 |
静态内部类 | 13 |
枚举类 | 20 |
5、总结
---单例对象 占用资源少,不需要延时加载 :枚举式好于饿汉式
---单例对象 占用资源大,需要延时加载 :静态内部类好于懒汉式
---双重检查锁不推荐使用,多线程使用懒汉式注意加锁!