单例模式
概念
单例模式属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)
单例模式下必须构造方法为私有其他类获取实例必须通过 getInstance方法调用
优点
- 提供了对唯一实例的受控访问
- 由于系统中内存只存在一个对象,因此可以节约系统的的资源,对于一些频繁的创建和销毁的对象单例模式无疑可以提高系统的性能
- 单例模式可以允许可变的数目的实例,使用单例模式进行扩展,使用控制单利对象相似的方法获取指定个数的实例,及解决了单利对象,共享过多,而有损性能的问题
缺点
- 由于单例模式,不是抽象的所以可扩展性比较差
- 职责过重,在一定程度上违背了单一职责
- 滥用单例将带来一些负面的问题,如为了节省资源将数据库连接池对象设计为单例模式,可能会导致共享连接池对象的程序过多未出而出现的连接池溢出,如果实例化对象长时间不用系统就会被认为垃圾对象被回收,这将导致对象状态丢失
使用场景
- 统计当前在线人数(网站计数器):用一个全局对象来记录。
- 打印机(设备管理器):当有两台打印机,在输出同一个文件的时候只一台打印机进行输出。
- 数据库连接池(控制资源):一般是采用单例模式,因为数据库连接是一种连接数据库资源,不易频繁创建和销毁。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率 的损耗还是非常昂贵的,因此用单例模式来维护,就可以大大降低这种损耗。
- 应用程序的日志(资源共享):一般日志内容是共享操作,需要在后面不断写入内容所以通常单例设计
饿汉模式
JAVA版本
package com.practice.singleton;
/**
* 饿汉模式
* 类加载到内存,实例化一个单例,jvm保证线程安全
* 简单实用,推荐使用
* 唯一缺点不管用到与否,类装载时就完成实例化
* */
public class Mrg01 {
private static final Mrg01 INSTANCE =new Mrg01();
private Mrg01(){};
public static Mrg01 getINSTANCE() {
return INSTANCE;
}
public static void main(String[] args) {
Mrg01 m1=Mrg01.getINSTANCE();
Mrg01 m2=Mrg01.getINSTANCE();
System.out.println(m1==m2);
}
}
JS版本
class Window {
//直接进行创建
private static instance: Window = new Window();
public static getInstance() {
return Window.instance;
}
}
//把Window做成单例
let w1 = Window.getInstance();
let w2 = Window.getInstance();
console.log(w1 === w2);
饿汉模式
* 类加载到内存,实例化一个单例,jvm保证线程安全
* 简单实用,推荐使用
* 唯一缺点不管用到与否,类装载时就完成实例化
懒汉模式
JAVA版本
package com.practice.singleton;
/**
* 懒汉模式
*
* 按需初始化,但是 线程不安全
* 可以通过synchornized 但是效率下降了
* */
public class Mrg02 {
private static volatile Mrg02 INSTANCE; // volatile 语句重排 如果不加volatile没有初始化 产生 instance
public Mrg02(){};
public synchronized static Mrg02 getINSTANCE() {
if(INSTANCE==null){
try{
Thread.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE =new Mrg02();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mrg02.getINSTANCE().hashCode());
}).start();
}
}
}
存在问题: 懒汉模式 按需初始化,但是 线程不安全 可以通过synchornized 但是效率下降了
package com.practice.singleton;
/**
* 懒汉模式
*
* 按需初始化,但是 线程不安全
* 可以通过synchornized 但是效率下降了
* */
public class Mrg02 {
private static volatile Mrg02 INSTANCE; // volatile 语句重排 如果不加volatile没有初始化 产生 instance
public Mrg02(){};
public synchronized static Mrg02 getINSTANCE() {
if(INSTANCE==null){
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE =new Mrg02();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mrg02.getINSTANCE().hashCode());
}).start();
}
}
}
JS版本
class Window {
// 存储单例
private static instance: Window;
public static getInstance() {
// 判断是否已经有单例了
if (!Window.instance) {
Window.instance = new Window();
}
//返回实例
return Window.instance;
}
}
//把Window做成单例
let w1 = Window.getInstance();
let w2 = Window.getInstance();
console.log(w1 === w2);//true
完整模式——双判断
package com.practice.singleton;
/**
* 完整模式
* 双判断
* */
public class Mrg03 {
private static Mrg03 INSTANCE;
public Mrg03(){};
public static Mrg03 getINSTANCE() {
if(INSTANCE==null){
synchronized(Mrg03.class){
if(INSTANCE==null){
try{
Thread.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE =new Mrg03();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mrg03.getINSTANCE().hashCode());
}).start();
}
}
}
完美模式——静态内部类
那INSTANCE在创建过程中又是如何保证线程安全的呢?
<clinit>: 在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行。
在《深入理解JAVA虚拟机》中,有这么一段话:
虚拟机会保证一个类的<clinit>方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>方法完毕。如果在一个类的<clinit>方法中有耗时很长的操作,就可能造成多个进程阻塞需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>方法后,其他线程唤醒之后不会再次进入<clinit>方法。同一个加载器下,一个类型只会初始化一次,在实际应用中,这种阻塞往往是很隐蔽的。故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
package com.practice.singleton;
/*
* 完美写法1
* 静态内部类不会在加载类的时候加载
* */
public class Mrg04 {
private static class Mrg04Holder{
private final static Mrg04 INSTANCE =new Mrg04();
}
public static Mrg04 getInstance(){
return Mrg04Holder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mrg04.getInstance().hashCode());
}).start();
}
}
}
线程安全:
虚拟机加载class只加载一次,所以Hodler 和instance也只加载一次
完美方法——枚举单例
java之父推荐在EffectiveJava中的写法
package com.practice.singleton;
/*
* 官方完美方法
* 可以防止反序列化 还可以解决线程同步问题
* enum 枚举类型只有一个实力所以不会有多个实力
* 且枚举类没有构造方法 所以没有反序列化问题
* */
public enum Mrg05 {
INSTANCE;
public void m(){
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Mrg04.getInstance().hashCode());
}).start();
}
}
}