目录
前言
补充其他知识:
1. Java中static的特征
2. 延迟加载
3. UML
4. volatile关键字
一、单例设计模式的介绍及相关内容
1.1. 介绍
1.2. 定义
1.3. 本质
二、单例模式的结构
三、单例模式的分类
3.1. 饿汉式
3.2. 懒汉式
四、单例模式的实现
4.1. 单例模式实现-饿汉式-方式1-使用静态变量方式
4.1.1. 说明
4.2. 单例模式实现-饿汉式-方式2-使用静态代码块方式
4.2.1. 说明
4.3. 单例模式实现-饿汉式-方式3-枚举方式
4.3.1. 说明
4.4. 单例模式实现-懒汉式-方式1-线程不安全
4.4.1. 说明
4.5. 单例模式实现-懒汉式-方式2-线程安全
4.5.1. 说明
4.6. 单例模式实现-懒汉式-方式3-双重检查锁
4.6.1. 说明
4.6.2 注意
4.7. 单例模式实现-懒汉式-方式4-静态内部类方式
4.7.1. 说明
五、JDK源码解析-Runtime类
提一个常见的问题;
在一个系统运行期间,某个类只需要一个类实例就可以了,那么应该怎样实现呢?
问题分析:
仔细分析该问题,现在一个类能够被创建多个实例,问题的根源在于类的构造方法是公开的,也就是可以让类的外部来通过构造方法创建多个实例。换句话说,只要类的构造方法能让类的外部访问,就没有办法去控制外部来创建这个类的实例个数。
要想控制一个类只被创建一个实例,那么首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法、而“把创建实例的权限收回来,让类自身来负责自己类实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法”,这就是单例模式的实现方式。
static变量在类装载的时候进行初始化。
多个实例的static变量会共享同一块内存区域。这就意味着,在Java中,static变量只会被初始化一次,就是在类装载的时候,而且多个实例都会共享这个内存空间。
延迟加载(Lazy Load)就是一开始不要加载资源或者数据,一直等,等到马上就要使用这个资源或者数据了,躲不过去了才加载。
统一建模语言(Unified Modeling Language,UML)是一种为面向对象系统的产品进行说明、可视化和编制文档的一种标准语言,是非专利的第三代建模和规约语言。UML是面向对象设计的建模工具,独立于任何具体程序设计语言。
关键字volatile:被 volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。
作用:
1.可以保证在多线程环境下共享变量的可见性
2.通过增加内存屏障防止多个指令之间的重排序
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
保证一个类仅有一个实例,并提供一个访问它的全局访问点
单例模式的本质:控制实例数目
单例模式的主要有以下结构:
- 单例类:只能创建一个实例的类
- 访问类:使用单例类
单例设计模式分类两种:
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
类加载就会导致该单实例对象被创建
饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
饿汉式是线程安全的
类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。
不加同步的懒汉式是线程不安全的
4.1.1. 说明
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。
如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
实现代码案例:
/** * 单例模式实现-饿汉式-方式1-使用静态变量方式 * 静态变量创建类的对象 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance = new Singleton(); //对外提供静态方法获取该对象 public static Singleton getInstance() { return instance; } }
测试:
/** * 测试 单例模式实现-饿汉式-方式1-使用静态变量方式 */ public class Client1 { public static void main(String[] args) { //创建Singleton类的对象 Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); //判断instance1 和 instance2是否为同一个对象 System.out.println(instance1 == instance2); //true,说明 instance1 和 instance2 是在同一个地址,是同一个对象 } }
4.2.1. 说明
该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,instance对象是随着类的加载而创建的。
如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。
实现代码案例:
/** * 单例模式实现-饿汉式-方式2-使用静态代码块方式 * 在静态代码块中创建该类对象 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance; static { instance = new Singleton(); } //对外提供静态方法获取该对象 public static Singleton getInstance() { return instance; } }
测试:
/** * 单例模式实现-饿汉式-方式2-使用静态变量方式 */ public class Client2 { public static void main(String[] args) { //创建Singleton类的对象 Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); //判断instance1 和 instance2是否为同一个对象 System.out.println(instance1 == instance2); //true,说明 instance1 和 instance2 是在同一个地址,是同一个对象 } }
4.3.1. 说明
枚举类实现单例模式是极力推荐的单例实现模式
因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中 唯一 一种不会被破坏的单例实现模式。
实现代码案例:
/** * 单例模式实现-饿汉式-方式3-枚举方式 * 枚举方式 */ public enum Singleton { INSTANCE; }
测试:
/** * 单例模式实现-饿汉式-方式3-枚举方式 * 枚举方式 */ public class Test5 { public static void main(String[] args) { Singleton instance = Singleton.INSTANCE; Singleton instance1 = Singleton.INSTANCE; System.out.println(instance == instance1); } }
4.4.1. 说明
该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作
当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。
但是,如果是多线程环境,会出现线程安全问题。
实现代码案例:
/** * 单例模式实现-懒汉式-方式1-线程不安全 * 线程不安全 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance; //对外提供静态方法获取该对象 public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
测试:
/** * 单例模式实现-懒汉式-方式1-线程不安全 */ public class Client3 { public static void main(String[] args) { //创建Singleton类的对象 Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); //判断instance1 和 instance2是否为同一个对象 System.out.println(instance1 == instance2); //true,说明 instance1 和 instance2 是在同一个地址,是同一个对象 } }
4.5.1. 说明
该方式也实现了懒加载效果,同时又解决了线程安全问题。
但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。
从代码可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
实现代码案例:
/** * 单例模式实现-懒汉式-方式2-线程安全 * 线程安全 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance; //对外提供静态方法获取该对象 public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
测试:
/** * 单例模式实现-懒汉式-方式2-线程安全 */ public class Client4 { public static void main(String[] args) { //创建Singleton类的对象 Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); //判断instance1 和 instance2是否为同一个对象 System.out.println(instance1 == instance2); //true,说明 instance1 和 instance2 是在同一个地址,是同一个对象 } }
4.6.1. 说明
讨论一下懒汉模式中加锁的问题,对于
getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以没必让每个线程必须持有锁才能调用该方法,需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
如何要解决双重检查锁模式带来空指针异常的问题,只需要使用
volatile
关键字,volatile
关键字可以保证可见性和有序性。添加
volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。、4.6.2 注意
在Java1 .4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java5及以上的版本。
实现代码案例:
/** * 单例模式实现-懒汉式-方式3-双重检查锁 * 双重检查方式 */ public class Singleton { //私有构造方法 private Singleton() {} private static volatile Singleton instance; /* 解决问题: 假如两个线程A、B,A执⾏了if (instance == null)语句,它会认为单例对象没有创建, 此时线程切到B也执⾏了同样的语句,B也认为单例对象没有创建, 然后两个线程依次执⾏同步代码块,并分别创建了⼀个单例对象。 */ //对外提供静态方法获取该对象 public static Singleton getInstance() { //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际 if(instance == null) { synchronized (Singleton.class) { //抢到锁之后再次判断是否为空 if(instance == null) { instance = new Singleton(); } } } return instance; } }
测试:
/** * 单例模式实现-懒汉式-方式3-双重检查锁 */ public class Client5 { public static void main(String[] args) { //创建Singleton类的对象 Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); //判断instance1 和 instance2是否为同一个对象 System.out.println(instance1 == instance2); //true,说明 instance1 和 instance2 是在同一个地址,是同一个对象 } }
4.7.1. 说明
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被
static
修饰,保证只被实例化一次,并且严格保证实例化顺序。第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
这个解决方案被称为Lazy initialization hold class模式,这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识很巧妙地同时实现了延迟加载和线程安全。
实现代码案例:
/** * 单例模式实现-懒汉式-方式4-静态内部类方式 * 静态内部类方式 */ public class Singleton { //私有构造方法 private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } //对外提供静态方法获取该对象 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
测试:
/** * 单例模式实现-懒汉式-方式4-静态内部类方式 */ public class Client6 { public static void main(String[] args) { //创建Singleton类的对象 Singleton instance1 = Singleton.getInstance(); Singleton instance2 = Singleton.getInstance(); //判断instance1 和 instance2是否为同一个对象 System.out.println(instance1 == instance2); //true,说明 instance1 和 instance2 是在同一个地址,是同一个对象 } }
Runtime类使用的是饿汉式(静态属性)方式来实现单例模式的
public class Runtime { private static Runtime currentRuntime = new Runtime(); /** * Returns the runtime object associated with the current Java application. * Most of the methods of class
Runtime
are instance * methods and must be invoked with respect to the current runtime object. * * @return theRuntime
object associated with the current * Java application. */ public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {} ... }