单例模式: 作用:保证了系统中 该类只存在一个对象,节省了系统资源的浪费,对于一些想要频繁 创建和销毁的对象, 使用单例模式即可提升系统性能 注意:当实例化一个单例类时,必须使用相应的方法创建 而不是使用new, new构造化实例都是多例的 使用场景: 频繁创建销毁的对象,创建对象耗时过多或耗资源多(即重量级对象),但又经常用到的对象,工具类对象、频繁访问数据库或者文件对象(比如,数据源、session工厂) 实例: jdk中 的java.lang.Runtime 就是经典的单例模式 原理:采取一定方法,保证整个软件系统中,对某个类只存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法) 懒汉式是,只有用到这个用例的时候,再将他实例化,不会浪费,所以效率要高一些。 饿汉式则是程序只要开始,就将他实例化,到用到他的时候就省去了再实例的时间,所以速度和反应快。这是这俩的区别
常见的几种单例模式有:懒汉式,饿汉式,枚举,静态内部类实现等,而懒汉式最重要的是双重检测锁模式的懒汉式单例称为DCL懒汉式(下面的例子有对DCL式进行原理分析)
单例模式的UML类图
角色分析:
单例 (Singleton) 类声明了一个名为 getInstance
获取实例的静态方法来返回其所属类的一个相同实例。
单例的构造函数必须对客户端 (Client) 代码隐藏。 调用 获取实例
方法必须是获取单例对象的唯一方式。
下面进行举例
饿汉式写法一:
package com.creational.singleton;
/**
* @author jdw
* @date 2021/8/18-11:45
*/
//饿汉式--单例 一上来就new 对象占据空间
/**
* 1.构造器私有化(防止外部new)
* 2.类的内部创建对象
* 3.向外暴露一个静态的公共方法。
* 4.实现代码
*/
public class Hungry {
private Hungry() { //构造器私有,别人无法new 此对象,保证单例
}
private final static Hungry hungry = new Hungry(); //添加static保证实例
public static Hungry getInstance() { //获取实例
return hungry;
}
public static void main(String[] args) {
//测试
Hungry instance = Hungry.getInstance();
Hungry instance2 = Hungry.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
/**
* 优点:简单,类加载时候完成实例化。避免线程同步问题
* 缺点:类加载时候完成实例化,没达到lazy loading效果, 存在资源浪费
* 这种方式基于 classloder(类装载)机制 避免了多线程的同步 ,不过 instance 在类转载就实例化 ,导致类转载的情况很多
* 在单例模式中 大都是调用getInstance方法装载, 不能确定有其他方式 (或其他静态方法)导致类装载 这时候初始化 instance
* 没达到懒加载效果
*/
}
饿汉式写法二:
package com.creational.singleton;
/**
* @author jdw
* @date 2021/8/18-11:45
*/
//饿汉式--单例 一上来就new 对象占据空间
/**
* 1.构造器私有化(防止外部new)
* 2.类的内部创建对象
* 3.向外暴露一个静态的公共方法。
* 4.实现代码
*/
public class Hungry2 {
private Hungry2() { //构造器私有,别人无法new 此对象,保证单例
}
private static Hungry2 hungry; //final是定义常量 ,必须要初始化,不让创建无法赋值. 修饰方法不可重写,修饰类不可继承
static { //在静态代码块创建单例对象
hungry = new Hungry2();
}
public static Hungry2 getInstance() { //获取实例
return hungry;
}
public static void main(String[] args) {
//测试
Hungry2 instance = Hungry2.getInstance();
Hungry2 instance2 = Hungry2.getInstance();
System.out.println(instance == instance2);
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
/**
* 和上一种方法类似,将类的实例化放在了静态代码块中,只是在类装载时候,执行了静态代码块中的代码,初始化类的实例。优缺点一样。
*/
}
懒汉式写法一:
package com.creational.singleton;
/**
* @author jdw
* @date 2021/8/18-11:52
*/
//懒汉式-单例
//提供一个静态的共有方法,使用到该方法时候采取 创建实例
public class LazyMan {
private LazyMan(){ //构造器私有
System.out.println(Thread.currentThread().getName()+"--ok");
}
private volatile static LazyMan lazyMan;
//双重检测锁模式的 懒汉式单例 DCL(Double-Check) 懒汉式 两次判断 是否有被实例化
public static LazyMan getInstance(){
if(lazyMan==null){ //等于空说明没创建
synchronized (LazyMan.class){ //LazyMan 加锁 只有一个线程可以进入
if(lazyMan==null){
lazyMan = new LazyMan();
/**
* 问题 : 不是原子性操作
* 原理:
* new一个对象发生了:
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把对象指向这个空间
*期待执行顺序123
*
* 实际可能是: 123 或 132
* A线程执行132 先分配空间 ,把一个空对象指向这个空间,而还没执行 2 的时候
* B线程来了 ,
*
* 解决: 使用 volatile 关键字 保证 数据的 可见性 有序性 避免指令重排 不保证原子性
* volatile 加了 volatile 其他线程可以相互看到 变量 volatile变量可以被看作是一种 程度较轻的 synchronized
*/
}
}
}
return lazyMan;
}
//------------------单线程下 单例ok
//多线程并发 不安全
//原因:当A线程进入,lazyMan等于null的 进行new操作,B线程也进入,A还没创建完成 lazyMan 依然是 null 然后B也创建 就有两个实例了不满足单例模型
//解决; 双重检测锁模式
//
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(LazyMan.getInstance());
}).start();
}
}
/**
* 优点:实现了懒加载,保证线程 安全
* *Double-Check 多线程常用方法 ,进行两次if 检测 保证线程安全
* 实例化代码只需要执行一次 ,后面在访问 通过 if 不为空 直接 返回第一次创建 的 实例化,避免反复同步操作
* 线程安全:延迟加载(用时创建),效率高
* 推荐
*/
}
懒汉式写法二
package com.creational.singleton;
/**
* @author jdw
* @date 2021/8/18-11:52
*/
//懒汉式-单例
//提供一个静态的共有方法,使用到该方法时候采取 创建实例
public class LazyMan2 {
private LazyMan2(){ //构造器私有
System.out.println(Thread.currentThread().getName()+"--ok");
}
private volatile static LazyMan2 lazyMan;
//双重检测锁模式的 懒汉式单例 DCL 懒汉式
public static synchronized LazyMan2 getInstance(){
if(lazyMan==null){ //等于空说明没创建
lazyMan = new LazyMan2();
}
return lazyMan;
}
//------------------单线程下 单例ok
//多线程并发 不安全
//原因:当A线程进入,lazyMan等于null的 进行new操作,B线程也进入,A还没创建完成 lazyMan 依然是 null 然后B也创建 就有两个实例了不满足单例模型
//解决; 需要加锁
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(LazyMan2.getInstance());
}).start();
}
}
/**
* 优点:实现了懒加载,
* 缺点:效率太低了,每个线程想要获得类的实例化时,在执行getInstance方法 都要进行同步操作。
* 而实际上,这个方法执行一次实例化就够了,后面的想获得实例化,直接return 就行了。
* 而在多线程环境下依然存在线程不安全问题。在实际开发中 不推荐
*/
}
使用静态内部类实现单例
package com.creational.singleton;
/**
* @author jdw
* @date 2021/8/18-17:29
*/
//静态内部类实现 单例线程安全 懒加载
public class StaticInClass {
//原理:当外部类 StaticInClass 在被加载时候 内部类 SingletonInstance 不会加载
//只有调用了内部类 里面 的方法 或者 想要使用里面的 属性 才会被加载
private static class SingletonInstance { //静态内部类
private static final StaticInClass instance = new StaticInClass();
}
//提供一个 静态共有 方法 ,直接返回 实例
public static StaticInClass getInstance() { //加载静态类是 线程安全的 不想要加 锁
return SingletonInstance.instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(StaticInClass.getInstance());
}).start();
}
}
/**
* 优点:实现了懒加载,保证线程安全 效率高 推荐
* 采用类的装载机制来保证实例化时 只有一个线程
* 类的静态属性只会在 第一次加载类时候初始化 , jvm(java虚拟机) 帮助我们保证了线程安全,在类的初始化时,别的线程无法进入
*
*/
}
枚举
package com.creational.singleton;
/**
* @author jdw
* @date 2021/8/18-17:46
*/
public class Enumerate {
enum Singleton{ //定义枚举实现单例
instance; //属性
public void sayOK(){
System.out.println("OK~");
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(Singleton.instance.hashCode());
}).start();
}
}
/**
* 优点: 利用jdk1.5添加的枚举 实现单例模式。避免了多线程同步问题,而且还可以防止序列化 重新创建新的 对象
* 推荐
*/
}
总结:
-
在类中添加一个私有静态成员变量用于保存单例实例。
-
声明一个公有静态构建方法用于获取单例实例。
-
在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
-
将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
-
检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。