单例模式
设计模式类型
设计模式分为三种类型,共23种
- 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。
- 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
- 行为型模式:模版方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)。
基本介绍:
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于 创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类
只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
何时使用:
当您想控制实例数目,节省系统资源的时候
比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session
对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个
SessionFactory就够,这是就会使用到单例模式。
单例模式的8种实现方式:
方式 | 是否推荐 |
---|---|
1 饿汉式(静态常量) | 可用 |
2 饿汉式(静态代码块) | 可用 |
3 懒汉式(线程不安全) | 禁止 |
4 懒汉式(线程安全,同步方法) | 不推荐 |
5 懒汉式(线程安全,同步代码块) | 错误 |
6 双重检查 | 推荐 |
7 静态内部类 | 推荐 |
8 枚举 | 推荐 |
1.饿汉式(静态常量)
特点 | |
---|---|
是否 Lazy 初始化 | 否 |
是否多线程安全 | 是 |
实现难度 | 易 |
描述:
- 构造器私有化 (防止 new )
- 类的内部创建对象
- 向外暴露一个静态的公共方法。getInstance
- 代码实现
代码实现:
package 设计模式.单例模式;
/**
* 静态常亮
*
* @author 孙一鸣 on 2020/2/4
*/
public class 饿汉式1 {
public static void main(String[] args) {
Singleton instance=Singleton.getSingleton();
Singleton instance2=Singleton.getSingleton();
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton{
//1.构造器私有化
private Singleton() {
}
//2.类内部创建对象实例
//static修饰的属性强调它们只有一个,final修饰的属性表明是一个常数(创建后不能被修改)。static final修饰的属性表示一旦给值,就不可修改,并且可以通过类名访问。
private final static Singleton SINGLETON = new Singleton();
//3.提供一个共有的静态方法,返回实例
public static Singleton getSingleton(){
return SINGLETON;
}
}
优缺点说明:
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同
步问题。 - 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始
至终从未使用过这个实例,则会造成内存的浪费 - 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载
时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载
的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类
装载,这时候初始化instance就没有达到lazy loading的效果 - 结论:这种单例模式可用,可能造成内存浪费
代码分析:
本次饿汉式代码实现围绕着静态常亮四字来展开,说到静态常亮就得说说关键字
static 和 final
static:关键字可以用来修饰代码块表示静态代码块,修饰成员变量表示全局静态成员变量,修饰方法表示静态方法。
静态是相对于动态的,动态是指Java程序在JVM上运行时,JVM会根据程序的需要动态创建对象并存储对象(分配内存),对象使命结束后,对象会被垃圾回收器销毁,即内存回收由JVM统一管理并分配给其他新创建的对象;静态是指Java程序还没有运行时,JVM就会为加载的类分配空间存储,被static关键字修饰的内容;如静态成员变量,Java类加载到JVM中,JVM会把类以及类的静态成员变量存储在方法区,我们知道方法区是线程共享且很少发生GC的区域,所以被static关键字修饰的内容都是全局共享的,且只会为其分配一次存储空间。
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
JDK1.8以后 。永久代被移除,转换为元空间。
final: final修饰的属性表明是一个常数(创建后不能被修改)
2.饿汉式(静态代码块)
特点 | |
---|---|
是否 Lazy 初始化 | 否 |
是否多线程安全 | 是 |
实现难度 | 易 |
描述:
- 构造器私有化 (防止 new )
- 类的内部创建对象
- 向外暴露一个静态的公共方法。getInstance
- 代码实现
代码实现:
package 设计模式.单例模式;
/**
* 静态代码块
*
* @author 孙一鸣 on 2020/2/4
*/
public class 饿汉式2 {
public static void main(String[] args) {
Singleton2 instance=Singleton2.getSingleton();
Singleton2 instance2=Singleton2.getSingleton();
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton2{
//1.构造器私有化
private Singleton2() {
}
//2.类内部创建对象实例
//static修饰的属性强调它们只有一个,static 可以通过类名访问。
private static Singleton2 SINGLETON ;
static {
SINGLETON = new Singleton2();
}
//3.提供一个共有的静态方法,返回实例
public static Singleton2 getSingleton(){
return SINGLETON;
}
}
优缺点说明:
- 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
- 结论:这种单例模式可用,但是可能造成内存浪费
3.懒汉式(线程不安全)
特点 | |
---|---|
是否 Lazy 初始化 | 是 |
是否多线程安全 | 否 |
实现难度 | 易 |
代码实现:
package 设计模式.单例模式;
/**
* 线程不安全
*
* @author 孙一鸣 on 2020/2/4
*/
public class 懒汉式3 {
public static void main(String[] args) {
Singleton3 instance=Singleton3.getSingleton();
Singleton3 instance2=Singleton3.getSingleton();
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton3{
private static Singleton3 instance;
private Singleton3(){};
//提供一个静态的公有方法,使用到该方法时才去创建对象
public static Singleton3 getSingleton(){
if (instance == null){
instance = new Singleton3();
}
return instance;
}
}
优缺点说明:
- 起到了Lazy Loading的效果,但是只能在单线程下使用。
- 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及
往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以
在多线程环境下不可使用这种方式 - 结论:在实际开发中,不要使用这种方式
4.懒汉式(线程安全,同步方法)
特点 | |
---|---|
是否 Lazy 初始化 | 是 |
是否多线程安全 | 是 |
实现难度 | 易 |
代码实现:
package 设计模式.单例模式;
/**
* 线程安全
*
* @author 孙一鸣 on 2020/2/4
*/
public class 懒汉式4 {
public static void main(String[] args) {
Singleton4 instance=Singleton4.getSingleton();
Singleton4 instance2=Singleton4.getSingleton();
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton4{
private static Singleton4 instance;
private Singleton4(){};
//提供一个静态的公有方法,使用到该方法时才去创建对象
public synchronized static Singleton4 getSingleton(){
if (instance == null){
instance = new Singleton4();
}
return instance;
}
}
优缺点说明:
- 解决了线程不安全问题
- 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行
同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,
直接return就行了。方法进行同步效率太低 - 结论:在实际开发中,不推荐使用这种方式
5. 懒汉式(线程安全,同步代码块)–错误示例
代码实现:
package 设计模式.单例模式;
/**
* 同步代码块 错误案例
*
* @author 孙一鸣 on 2020/2/4
*/
public class 饿汉式5 {
public static void main(String[] args) {
Singleton5 instance=Singleton5.getSingleton();
Singleton5 instance2=Singleton5.getSingleton();
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton5{
private static Singleton5 instance;
private Singleton5(){};
//提供一个静态的公有方法,使用到该方法时才去创建对象
public static Singleton5 getSingleton(){
if (instance == null){
synchronized (Singleton.class){
instance = new Singleton5();
}
}
return instance;
}
}
缺点说明:
- 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,
改为同步产生实例化的的代码块 - 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例
- 结论:在实际开发中,不能使用这种方式
6 .双重检查
特点 | |
---|---|
是否 Lazy 初始化 | 是 |
是否多线程安全 | 是 |
实现难度 | 难 |
代码实现:
package 设计模式.单例模式;
/**
*
*
* @author 孙一鸣 on 2020/2/4
*/
public class 双重检查 {
public static void main(String[] args) {
Singleton6 instance=Singleton6.getSingleton();
Singleton6 instance2=Singleton6.getSingleton();
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton6{
private static volatile Singleton6 instance;
private Singleton6(){};
//提供一个静态的公有方法,使用到该方法时才去创建对象
public static Singleton6 getSingleton(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton6();
}
}
}
return instance;
}
}
优缺点说明:
- Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两
次if (singleton==null)检查,这样就可以保证线程安全了。 - 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),
直接return实例化对象,也避免的反复进行方法同步. - 线程安全;延迟加载;效率较高
- 结论:在实际开发中,推荐使用这种单例设计模式
代码分析:
双重检查使用了多线程中常用关键字 volatile
1.volatile是JAVa虚拟机提供的轻量级的同步机制,有三大特点:
1.1保证可见性 1.2 不保证原子性 1.3禁止指令重排
7.静态内部类
特点 | |
---|---|
是否 Lazy 初始化 | 是 |
是否多线程安全 | 是 |
实现难度 | 一般 |
代码实现:
package 设计模式.单例模式;
/**
* @author 孙一鸣 on 2020/2/4
*/
public class 静态内部类7 {
public static void main(String[] args) {
Singleton8 instance=Singleton8.getInstance();
Singleton8 instance2=Singleton8.getInstance();
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode="+ instance2.hashCode());
}
}
class Singleton8{
//构造器私有化
private Singleton8(){};
//写一个静态内部类,该类有静态属性Singleton
private static class SingletonInstance{
private static final Singleton8 SINGLETON_8= new Singleton8();
}
//提供静态公有方法
public static synchronized Singleton8 getInstance(){
return SingletonInstance.SINGLETON_8;
}
}
优缺点说明:
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化
时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的
实例化。 - 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们
保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。 - 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用.
代码分析:
静态内部类: 使用它的时候才会加载
静态变量,静态方法,静态块等都是类级别的属性,而不是单纯的对象属性。他们在类第一次被使用时被加载(记住,是一次使用,不一定是实例化)。我们可以简单得用 类名.变量 或者 类名.方法来调用它们。与调用没有被static 修饰过变量和方法不同的是:一般变量和方法是用当前对象的引用(即this)来调用的,静态的方法和变量则不需要。从一个角度上来说,它们是共享给所有对象的,不是一个角度私有。这点上,静态内部类也是一样的。
静态内部类的加载过程:
静态内部类的加载不需要依附外部类,在使用时才加载。不过在加载静态内部类的过程中也会加载外部类。以上花了很多功夫来说明了
8.枚举
特点 | |
---|---|
是否 Lazy 初始化 | 否 |
是否多线程安全 | 是 |
实现难度 | 易 |
代码实现:
package 设计模式.单例模式;
/**
* @author 孙一鸣 on 2020/2/4
*/
public class 枚举8 {
public static void main(String[] args) {
Singleton9 instance=Singleton9.INSTACNCE;
Singleton9 instance2=Singleton9.INSTACNCE;
System.out.println(instance == instance);
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode="+ instance2.hashCode());
instance.sayOK();
}
}
enum Singleton9{
INSTACNCE;
public void sayOK(){
System.out.println("ok~");
}
}
分析:
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。