【Java 并发编程】单例模式

ops/2024/10/16 0:18:38/

前言


        单例模式是一种十分常用但却相对而言比较简单的单例模式。虽然它简单但是包含了关于线程安全、内存模型、类加载机制等一些比较核心的知识点。本章会介绍单例模式的设计思想,会去讲解了几种常见的单例实现方式,如饿汉式、懒汉式、双重检锁、静态内部类、枚举等。


前期回顾:【Java 线程通信】模拟ATM取钱(wait 和 notify机制)


目录

前言

单例模式简介

 单例模式设计

饿汉式实现方式

代码分析

代码优劣

代码测试

懒汉式实现方式

代码优化

线程安全

效率低下 

双重检锁

内存可见性

完美代码

代码优劣

静态内部类的实现方式

枚举的实现方式

关于反射破坏

 

单例模式简介


        单例模式,顾名思义就是一个运行时域,一个类只有一个实例对象

        那么为什么需要单例模式呢?单例模式的使用场景是什么?

        像我们之前的写的类的实例对象的创建与销毁对资源来说消耗不大,用不用单例模型其实无所谓。但是有些类的消耗比较大,如果频繁的创建与销毁而且这些类的对象完全可以复用的话,这势必会造成不必要的性能浪费

举个栗子~

        我们要写一个访问数据库类,但是创建数据库链接对象是一个十分耗资源的操作,并且数据库链接是完全可以复用的。那么可以把这个类设置为单例的,这样只需要创建一次对象并且重复使用这个对象就好了,而不用每次去访问数据库都要创建链接对象。

 

 单例模式设计


        单例模式有多种写法,比如饿汉式、懒汉式等等。但是不管是哪一种写法其实都要考虑一下三点:

是否代程安全
是否懒加载(也叫延迟加载)
能否反射破坏

 

饿汉式实现方式

class Singleton {private static Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}

        因为这种方法在 类加载时 就立即创建了实例,就如饿汉看见吃的就 “迫不及待”的去吃的感觉。这里也是如此,类一加载就迫不及待的创建了对象,所以称之为饿汉。

 

代码分析

(1) 由于单例就是一个类只有一个实例对象的,所以我们并不希望别人能通过 new 直接创建对象,所以我们使用 private 来修饰构造方法

(2) 这个对象由于 static 静态修饰的,所以在类加载的时候就已经创建好了,通过 getInstance 调用只是获取这个对象实例而已,并且这种创建方式是天生线程安全的。

 

代码优劣

优点:

JVM 在加载这个类的时候就会对它进行初始化, 这里包含对静态变量的初始化,天生线程安全
没有加锁,运行效率更高


缺点:

类加载时就初始化,若是重启服务的话,会拖慢运行速度
类加载时就初始化,如果创建了不使用,会导致内存浪费

 

代码测试

class Test{public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);}
}

运行结果

true

        所以饿汉的方式创建对象只会创建一个单例,考虑到空间浪费,使用的时候还需权衡优劣。

懒汉式实现方式

以下是只是标准模板(单线程版本),还有很多因素没有考虑 ~

class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

        以上代码我们可以发现这个对象并不会随着类加载而创建,而是在第一次访问单例类的实例时才去创建(第一次调用 getInstance 方法),我们将这种延迟创建的行为称之为 “懒汉”。

 

代码优化

线程安全

(1) 首先我们发现以上代码是线程不安全的,在执行以下这条语句时

if (instance == null)

可能会有多个线程已经越过这个语句去创建对象了,所以它是不安全的。

我们需要改进一下:

class Singleton {private static Singleton instance = null;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

 

效率低下 

(2) 先给这个方法加上 synchronized ,这样就能保证同一时刻只有一个线程访问这个方法了,但是这样又会引入新的问题:其实我们只想要对像构建的时候同步线程,像以上这种代码是每次在获取对象的时候都要进行同步操作,这样对性能影响是是十分大的。所以这种方法并不推荐。

        通过以上可以知道要想提升效率,直接在对象构建的时候加同步锁就可以了,而使用对象是不需要同步的,那么我们就可以改成这样。如下图:

class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {instance = new Singleton();}}return instance;}
}

 

双重检锁

(3) 关于上述代码,getInstance 是不需要参与锁竞争的所有线程都可直接进入,那么现在就开始第二步判断,如果实例对象没有创建,那么所有线程都会去争抢锁,抢到锁的那个线程会开始创建实例对象。实例对象创建了之后,以后所有的 getInstance 操作都是进行到第二步直接跳过,然后返回当前实例对象。这就解决了上述代码的低效问题。

        但是以上代码仍然是有问题的:

 if (instance == null) {synchronized (Singleton.class) {instance = new Singleton();}}

        假设 线程A、线程B 同时进入 if 语句,那么线程A拿到锁后创建了一个实例对象后将锁释放了;此时 线程B 拿到锁也可以创建实例对象。此时就可以创建多个实例对象了,所以这也是线程不安全的。有没有一种办法保证线程安全呢?其实我们只要在内部在加上一条 if 判断检查当前对象是否被创建即可。这种方法也被叫做双重检锁

class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

        此时 线程B 获得锁以后会进行一个判空,此时 线程A 已经实例过一次了,线程B 自然就不能创建对象了。

 

内存可见性

(4) 关于上述代码虽然看上去已经很完美了,但是还是有一点瑕疵。这里就要谈到 happens-before 内存可见性原则。简单来讲就是我们简单的一条 Java 语句,内部其实是有多种指令运行完成的。

        比如像以下这行代码,由于不是原子操作,虽然只是一条语句,但实际有三个指令在完成操作。

 instance = new Singleton();
