Java单例模式,看这一篇就够了
在创建型设计模式中,我们第一个学习的是单例模式(Singleton Pattern),这是设计模式中最简单的模式之一。
单例是什么意思呢?
单例就是单实例的意思,即在系统全局,一个类只创建一个对象,并且在系统全局都可以访问这个对象而不用重新创建。
一、单例模式的基本写法
单例模式示例代码:
public class Singleton {
// Singleton类自己持有这个单例对象
private static Singleton instance = new Singleton();
// 构造方法设置为私有,避免在Singleton类外部创建Singleton对象
private Singleton() {}
// 提供获取单例对象的静态方法
public static Singleton getInstance() {
return instance;
}
public void hello() {
System.out.println("Hello!");
}
}
使用:
Singleton obj = Singleton.getInstance();
obj.hello();
分析SingleObject类的特征:
- SingleObject类的构造方法是私有的,这样可以保证只能在SingleObject类内部才能创建对象,而无法在类外部创建SingleObject对象。
- SingleObject类中有一个instance成员属性,它用来持有这个SingleObject对象。
- SingleObject类提供了一个静态方法getInstance,它可以让我们在任何可以访问到SingleObject类的地方,都可以使用
SingleObject.getInstance()
来获取到这个SingleObject对象。
二、单例模式的作用
单例模式有什么用呢?
1. 控制对象的数量
当你编写了一个类提供给其他人调用时,对方看到是一个类,很有可能第一反应是尝试new一下。
你自己编写的类你自己是清楚如何使用的,在整个系统内这个类只需要创建一个对象就够了,但对方可能并不清楚。
这时候你可以把这个类编写为单例形式,把构造方法私有化,让对方无法通过new来创建对象,只能使用getInstance来获取。
这个模式可以帮助你有效的控制对象的数量,毕竟,有的类其内部实现复杂,如果频繁创建销毁对象,可能还是很耗费服务器资源的。
2.全局访问
单例模式的特点是单例类自己持有这个单例对象,并且提供一个静态方法可在全局获取到这个单例对象。
如果没有单例模式的情况下,我们一般是在代码A处创建这个对象,在代码B处如果也要使用这个对象,就需要将这个对象进行参数传递。为了避免传来传去,我们可能会写个Holder类,把这个对象放在Holder的成员变量中。
而单例模式的这个优点是,我们可以避免这样的困扰,直接从单例类中获取。
三、单例模式的变种
上面介绍的是单例模式的一种基本写法,实际我们还可以对其进行优化和变种。
1. 饿汉式
基本写法中,对象的创建是直接写在Singleton类的成员属性上的,因此当Singleton类被加载时,就会立即创建Singleton对象,这个写法比较简单,但我们可能并不会马上使用到这个Singleton对象,过早的创建会造成内存资源浪费。
这种一加载类就急于创建对象的写法,我们称之为饿汉式。
如果对内存资源不在意,那么其实饿汉式这个写法也就没什么大的缺点,而且写起来还简单,还是可以用的。
2. 懒汉式(线程不安全)
此变种仅是介绍,不要使用。
既然饿汉式在类加载时就创建对象会造成内存浪费,那么我们把创建对象这个步骤挪到要用时再创建不就好了?
我们要使用对象时,都是通过getInstance
方法先获取对象,我们可以在getInstance
方法中完成对象创建。
这种需要时再创建的写法,我们称之为懒汉式
示例代码:
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
分析懒汉式(线程不安全)写法的特点:
- 创建对象的时机修改为了在getInstance内部,需要时再创建,这可以节约系统资源
- getInstance方法在多个线程并发调用时,有可能会出现创建了多个实例,所以这算是一个不好的单例变种示范
饿汉式没有多线程并发问题吗?
确实没有,因为饿汉式是在类加载时进行创建对象,类加载classloader是单线程的,不存在这个问题。
3. 懒汉式(线程安全)
此变种仅是介绍,不要使用。
懒汉式(线程不安全)有可能存在并发问题,导致创建多个实例,那么我们给他加上锁不就好了吗?
示例代码:
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
分析懒汉式写法的特点:
由于调用getInstance时如果instance为null会创建对象,如果多个线程同时调用getInstance方法,有可能出现同步问题导致创建多个实例,所以getInstance方法使用了synchronized加锁来保障并发情况下也只会创建一个实例,不过synchronized的粒度较大,如果每次请求都经过getInstance方法,性能影响较大。
4. 双检锁/双重校验锁(DCL,double-checked locking)
懒汉式(线程安全)已经可以达到节省资源的目的,也达到了线程安全的目的,但是使用synchronized加锁对性能有较大影响,双检锁的方式,则是把锁的粒度尽可能降低,减少加锁对性能的影响。
示例代码:
public class Singleton {
private volatile static Singleton instance;
private Singleton () {}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return singleton;
}
}
分析双检锁的写法:
- 在成员属性instance上,我们增加了volatile关键字,保障多线程对instance值的可见性以及禁止指令重排。
- 通过双重检查的方式,在内部再进行synchronized加锁,可以降低锁的粒度,有效避免每次调用getInstance都加锁,因为getInstance在创建对象之后,instance一直都是非null的。
双检锁这个方式,既可以保障不浪费资源,又可以保障在多线程的环境下保持高性能。
如果大家自行编写单例类,追求节约资源和高性能,可以使用这种写法,但据《Java并发编程实践》提到不赞成这个写法,推荐静态内部类的方式(这一点我尚未验证)。
5. 静态内部类
这个变种,可以达到和双检锁一样的效果,并且写起来更加简单,推荐使用。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton () {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
分析一下静态内部类的特点:
将instance放在了内部类SingletonHolder中,前面我们提到饿汉式是类加载时就会立即创建对象,而静态内部类不会,它只会在调用了getInstance时,才会加载内部类SingletonHolder,此时才会创建对象。
6. 枚举
这个方式,这里仅是从网上摘抄,据说是很好,但是没有试过,工作中也很少见。
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。
它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
7. 登记式
如果熟悉我们封装的工具包Toolbox,就会知道工具包内提供了一个登记式单例工具类Singleton。
单例模式是一种非常常用的设计模式,但以上介绍的各种方法,都需要为每个单例类编写一些模板式的代码,为了简化,我们可以使用Singleton工具类。
// 获取单例对象
// Student类必须要具备无参构造方法
// 每个类在一个进程中只能获得一个单例对象
Student student = Singleton.get(Student.class);
// 移除单例对象
Singleton.remove(Student.class);
// 清空所有单例对象
Singleton.clear();
// 单例对象数量
int size = Singleton.size();
其实他就是很像是spring容器。
Singleton.java:
/**
* 单例工具
* @author Unicorn
*/
public final class Singleton {
/**
* 对象池
*/
private static Map<String, Object> pool = new ConcurrentHashMap();
private Singleton() {}
public static <T> T get(Class<T> clazz) {
Assert.notNull(clazz);
String key = clazz.getName();
T obj = (T) pool.get(key);
if (null == obj) {
synchronized(Singleton.class) {
obj = (T) pool.get(key);
if (null == obj) {
obj = ReflectUtil.newInstance(clazz);
pool.put(key, obj);
}
}
}
return obj;
}
/**
* 移除对象
* @param clazz
*/
public static void remove(Class clazz) {
if (null != clazz) {
String key = clazz.getName();
pool.remove(key);
}
}
/**
* 销毁,清空对象池
*/
public static void clear() {
pool.clear();
}
public static int size() {
return pool.size();
}
}
8. Spring容器
spring容器核心机制是IoC和DI,其本身也提供了单例对象的支持。
Java单例模式,看这一篇就够了的更多相关文章
- Java 集合看这一篇就够了
大家好,这里是<齐姐聊数据结构>系列之大集合. 话不多说,直接上图: Java 集合,也称作容器,主要是由两大接口 (Interface) 派生出来的: Collection 和 Map ...
- 想真正了解JAVA设计模式看着一篇就够了。 详解+代码实例
Java 设计模式 设计模式是对大家实际工作中写的各种代码进行高层次抽象的总结 设计模式分为 23 种经典的模式,根据用途我们又可以分为三大类.分别是创建型模式.结构型模式和行为型模式 列举几种设 ...
- Java NIO看这一篇就够了
原文链接:https://mp.weixin.qq.com/s/c9tkrokcDQR375kiwCeV9w? 现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技术,譬如Tomca ...
- Java注解 看这一篇就够了
注解 1.概念 注解:说明程序的.给计算机看的 注释:用文字描述程序的.给程序员看的 注解的定义:注解(Annotation),也叫元数据.一种代码级别的说明.它是JDK1.5及以后版本引入的一个特性 ...
- 关于Java多线程看这一篇就够了,从创建线程到线程池分析的明明白白
前言 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间).进程不依赖于线程而独立存在,一个进程中可以启动多个线程. 线程是指进程中的一个执行流程,一个进程中可 ...
- 【java编程】ServiceLoader使用看这一篇就够了
转载:https://www.jianshu.com/p/7601ba434ff4 想必大家多多少少听过spi,具体的解释我就不多说了.但是它具体是怎么实现的呢?它的原理是什么呢?下面我就围绕这两个问 ...
- Java中的多线程=你只要看这一篇就够了
如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个话其 ...
- 关于 Docker 镜像的操作,看完这篇就够啦 !(下)
紧接着上篇<关于 Docker 镜像的操作,看完这篇就够啦 !(上)>,奉上下篇 !!! 镜像作为 Docker 三大核心概念中最重要的一个关键词,它有很多操作,是您想学习容器技术不得不掌 ...
- JVM内存模型你只要看这一篇就够了
JVM内存模型你只要看这一篇就够了 我是一只孤傲的鱼鹰 让我们不厌其烦的从内存模型开始说起:作为一般人需要了解到的,JVM的内存区域可以被分为:线程栈,堆,静态方法区(实际上还有更多功能的区域,并且这 ...
- [转帖]nginx学习,看这一篇就够了:下载、安装。使用:正向代理、反向代理、负载均衡。常用命令和配置文件
nginx学习,看这一篇就够了:下载.安装.使用:正向代理.反向代理.负载均衡.常用命令和配置文件 2019-10-09 15:53:47 冯insist 阅读数 7285 文章标签: nginx学习 ...
随机推荐
- jQuery 查找父元素的函数 parent 和 parents 的区别
函数 描述 parent([expr]) 查找子节点childNode的父节点,不包括祖先节点 parents([expr]) 查找子节点childNode的父节点,包括祖先节点 Talk is ch ...
- C++ 对于函数名的操作,函数名本身和取*以及取&的区别
void TestFunc() { } int _tmain(int argc, _TCHAR* argv[]) { cout<<TestFunc<<endl; cout< ...
- OpenJudge 1.5.24 正常血压
24:正常血压 总时间限制: 1000ms 内存限制: 65536kB 描述 监护室每小时测量一次病人的血压,若收缩压在90 - 140之间并且舒张压在60 - 90之间(包含端点值)则称之为正常,现 ...
- 【AGC】使用云调试优惠扣费、华为设备上触发崩溃、无法下载华为应用市场问题小结
1.使用云调试剩余优惠时长还剩300分钟,但还会扣费的情况. 问题描述:用户的云调试账户显示剩余优惠时长还有300分钟,但在使用云调试的过程中,还是产生了扣费的情况. 解决方案: 当开发者成功 ...
- Windows优先使用IPv4
当前主流的Windows系统(从Windows 7之后)都会同时使用ipv6和ipv4,并且优先使用ipv6.当你ping另一个服务器的时候就能看到,优先使用的是ipv6进行通信.由于能够在DNS中解 ...
- [GWCTF 2019]我有一个数据库 WP
打开环境访问看到 提示我有一个数据库,但里面什么都没有,于是拿dirsearch跑了一下,没有出结果 但是有数据库嘛,那么试试常见的几个加上phpmyadmin 试试 于是看到了版本是4.8.1 拿到 ...
- MySQL集群搭建(6)-双主+keepalived高可用
双主 + keepalived 是一个比较简单的 MySQL 高可用架构,适用于中小 MySQL 集群,今天就说说怎么用 keepalived 做 MySQL 的高可用. 1 概述 1.1 keepa ...
- vscode展示子文件夹
取消勾选设置-功能-compact Folders
- MySQL 主从同步延迟监控
MySQL5.7和8.0支持通过 replication_applier_status 表获同步延迟时间,当从库出现延迟后,该表中的字段 REMAINING_DELAY 记录延迟秒数,当没有延迟时,该 ...
- 【前端必会】NVM,管理你的node版本
介绍 用nvm管理node,可以随时修改node版本 使用 下载nvm https://github.com/coreybutler/nvm-windows/releases/tag/1.1.9 安装 ...