一、单例模式的介绍与定义
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
二、单例模式实现方式
- 饿汉模式
- 懒汉模式
1、恶汉模式单例
(1)、实现步骤
第一步:
构造方法的私有化,不允许外部直接创建对象。
第二步:
创建类的唯一实例,使用private static修饰
第三步:
提供一个用于获取实例的方法,使用public static修饰
(2)、饿汉模式的特点
在类加载的时候创建一个唯一实例,还没调用对象的时候就被创建了。
(3)、具体代码实现
/******************需要设置单例模式的类*********************/
/**
* 1、饿汉模式:在类加载的时候创建一个唯一实例,还没调用对象的时候就被创建了
*/
public class SingLeton {
/**
* 1、构造方法的私有化,不允许外部直接创建对象
*
*/
private SingLeton() {
}
/**
* 2、创建类的唯一实例,使用private static修饰
*/
private static SingLeton singLeton = new SingLeton();
/**
* 3、提供一个用于获取实例的方法,使用public static修饰
*/
public static SingLeton singLeton() {
return singLeton;
}
}
/******************客户端的调用*********************/
public class SingletonTest {
public static void main(String[] arg){
/***********饿汉模式*************/
SingLeton s1 = SingLeton.singLeton();
SingLeton s2 = SingLeton.singLeton();
if (s1 == s2) {
System.out.println("s1和s2是同一个实例");
}else {
System.out.println("s1和s2不是同一个实例");
}
}
}
/******************返回结果*********************/
s1和s2是同一个实例
2、懒汉模式单例
(1)、实现步骤
第一步:
构造方法的私有化,不允许外部直接创建对象。
第二步:
创建类的唯一实例,使用private static修饰,但是是声明类的实例,没有去实例化
第三步:
提供一个用于获取实例的方法,使用public static修饰。
(2)、懒汉模式的特点
在类调用运行的时候去判断是否创建对象了,如果没创建一个,也就是第一次会创建,后面就不会再创建了。
(3)、具体代码实现
/******************实现懒汉模式的类***********************/
/**
* 懒汉模式
* 区别:饿汉模式的特点,加载类时比较慢,但运行时获取对象的速度比较快,是线程安全的
* 懒汉模式的特点,加载类时比较快,但运行时获取对象的速度比较慢,是非线程安全的
*/
public class SingLeton2 {
/**
* 1、构造方法的私有化,不允许外部直接创建对象
*/
private SingLeton2() {
}
/**
* 2、创建类的唯一实例,使用private static修饰
*/
private static SingLeton2 singLeton2; //声明类的实例,没有去实例化
/**
* 3、提供一个用于获取实例的方法,使用public static修饰
*/
public static SingLeton2 getSingLeton2(){
if (singLeton2 == null) {
return new SingLeton2();
}
return singLeton2;
}
}
/******************客户端调用***********************/
public class SingletonTest {
public static void main(String[] arg){
/***********懒汉模式*************/
SingLeton2 s3 = SingLeton2.getSingLeton2();
SingLeton2 s4 = SingLeton2.getSingLeton2();
if (s3 == s4) {
System.out.println("s3和s4是同一个实例");
}else {
System.out.println("s3和s4不是同一个实例");
}
}
}
/******************返回结果***********************/
s3和s4是同一个实例
三、单例模式的使用场景和实际的问题
1、饿汉模式和懒汉模式的区别
二者的区别是创建实例的时机,饿汉式在应用启动时就创建了实例,饿汉式是线程安全的,是绝对单例的。懒汉式在对外提供的获取方法被调用时会实例化对象。 饿汉模式的特点,加载类时比较慢,但运行时获取对象的速度比较快,是线程安全的。 懒汉模式的特点,加载类时比较快,但运行时获取对象的速度比较慢,是非线程安全的。
2、单例模式的常用使用场景
- 配置文件的读取
- 工具类的封装
- 线程池的创建
- 缓存的创建
- 日志对象的创建
- spring容器注册bean的时候默认使用的就是单例模式,可以通过@Scope注解去指定选择
3、为什么说懒汉模式的单例是非线程安全的?怎么去解决线程安全问题
问题分析:
在代码断点调式过程中发现可得知,在多线程的情况下,有一定的概率会出现一个单例类会有多个实例的情况。
解决方案:
(1)、方案1----对实例化方法加锁
public static synchronized SingLeton2 getSingLeton2(){
if (singLeton2 == null) {
return new SingLeton2();
}
return singLeton2;
}
方案一缺点:
加锁可以确保实例话方法只能有一个线程执行,确保两个线程不会同时进入实例化方法。缺点是同步锁的加锁和解锁比较消耗资源,而且synched关键字修饰static方法时锁的是整个class,对性能会有影响。
(2)、方案2----double check+volatile方案
public static SingLeton2 getSingLeton2(){
if (singLeton2 == null) {
synchronized (SingLeton2.class){
if (singLeton2 == null){
/**
* 1、分配内存
* 2、初始化对象
* 3、设置SingLeton2类执行内存(将实例分配到堆内存中)
*/
singLeton2 = new SingLeton2();
}
}
}
return singLeton2;
}
double check方案可以实现线程安全,且降低内存消耗。代码中三行注释代表了new对象时底层进行了三步操作,由于JVM优化算法可能会指令重排,也就是第二步和第三步执行的顺序会互换。这样可能会出现第一个第一个线程出现了指令重排情况,还没来得及初始化对象,第二个线程就进行了对象获取,导致获取到的对象是null。
为避免这种情况的发生可以使用volatile关键字修饰静态类,禁止指令重排
private volatile static SingLeton2 singLeton2 = null; //声明类的实例,没有去实例化
(3)、方案3----也可以使用静态内部类的初始化延迟加载解决,静态内部类有初始化锁,达到线程安全的目的