目录
0.对象性能模式
- 面向对象很好地解决了“抽象”的问题,但是必不可免地要付出一定的代价,对于通常情况来讲,面向对象的成本大都可以忽略不计,但是某些情况,面向对象所带来的成本必须谨慎处理
- 典型模式
- 单例模式
- 享元模式
1.基本介绍
- 简单来说,就是采用一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
- 单例模式有8种写法:
- 1.饿汉式(静态常量)
- 2.饿汉式(静态代码块)
- 3.懒汉式(线程不安全)
- 4.懒汉式(线程安全,同步方法)
- 5.懒汉式(线程安全,同步代码块)
- 6.双重检查
- 7.静态内部类
- 8.枚举
2.八种写法解读
2.1 饿汉式(静态常量)
(1)创建步骤
- 1.构造函数私有化
- 2.类的内部创建对象
- 3.向外暴露一个静态的公共方法:getInstance
(2)代码实现
public class Singleton01 {
public static void main(String[] args){
Singleton instance = Singleton.getInstance();
Singleton instance01 = Singleton.getInstance();
System.out.println(instance == instance01);
System.out.println("Instance.hashCode="+instance.hashCode());
System.out.println("Instance.hashCode1="+instance01.hashCode());
}
}
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.在本类内部创建对象实例(在类加载的时候就创建对象实例)
private final static Singleton instance = new Singleton();
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return instance;
}
}
分析此写法优缺点:
- 优点:
- 在类装载的时候就完成实例化,避免了线程同步问题
- 缺点:
- 在这种方式基于类加载机制避免了多线程的同步问题,不过,instance在类加载时就实例化,在单例模式中大多数都是会调用getinstance方法,但是导致类加载的原因有很多种,因此不能确定是否有其他的方式(或其他的静态方法)导致类加载,这时候初始化instance就没有达到懒加载的效果,造成内存的浪费
- 结论:
- 这种单例模式可用,可能造成内存浪费
2.2 饿汉式(静态代码块)
代码实现:
public class Singleton02 {
public static void main(String[] args){
Singleton instance = Singleton.getInstance();
Singleton instance01 = Singleton.getInstance();
System.out.println(instance == instance01);
System.out.println("Instance.hashCode="+instance.hashCode());
System.out.println("Instance.hashCode1="+instance01.hashCode());
}
}
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.在本类内部创建对象实例(在类加载的时候就创建对象实例)
private static Singleton instance;
static { //在静态代码块中,创建单例对象
instance = new Singleton();
}
//3.提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return instance;
}
}
分析优缺点:
- 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类加载的时候,就执行静态代码块中的代码,初始化类的实例,优缺点和上面是一样的
结论:
- 这种单例模式可用,但是可能造成内存浪费
2.3 懒汉式(线程不安全)
代码实现:
package cn.cqu.Singleton.type1;
public class Singleton03 {
public static void main(String[] args){
System.out.println("懒汉式1:线程不安全");
Singleton instance = Singleton.getInstance();
Singleton instance01 = Singleton.getInstance();
System.out.println(instance == instance01);
System.out.println("Instance.hashCode="+instance.hashCode());
System.out.println("Instance.hashCode1="+instance01.hashCode());
}
}
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.在本类内部创建对象实例
private static Singleton instance;
//3.提供一个公有的静态方法,返回实例对象
// 当使用到该方法时,才实例化对象
public static Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
分析优缺点:
优点:
- 起到了懒加载的效果
缺点:
- 线程不安全,只能在单线程下使用
- 如果在多线程下,一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可使用这种方式
结论:
- 在实际开发中,不要使用这种方式
2.4 懒汉式(线程安全,同步方法)
代码实现:
public class Singleton04 {
public static void main(String[] args){
System.out.println("懒汉式2:线程安全,同步方法");
Singleton instance = Singleton.getInstance();
Singleton instance01 = Singleton.getInstance();
System.out.println(instance == instance01);
System.out.println("Instance.hashCode="+instance.hashCode());
System.out.println("Instance.hashCode1="+instance01.hashCode());
}
}
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.在本类内部创建对象实例
private static Singleton instance;
//3.提供一个公有的静态方法,返回实例对象,并且为该方法加入了同步处理的代码
// 当使用到该方法时,才实例化对象
public static synchronized Singleton getInstance(){
if(null == instance){
instance = new Singleton();
}
return instance;
}
}
分析优缺点:
优点:
- 解决了线程不安全问题
缺点:
- 效率太低,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步,而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了,方法进行同步效率太低
结论:
- 在实际开发中,不推荐使用这种方式
2.5 懒汉式(线程不安全,同步代码块)
代码实现:
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.在本类内部创建对象实例
private static Singleton instance;
//3.提供一个公有的静态方法,返回实例对象
// 当使用到该方法时,才实例化对象
public static Singleton getInstance(){
if(null == instance){
synchronized(Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
缺点:
- 这种方式,本意是想对第四种方式实现方式的改进,因为前面同步方法效率太低改为同步产生实例化的代码块
- 但是这种同步并不能起到线程同步的作用,跟第三种方式遇到的情形一致,假如一个线程进入了还未来得及往下执行,另一个线程也通过这个判断语句,这时便会产生多个实例
结论:
- 在实际开发中,不能使用这种方式
2.6.双重检查+volatile
代码实现:
public class Singleton06 {
public static void main(String[] args){
System.out.println("懒汉式4:线程安全,双重检查");
Singleton instance = Singleton.getInstance();
Singleton instance01 = Singleton.getInstance();
System.out.println(instance == instance01);
System.out.println("Instance.hashCode="+instance.hashCode());
System.out.println("Instance.hashCode1="+instance01.hashCode());
}
}
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.在本类内部创建对象实例
private static volatile Singleton instance;
//3.提供一个公有的静态方法,加入双重检查的代码,返回实例对象
// 当使用到该方法时,才实例化对象
public static Singleton getInstance(){
if(null == instance){
synchronized(Singleton.class){
if(null == instance){
instance = new Singleton();
}
}
}
return instance;
}
}
分析:
- 双检查机制是多线程开发中经常使用到的,如代码中所示,我们进行了两次的检查,这样可以保证线程安全了
- 当第一次判断不通过(即instance已经被实例化过)的时候,线程直接跳到return语句,避免了反复进行方法同步,保证了效率
- 当通过第一次判断进入同步代码块中的第二次判断时,这时候再次检查保证了,多个都通过第一次检查的线程不会重复创建多个实例,解决了线程安全的问题
- 线程安全,懒加载,效率高
结论:
- 在实际开发中推荐使用
注意:
- 双检查必须将实例引用instance定义为volatile才是线程安全的,下面是仅有双检查锁,无volatile的分析:
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.在本类内部创建对象实例
private static Singleton instance;
//3.提供一个公有的静态方法,加入双重检查的代码,返回实例对象
// 当使用到该方法时,才实例化对象
public static Singleton getInstance(){
if(null == instance){
synchronized(Singleton.class){
if(null == instance){
instance = new Singleton();
}
}
}
return instance;
}
}
首先此种使用双检查锁,不使用volatile的写法有问题——内存读写指令重排序(reorder)不安全(关于指令重排序的解释,见我的另一篇博客:volatile详解)
原因解释:
- 某一个线程执行到第一次检测时,读取到的instance不为null时,instance的引用对象可能没有完成初始化
- instance = new Singleton();在底层执行时可以分为以下三个步骤(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.为该内存空间初始化对象
instance = memory; //3.将对象引用指向刚分配并初始化对象的内存
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的,所以可能出现以下优化顺序:
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.将对象引用指向刚分配并初始化对象的内存
instance(memory); //2.为该内存空间初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关系多线程间的语义一致性
- 所以当一条线程正在new对象的时候,但它的底层按照上述1,3,2的顺序执行,
- 当执行完步骤3时,这时CPU调度另一个线程来创建对象,
- 此时访问instance不为null,直接返回该instance,
- 但是实际该instance实例并未完成初始化,此时如果被返回直接使用,就会造成异常,即造成了线程安全问题
而volatile能禁止指令重排序,所以使用volatile可以保证线程安全,这种正确的写法是双检查锁+volatile
2.7 静态内部类
代码实现:
public class Singleton07 {
public static void main(String[] args){
System.out.println("懒汉式5:线程安全,静态内部类");
Singleton instance = Singleton.getInstance();
Singleton instance01 = Singleton.getInstance();
System.out.println(instance == instance01);
System.out.println("Instance.hashCode="+instance.hashCode());
System.out.println("Instance.hashCode1="+instance01.hashCode());
}
}
class Singleton {
//1.构造器私有化,以保证外部不能new对象
private Singleton(){
}
//2.创建一个静态内部类,并且它的属性为Singleton实例
private static class SingletonInstance{
private static final Singleton instance = new Singleton();
}
//3.提供一个公有的静态方法,返回实例对象
// 当使用到该方法时,才加载SingletonInstance,进行实例化对象
public static Singleton getInstance(){
return SingletonInstance.instance;
}
}
分析:
静态内部类的特点说明:
- 1.上述的外部类Singleton被加载的时候,静态内部类SingletonInstance不会被加载
- 2.当我们使用getInstance方法时,使用到内部类SingletonInstance的属性instance时,该内部类才会被加载,并且在类加载时,线程是安全的(JVM底层的加载机制保证了线程安全,在类进行初始化的时候,别的线程是无法进入的)
通过静态内部类的特点,保证了线程安全,实现了懒加载,效率高
结论:
- 在实际开发中,推荐使用
2.8 枚举
代码实现:
public class Singleton08 {
public static void main(String[] args){
System.out.println("枚举方式实现");
Singleton instance = Singleton.INSTANCE;
Singleton instance01 = Singleton.INSTANCE;
System.out.println(instance == instance01);
System.out.println("Instance.hashCode="+instance.hashCode());
System.out.println("Instance.hashCode1="+instance01.hashCode());
instance.sayOK();
}
}
enum Singleton {
INSTANCE;//属性
public void sayOK(){
System.out.println("ok!");
}
}
分析:
- 1.这借助JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步的问题,而且还能防止反序列化重新创建新的对象
- 2.这种方式是Effective Java的作者提倡的方式
结论:
- 在实际开发中,推荐使用
3.单例模式在JDK源码中应用的分析
4.总结
- 1.单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
- 2.当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
- 3.单例模式使用的场景:
- 需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即,重量级对象),但又经常用到的对象、
- 工具类对象、
- 频繁访问数据库或文件的对象(比如数据源、session工厂等)