(1)为对象分配内存
(2)初始化对象
(3)返回对象指向的内存地址

        以上的一条语句在非并发也就是单线程中是没有问题的,但是在并发执行时,虚拟机为了效率可能会对指令进行重排比如说 线程A 的执行顺序是:1->3->2。那么这个线程是先为对象分配好内存,再返回这个对象指向的内存地址,但是由于这个对象还没来得及初始化。此时如果有一个线程 B 进行 if (instance == null) 判空操作就会返回 false 跳过创建对象这个步骤,直接返回这个未初始化的对象。这也是造就了线程安全问题。那么怎么解决呢?我们只需加上 volatile 修饰即可。

 

完美代码
class Singleton {private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

 

代码优劣

优点

需要这个实例的时候,先判断它是否为空,如果为空,再创建单例对象
用到的时候再去创建,避免了创建了对象不去的用而造成浪费

缺点

由于懒汉模式经过优化过后已经没有什么缺点了,唯一的缺点就是编写略显复杂。

关于其他的创建方式这里简述一下: 

静态内部类的实现方式

        JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性、方法被调用时才会被加载,并初始化其静态属性

class StaticInnerSingleton {private StaticInnerSingleton() {}public static StaticInnerSingleton getInstance() {return SingletonHolder.instance;}private static class SingletonHolder {private static StaticInnerSingleton instance = new StaticInnerSingleton();}
}

        比较推荐这种方式,没有加锁,线程安全。用到时再加载,并发行能高。

枚举的实现方式

        枚举单例是最好的单例,有效防止反射

enum EnumSingleton {// 此枚举类的一个实例, 可以直接通过EnumSingleton.INSTANCE来使用INSTANCE
}

关于反射破坏

        以上的方式除了枚举,其他都能被放射破坏。但是反射是一种人为操作,只有故意去这样操作才会造成反射破坏。

class Singleton {private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}class Test1{public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Singleton s1  = Singleton.getInstance();// 使用反射创建Singleton实例Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);// 通过反射获取的实例Singleton s2 = declaredConstructor.newInstance();System.out.println(s1 == s2);}
}

运行结果:

false

 关于如何利用反射破坏单例,请参考以上代码 ~


http://www.ppmy.cn/ops/126185.html

相关文章

Python脚本爬取目标网站上的所有链接

一、爬取后txt文件保存 需要先pip install requests和BeautifulSoup库 import requests from bs4 import BeautifulSoup# 定义要爬取的新闻网站URL url https://www.chinadaily.com.cn/ # China Daily 网站# 发送请求获取页面内容 response requests.get(url)# 检查请求是否…

杨中科 .netcore Linq 。一前期准备知识

为什么要学Linq 一、为什么要学LINQ? 让数据处理变得简单: 统计一个字符串中每个字母出现的频率(忽略大小写)&#xff0c;然后按照从高到低的顺序输出出现频率高于2次的单词和其出现的频率。 var itemss.Where(c >char.lsLetter(c))//过滤非字母 .Select(c>char.ToLo…

Qt 每日面试题 -8

71、了解Qt的QPointer吗? QPointer只能用于指向QObject及派生类的对象。 当一个QObject或派生类对象被删除后&#xff0c;QPointer能自动将其内部的指针设置为0 &#xff0c;这样在使用QPointer之前就可以判断一下是否有效。QPointer对象超出作用域时&#xff0c;并不会删除它…

Linux:进程控制(三)——进程程序替换

目录 一、概念 二、使用 1.单进程程序替换 2.多进程程序替换 3.exec接口 4.execle 一、概念 背景 当前进程在运行的时候&#xff0c;所执行的代码来自于自己的源文件。使用fork创建子进程后&#xff0c;子进程执行的程序中代码内容和父进程是相同的&#xff0c;如果子进…

Ubuntu+CLion+OpenCV+NCNN+Squeezenet 从源码编译到代码输出全流程记录

✨博客主页&#xff1a;王乐予&#x1f388; ✨年轻人要&#xff1a;Living for the moment&#xff08;活在当下&#xff09;&#xff01;&#x1f4aa; &#x1f3c6;推荐专栏&#xff1a;【图像处理】【千锤百炼Python】【深度学习】【排序算法】 目录 &#x1f63a;一、引言…

P1001 | 禾木切西瓜

P1001 | 禾木切西瓜 题目描述 酷热的夏天&#xff0c;禾木从冰箱中拿出一只西瓜&#xff0c;想要将它一分为二&#xff0c;用勺子舀着吃&#xff0c;但作为一个资深的吃货&#xff0c;禾木认为一只好西瓜&#xff0c;一分为二后&#xff0c;两部分的重量都应该为偶数。 例如一…

在 Jupyter Notebook 中,无法看到特定 Conda 环境的内核

问题概述 在 Jupyter Notebook 中&#xff0c;无法看到特定 Conda 环境的内核&#xff0c;导致无法在该环境下运行代码。这通常是由于内核未正确注册到 Jupyter 所致。 常见原因 未安装 ipykernel&#xff1a;每个 Conda 环境需要安装 ipykernel 才能作为 Jupyter 内核使用。…

【鱼类识别】Python+卷积神经网络算法+人工智能+深度学习+计算机毕设项目+Django网页界面+TensorFlow

一、介绍 鱼类识别系统。使用Python作为主要编程语言开发&#xff0c;通过收集常见的30种鱼类&#xff08;‘墨鱼’, ‘多宝鱼’, ‘带鱼’, ‘石斑鱼’, ‘秋刀鱼’, ‘章鱼’, ‘红鱼’, ‘罗非鱼’, ‘胖头鱼’, ‘草鱼’, ‘银鱼’, ‘青鱼’, ‘马头鱼’, ‘鱿鱼’, ‘鲇…