Java并发编程学习系列八:单例模式

简介: Java并发编程学习系列八:单例模式

介绍


什么是单例模式?

通俗的讲,就是在应用程序中只需要某个类保留唯一一个实例对象,不希望有更多的实例。单例模式是 Java 设计模式中最简单的设计模式之一,在应用程序中经常被用到。


应用场景


单例模式的应用场景有很多,比如线程池、日志对象、缓存、数据库连接池、计算机系统设备管理器等等。这些常常都设计成全局唯一的,方便集中管理,也节省系统的开销。


实现方式


实现单例模式要注意以下三点:

1、单例类只能有一个实例,不能从其他对象中 new 出来, 即构造器用 private 修饰。

2、单例类必须自己创建自己的唯一实例,需要实现一个方法提供这个实例。

3、单例类必须能给其他对象提供这一实例。


接下来我们讲讲在 Java 中如何实现单例模式 :


(1)饿汉式


饿汉式,顾名思义指的是在类加载的时候就初始化好对象,不管有没有用到。绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。

Spring 中 IOC 容器 ApplicationContext 就是典型的饿汉式单例。


public class Singleton {
    private final static Singleton singleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return singleton;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Singleton singleton1 = Singleton.getInstance();   //获取都是同一个对象
            System.out.println(singleton1.hashCode());      
        }
    }
}
复制代码


还有另外一种写法,利用静态代码块的机制:


public class Singleton {
    // 1. 私有化构造器
    private Singleton(){}
    // 2. 实例变量
    private static final Singleton instance;
    // 3. 在静态代码块中实例化
    static {
        instance = new Singleton();
    }
    // 4. 提供获取实例方法
    public static Singleton getInstance(){
        return instance;
    }
}
复制代码


(2)懒汉式


懒汉式和饿汉式相对,指的在程序加载时不初始化对象,什么时候被引用什么时候才初始化对象,即在第一次使用的时候才去初始化对象,可以避免内存浪费。注意在获取实例的 getInstance()方法前加上了 synchronized 关键字,这是为了保证线程安全,避免多线程同一时刻获取对象时造成生成了多个实例。


public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public synchronized static Singleton getInstance(){
        if(singleton == null){   // 1
            singleton = new Singleton(); // 2
        }
        return singleton;
    }
}
复制代码


(3)双重检查锁


双重检查锁是在懒汉式基础上演变过来的,当分析懒汉式代码时,你会发现只有在第一次调用获取实例方法时才需要同步。因为仅步骤2处的代码需要同步,但只有第一次调用才执行此行,后面的其他调用没有执行此行,但都付出了同步的代价。因此为避免在实例已经创建的情况下每次获取实例都加锁取,提高效率,双重检查锁应运而生。

为什么要二次检查?分析双重检查锁代码,多线程并发情况下, 第一个线程执行完 synchronized 的代码块后,后面的线程仍然需要对 singleton 进行第二次检查,即步骤3,避免重复实例化对象。所以需要对实例对象做两次检查。


既然 synchronized 能保证有序性,为什么还要加 volatile?多线程情况下,synchronized 关键字能够起到同步的作用,保证每次只有一个线程能够操作。这样一来对于其内部就相当于单线程操作,但是不会影响其内部的指令重排。我们知道 volatile 可以禁止指令重排,步骤4是属于复合操作指令,首先是实例化对象,然后才是写操作,关于实例化对象其实分为以下三个步骤:


   (1)分配内存空间。

   (2)初始化对象。

   (3)将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

   (1)分配内存空间。

   (2)将内存空间的地址赋值给对应的引用。

   (3)初始化对象


如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为 volatile 类型的变量。volatile 变量的写操作,会保证之前的所有指令一定会在 volatile 写操作之前完成,那么 instance = new SingleTon() 这个复合操作指令一定是对象创建完成再进行赋值。


public class Singleton {
    private volatile static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(singleton == null){ // 步骤1
            synchronized (Singleton.class){ // 步骤2
                if(singleton == null){ // 步骤3
                    singleton = new Singleton(); // 步骤4
                }
            }
        }
        return singleton;
    }
}
复制代码


(4)静态内部类


这种方式能达到双检锁方式一样的功效,但实现更为简单。这种和饿汉式比较,在类加载时,singleton实例并没有被初始化,需要显示调用getInstance()方法才会转载SingleHolder类,从而初始化singleton实例,所以达到了延时加载的效果。此方法在实际使用中用的最多,推荐此种写法。


public class Singleton {
    private static class SingleHolder{
        private static Singleton singleton = new Singleton();
    }
    private Singleton(){ }
    public static Singleton getInstance(){
        return SingleHolder.singleton;
    }
}
复制代码


(5)枚举


这种方式巧妙的应用了枚举的特点,构造器本身私有,写法简单,自动支持序列化机制,防止多次实例化,获取实例可以通过 Singleton.INSTANCE 来访问。


public enum Singleton {
    INSTANCE;
}
复制代码


参考文献


java 在同步锁内外判断两次,有什么用处?

Java设计模式(一)—— 单例模式

设计模式 - 单例模式(详解)看看和你理解的是否一样?

设计模式 - 单例模式之多线程调试与破坏单例


目录
相关文章
|
23天前
|
Java
Java基础学习day08-作业
本作业涵盖Java中Lambda表达式的应用,包括Runnable与Comparator接口的简化实现、自定义函数式接口NumberProcessor进行加减乘及最大值操作,以及通过IntProcessor处理整数数组,实现遍历、平方和奇偶判断等功能,强化函数式编程实践。
48 5
|
23天前
|
Java API 容器
Java基础学习day08-2
本节讲解Java方法引用与常用API,包括静态、实例、特定类型方法及构造器引用的格式与使用场景,并结合代码示例深入解析。同时介绍String和ArrayList的核心方法及其实际应用。
105 1
|
28天前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
285 1
|
28天前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
346 100
|
24天前
|
Java 程序员
Java基础学习day08
本节讲解Java中的代码块(静态与实例)及其作用,深入介绍内部类(成员、静态、局部及匿名)的定义与使用,并引入函数式编程思想,重点阐述Lambda表达式及其在简化匿名内部类中的应用。
99 5
|
24天前
|
Java
Java基础学习day07-作业
本作业包含六个Java编程案例:1)动物类继承与多态;2)加油卡支付系统;3)员工管理类设计;4)学生信息统计接口;5)USB设备控制;6)家电智能控制。综合运用抽象类、接口、继承、多态等面向对象技术,强化Java基础编程能力。
141 3
|
24天前
|
Java
Java基础学习day06-作业
本内容为Java基础学习作业,涵盖两个案例:一是通过Card类及其子类GoldenCard、SilverCard实现加油卡系统,体现封装与继承;二是通过Shape类及子类Circle、Rectangle演示多态与方法重写,强化面向对象编程理解。
48 1
|
24天前
|
设计模式 存储 Java
Java基础学习day07
本节讲解Java中的final关键字、单例设计模式、枚举类、抽象类与接口。涵盖常量定义、单例写法(饿汉式/懒汉式)、枚举特点及应用场景,以及抽象类与接口的使用与区别,助力掌握核心面向对象编程思想。
96 1
|
29天前
|
Java
Java基础学习day05-作业
本文为Java基础学习第五天作业,通过五个案例练习类与对象的定义、构造方法、set/get方法及成员方法的应用。涵盖女友、学生、教师、手机和电影等类的设计与测试,强化面向对象编程基础。
56 2
【JAVA学习之路 | 进阶篇】Map接口及其实现类及常用方法
【JAVA学习之路 | 进阶篇】Map接口及其实现类及常用方法