单例模式:线程安全,以及volatile关键字

对于OOP语言而言,每new() 一个对象,就会有一个对象实例生成。但是很多时候需要在程序运行时全局使用同一个实例,避免生成多余的实例或者资源浪费。这类设计模式就是单例模式。

单例模式有如下要求:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

其中第二和第三点都是为了给第三点打补丁,如果不是由单例类本身创建和提供实例,那么就没办法在全局中确保使用同一个实例。单例模式通过设置私有的构造函数以防止外部创建对象,为此,需要设置一个对外接口getInstance()方法提供对象。

饿汉式

public class SingleObject {  
    // 1.饿汉模式,类加载时通过静态关键字就创建好对象
    private static SingleObject instance = new SingleObject();  
  
    private SingleObject() {  
    }  
  
    public static SingleObject getInstance() {  
        return instance;  
    }  
  
}

饿汉式单例模式认为对象总会使用,因此提前创建好对象。这种方式线程安全,但是在类加载时就初始化,浪费内存,容易产生垃圾对象。

测试可以发现得到的是同一对象:

public class Main {  
    public static void main(String[] args) {  
        for (int i = 0; i < 50; i++) {  
            SingleObject instance = SingleObject.getInstance();  
            System.out.println(instance);  
        }  
    }  
}

输出:

SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
……

懒汉式

public class SingleObject {  
    //2. 懒汉模式  
    private static SingleObject instance;  
    private SingleObject() {  
    }  
    public static SingleObject getInstance() {  
        if (instance == null) {  
            instance = new SingleObject();  
        }  
        return instance;  
    }  
  
}

和饿汉式单例模式不同,懒汉式单例模式只在外部第一次需要对象的时候才创建。这种方式也被称为懒加载(Lazyload),spring中的@Lazy注解通过类似的方式实现类的懒加载。避免了饿汉式浪费内存的问题。但是在多线程并发的情况下,由于多个线程可能同时进入判断if (instance == null),可能会导致多个实例的创建。

并发情况下测试代码:

public class Main {  
    public static void main(String[] args) throws InterruptedException {  
  
        int threadCount = 10;  
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);  
  
        Runnable runnable = new Runnable() {  
            @Override  
            public void run() {  
                SingleObject instance = SingleObject.getInstance();  
                System.out.println(instance + ",线程" +  Thread.currentThread().getName());  
                countDownLatch.countDown();  
            }  
        };  
        for (int i = 0; i < threadCount; i++) {  
            new Thread(runnable).start();  
        }  
  
    }  
}

运行结果:

SingleObject.SingleObject@65a9a79e,线程Thread-4
SingleObject.SingleObject@65a9a79e,线程Thread-5
SingleObject.SingleObject@65a9a79e,线程Thread-2
SingleObject.SingleObject@6517209b,线程Thread-9 //新实例
SingleObject.SingleObject@65a9a79e,线程Thread-7
SingleObject.SingleObject@65a9a79e,线程Thread-3
SingleObject.SingleObject@65a9a79e,线程Thread-8
SingleObject.SingleObject@65a9a79e,线程Thread-1
SingleObject.SingleObject@2f992f4f,线程Thread-0 //新实例
SingleObject.SingleObject@65a9a79e,线程Thread-6
Process finished with exit code 0

懒汉式+双重检查

线程安全的加锁方式

避免线程不安全的方式最常用的就是加锁,对于懒汉式的改进很简单,就是直接加锁。

public class SingleObject {  
    //2. 懒汉模式  
    private static SingleObject instance;  
    private SingleObject() {  
    }  
    public static SingleObject getInstance() {  
        synchronized (SingleObject.class){  
            if (instance == null) {  
                instance = new SingleObject();  
            }  
        }  
        return instance;  
    }  
}

双重检查

加锁很重要的一点就是性能差,会影响效率。为此给出双重检查版本。

public class SingleObject {  
    //2. 懒汉模式  
    private static SingleObject instance;  
    private SingleObject() {  
    }  
    public static SingleObject getInstance() {  
        if (instance == null){  // 第一次检查,如果不为null,直接返回
            synchronized (SingleObject.class){  
                if (instance == null) {  // 第二次检查
                    instance = new SingleObject();  
                }  
            }  
        }  
        return instance;  
    }  
}

结果

SingleObject.SingleObject@7a5f6ff8,线程Thread-5
SingleObject.SingleObject@7a5f6ff8,线程Thread-9
SingleObject.SingleObject@7a5f6ff8,线程Thread-7
SingleObject.SingleObject@7a5f6ff8,线程Thread-2
SingleObject.SingleObject@7a5f6ff8,线程Thread-3
SingleObject.SingleObject@7a5f6ff8,线程Thread-4
SingleObject.SingleObject@7a5f6ff8,线程Thread-8
SingleObject.SingleObject@7a5f6ff8,线程Thread-0
SingleObject.SingleObject@7a5f6ff8,线程Thread-1
SingleObject.SingleObject@7a5f6ff8,线程Thread-6

Process finished with exit code 0

volatile关键字

private volatile static SingleObject instance;  

然而以上的双重检查并不能完全保障完美实现单例模式,因为instance = new Singleton()并非原子操作,包含给对象实例分配内存空间,调用构造方法初始化对象,将对象指向分配的内存空间三步。这三步可能由于提高执行效率而改变执行顺序。

a. memory = allocate() //分配内存  写操作
b. ctorInstanc(memory) //初始化对象 写操作
c. instance = memory   //设置instance指向刚分配的地址 写操作

被volatile关键字修饰的变量被称为volatile变量。通过插入内存屏障禁止指令重排序,保证写操作前的代码不会重排到写操作之后,读操作后的代码不会重排到读操作之前。此时会按照a-b-c的顺序执行。

如果没有加上volatile关键字,上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行instance = new Singleton()时,B线程进来执行到第一次if (instance == null)。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接返回一个未初始化的对象。

参考

Java设计模式—单例模式的实现方式和使用场景-阿里云开发者社区
Java中Volatile关键字详解 - 郑斌blog - 博客园
Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)-CSDN博客

posted @ 2025-09-13 18:04  eien  阅读(34)  评论(0)    收藏  举报