[置顶] 单例模式 - 确定 N 先生的GirlFriend

确定唯一的对象 - 单例模式

一、GrilFriendClass 引言

    N 先生很快就要大学毕业,大学四年忙于学(网)业(游),也没有找个女朋友。每每回家,家人都催促他赶快谈一个,或者干脆说要直接给 N 先生介绍对象。
    有句话说什么样的年龄就该做什么样的事情,这句话放在当下还是很有道理的。可是 N 先生心中对成家立业是有自己的认识的,认为先立业而后成家,自己都没条件过平凡的日子怎么给另一半想要的生活呢。
    但是由于家里催的紧,N 先生就想到了租女友先应付家里的姑姑阿姨们。
---
    于是这一年每逢过节回家,N 先生都会带新对象回家,
    这年端午节,N 先生租安琪拉回家了,家里人都夸着说 N 的对象看着很乖巧,
    我们都知道这里 N 的对象就是指的安琪拉。
    同年中秋节,N 先生租孙尚香回家了,家里人都夸着说 N 的对象像个大小姐。
    我们都知道这里 N 的对象就是指的孙尚香。
    又到国庆节,N 先生租蔡文姬回家了,家里人都夸着说 N 的对象十分卡哇伊。
    我们都知道这里 N 的对象就是指的蔡文姬。
---

    这一年来,家人每见到 N 先生时都会夸 N 的对象,但每次夸的对象都不一样,这里不是同一个对象。

    当全局系统每需要调用某个类 N 时,需要创建一个类 N 的新对象,即便创建的对象名称相同,也不是同一个对象。
   终于,毕业后的 N 先生在合适的年龄遇到了真爱不知火舞,确定了自己唯一的对象,这几趟回家,家里人提起 N 先生的对象时,都赞不绝口说大学生活不错。
    我们都知道,毕业后大家提到的 N 的对象都是不知火舞。这里是同一个对象。
    在软件工程单例模式思想出来之前,每当开发者使用某个类时都会新建一个对象实例使用,优秀的开发者会在使用结束后释放掉对象占用的资源,但一般开发过程中很少在意这一点。于是这些没被释放资源的对象就成为了Java 虚拟机里的垃圾。单例模式就是保证了系统全局只存在唯一的一个实例对象。确定了唯一对象的 N 先生,也代表了毕业前后 N 先生更加的成熟。

二、GrilFriendSingleton 实例演示

2-1 GrilFriendUnSingleton 非单例模式实例

    在单例模式之前,我们会这样写一个Class ,写基本属性和类的相关方法,及构造函数或者带参数的构造函数。下面以默认构造方法为例代码演示
package pers.niaonao.entity;

/**
 * @author niaonao
 * 非单例模式下的基本类
 */
public class GrilFriendUnSingleton {

	//默认的构造方法
	public GrilFriendUnSingleton() {
	}
	
	private String info = "我是你的新 GrilFriend";
	
	/**
	 * 获取对象信息的方法
	 * @return
	 */
	public String getGrilFriend() {
		return info;
	}
}
    通过新建该类的实体对象来调用相关方法
package pers.niaonao.test;

import pers.niaonao.entity.GrilFriendUnSingleton;

/**
 * @author niaonao
 * 
 */
public class TestGrilFriend {

	public static void main(String[] args) {
		
		//对实体类创建两个对象
		GrilFriendUnSingleton n_first_grilfriend = new GrilFriendUnSingleton();
		GrilFriendUnSingleton n_next_grilfriend = new GrilFriendUnSingleton();
		
		String info = "n_first_grilfriend 和 n_next_grilfriend";
		
		//判断对象是否是同一个实例
		if (n_first_grilfriend != n_next_grilfriend) {
			info += "\n\t不是同一个实例,是两个不同的对象:"
					+ "\n\t魔法为我而存在," + n_first_grilfriend.getGrilFriend()
					+ "\n\t大小姐驾到," + n_next_grilfriend.getGrilFriend();
		} else {
			info += "\n\t是同一个实例,同一个对象:"
					+ "\n\t会用不知火流的烈焰烧死你的呦," + n_first_grilfriend.getGrilFriend()
					+ "\n\t会用不知火流的烈焰烧死你的呦," + n_next_grilfriend.getGrilFriend();
		}
		
		System.out.println(info);
	}
}
程序执行结果如下:
    我们在系统全局每需要使用该类时,都会在使用该类的地方通过构造方法新建类的实体对象。所以就会出现在系统全局内,存在多个当前类的对象,并且其他地方就使用一次就不使用了,新建了一个对象占用了一定的资源,但没有释放资源,这就造成资源的浪费,也是系统垃圾的一种。

2-2 GrilFriendBySingleton 单例模式实例

    使用单例模式去写一个类 GrilFriendBySingleton.java
package pers.niaonao.entity;

/**
 * @author niaonao
 * 单例模式下的基本类
 */
public class GrilFriendBySingleton {

	//初始化静态私有变量grilfriend
	private static GrilFriendBySingleton grilfriend = null;
	
	/**
	 * 构造单例模式基本方法getInstance()
	 * 调用该方法,会先判断对象grilfriend是否不为null,即对象是否已存在,
	 * 若实例对象已存在则不再创建新对象,否则创建一个新对象给变量grilfriend。
	 * 
	 * 保证当前类只存在一个实体对象。即单例模式
	 * @return grilfriend
	 */
	public static GrilFriendBySingleton getInstance() {

		if (null == grilfriend)
			grilfriend = new GrilFriendBySingleton();

		return grilfriend;
	}
	
	//默认的构造方法
	public GrilFriendBySingleton() {
	}
	
	private String info = "我是你的新 GrilFriend";
	
	/**
	 * 获取对象信息的方法
	 * @return
	 */
	public String getGrilFriend() {
		return info;
	}
}
    测试类 TestGrilFriend.java 来测试单例模式
package pers.niaonao.test;

import pers.niaonao.entity.GrilFriendBySingleton;

/**
 * @author niaonao
 * 
 */
public class TestGrilFriend {

	public static void main(String[] args) {
		
		//通过单例模式创建出两个实体常量
		GrilFriendBySingleton n_first_grilfriend = GrilFriendBySingleton.getInstance();
		GrilFriendBySingleton n_next_grilfriend = GrilFriendBySingleton.getInstance();
		
		String info = "n_first_grilfriend 和 n_next_grilfriend";
		
		//判断对象是否是同一个实例
		if (n_first_grilfriend != n_next_grilfriend) {
			info += "\n\t不是同一个实例,是两个不同的对象:"
					+ "\n\t魔法为我而存在," + n_first_grilfriend.getGrilFriend()
					+ "\n\t大小姐驾到," + n_next_grilfriend.getGrilFriend();
		} else {
			info += "\n\t是同一个实例,同一个对象:"
					+ "\n\t会用不知火流的烈焰烧死你的呦," + n_first_grilfriend.getGrilFriend()
					+ "\n\t会用不知火流的烈焰烧死你的呦," + n_next_grilfriend.getGrilFriend();
		}
		
		System.out.println(info);
	}
}
程序执行结果如下:
    即使对 GrilFriendBySingleton 创建了两个变量,但通过单例模式保证是同一个实例,即在内存中是存放在一个位置的一个对象。保证了系统全局内只存在唯一的一个实例。这就是单例模式

三、Singleton Pattern 单例模式

    通过前两部分,应该对单例模式有一定的理解及认识,并能运用单例在实际开发中。下面说一下单例模式的定义,意义。

3-1 定义

    Ensure a class only has one instance,and provide a global point of access to it.
    GOF 对单例模式的定义是,保证一个类只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。

3-2 类图


3-3 三要素

  • 必须保证一个类只能有一个实例
  • 必须是这个类自行创建这个实例
  • 必须自行向整个系统提供这实例

3-4.优缺点

使用单例模式必然要了解其优缺点。
  • 优点
客户端使用单例模式类的实例,只需要调用一个单一的方法即可生成唯一的实例,节约了资源。
  • 缺点
单例模式难以实现序列化 Serialization,所以采用单例模式的类很难被持久化,也不易通过网络传输,一般网络传输数据都会序列化对象相关的类。
单例模式采用静态方法 static,不支持面向对象的三大特性之一的继承,无法再继承结构extend 中使用。
如果分布式集群的环境下存在多个 Java 虚拟机,不易确定具体的哪个单例在运行。

3-5 应用场景

    一般会在Util 工具类中 service服务类中使用,部分entity 类中。
    在多线程之间,公用同一个资源或操作同一个对象,这时候就用单例模式;在系统全局都会使用到的,共享资源,全局变量,可以使用单例模式;之所以说部分 entity 实体类会使用单例模式,是因为在大规模系统中,考虑到性能节约对象的创建时间,会使用单例模式。

3-6 说明

    上面的GrilFriendBySingleton 实现中,我们通过getInstance() 保证只有一个实例存在,通过关键字 static 修饰getInstance() 方法,提供全局访问的方法。
    我们得通过GrilFriendBySingleton 提供的方法去获取单例,让类自己创建实例,不能通过构造方法new GrilFriendBySingleton()再去创建,这就违背了单例模式的创建初衷。

3-7 标准单例代码演示

    标准单例模式代码演示:
public class Singleton {
    private static Singleton uniqueInstance = null;
 
    private Singleton() {
       // Exists only to defeat instantiation.
    }
 
    public static Singleton getInstance() {
       if (uniqueInstance == null) {
           uniqueInstance = new Singleton();
       }
       return uniqueInstance;
    }
    // Other methods...
}

四、GrilFriend 在多线程下的线程安全

    N 先生与不知火舞确定了对象关系,这年春节,N 先生会带着火舞去拜访亲近的街坊,走走远方的亲戚。了解到大姑、二姑、三姨、四舅只有初一在家,按常理,这些长辈都应该见见 N 先生的对象。初一过去了,亲戚们都说见到了 N 先生的对象,但是亲戚们口中所说的N 先生的对象都是不知火舞吗?事实上初一那天不知火舞只去了大姑、二姑、三姨家。**高能预警**那么四舅家只是见到了 N 先生的对象,只不过不是不知火舞本人罢了。
    这就是一个多线程问题,当多个线程使用这个单例时,用上面经典单例模式的示例代码能保证单例吗?答案是不一定。就是说会存在多个线程通过静态方法访问这个实例时,不是同一个实例的情况。这就是线程安全问题。
    线程安全定义:若代码所在的进程有多个线程同时进行,而这些线程可能会同时运行这段代码;若每次运行的结果和单线程的结果是一样的,而且其他变量的值与预期是一样的,则称为线程安全。

4-1 经典单例模式下的线程安全

    为什么会有这种情况,请看下面的实例
    我们模拟多线程访问单例模式类,来测试多线程下经典单例代码的线程安全问题。

新建测试类TestGFriendSingle 测试GrilFriendBySingleton。

package pers.niaonao.test;

import pers.niaonao.entity.GrilFriendBySingleton;

/**
 * @author niaonao
 * 测试经典单例模式代码的线程安全问题
 * 
 */
public class TestGFriendSingle {  
    
    public static void main(String[] args) {   
          
    	Thread[] threads = new Thread[10];//模拟10 个线程
    	
        for(int i = 0 ; i < threads.length ; i++){  
        	threads[i] = new Thread() {
            	@Override
            	public void run() {
            		super.run();
            		//获取单例类的实例
            		System.out.print(GrilFriendBySingleton.getInstance().hashCode()+"\t");
            	}
            };  
        }  
          
        for (int j = 0; j < threads.length; j++) {  
        	threads[j].start();  //启动线程
        }  
    }  
}  
运行结果如下:
如果说ID 为'16125728' 的是不知火舞的话,那么一号、三号、十号线程获取的对象又是谁呢?反正不是不知火舞就对了。线程安全就是要保证多线程,高并发下线程获取单例模式的实例是唯一的。

4-2 双锁机制下单例模式的线程安全

    我们如何保证线程安全呢?单例模式当然能保证线程安全。他必须保证线程安全,我说的!算了,其实是文档规范里说的。
  • 通过线程相关的常用关键字synchronized ,修饰同步代码块;
  • 同时使用 双锁机制来优化单例模式就能解决这个问题。
    双锁检查机制(Double Check Locking)是什么???看了下面的示例代码你就明白了。
新建优化后的单例模式类GFriendSingleMultithread.java 如下:
package pers.niaonao.entity;

/**
 * @author niaonao
 * 线程安全的单例模式
 * Double Check Locking 双锁机制,对单例进行两层加锁
 * 使用使用了volatile关键字来保证其线程间的可见性
 * 同步代码块中使用二次检查,以保证其不被重复实例化
 */
public class GFriendSingleMultithread {
    volatile private static GFriendSingleMultithread grilfriend = null;  
      
    //默认的构造方法通过private 修饰为私有方法,该单例类就只允许通过getInstance() 获取创建实例。
    private GFriendSingleMultithread(){
    	
    }  
       
    public static GFriendSingleMultithread getInstance() {  
        try {    
        	
            if(null == grilfriend){		//锁一:第一层检查
                Thread.sleep(300);  	//创建实例之前可能会有一些准备性的耗时工作 (参考其他博主的说法)
                
                //synchronized 修饰,同步代码块,处理多线程的关键字
                synchronized (GFriendSingleMultithread.class) {  
                	
                    if(null == grilfriend){//锁二:第二层检查
                    	grilfriend = new GFriendSingleMultithread();  
                    }  
                }  
            }   
        } catch (InterruptedException e) {   
            e.printStackTrace();  
        }  
        return grilfriend;  
    }  
}
    我们再去模拟多个线程来测试获取单例模式的实例对象。
新建测试类TestMultithread.java 如下:
package pers.niaonao.test;

import pers.niaonao.entity.GFriendSingleMultithread;

/**
 * @author niaonao
 * 测试双锁机制下的单例模式
 * 
 */
public class TestMultithread {  
    
    public static void main(String[] args) {
        
    	Thread[] threads = new Thread[10];//模拟10 个线程
        for(int i = 0 ; i < threads.length ; i++){  
        	threads[i] = new Thread() {
            	@Override
            	public void run() {
            		super.run();
            		//获取单例类的实例
            		System.out.print(GFriendSingleMultithread.getInstance().hashCode()+"\t");
            	}
            };  
        }  
          
        for (int j = 0; j < threads.length; j++) {  
        	threads[j].start();  //启动线程
        }  
    }  
}  
运行结果如下:
通过结果可以看到我们解决了单例模式在多线程下的线程安全的问题。

4-3 说明

  • 第二层检查的作用:
    高并发下会出现两个线程都通过了第一层检查,即第一个if(null == grilfriend);此时若第一个线程先抢到锁new 了一个对象,释放锁,然后第二个线程再抢到了锁,不做第二层检查,即第二个if 判断,则会再new 一个的对象出来。这就违反了单例模式的理念。
  • 构造方法创建对象:
    我们的新建类一般会有默认的构造方法,即便我们写出构造方法一般是public 关键字修饰的。这就使得单例类虽然能够通过getInstance() 方法获取实例,并保证获取的是唯一的实例。但是通过构造方法我们仍可以new 出来类的对象。这就存在了第二个,第三个实例。这个问题的处理用private 关键字修饰构造方法即可解决。

4-4 标准线程安全的单例代码演示

public class Singleton {
	volatile private static Singleton uniqueInstance = null;

	private Singleton() {
		// Exists only to defeat instantiation.
	}

	public static Singleton getInstance() {

		try {

			if (uniqueInstance == null) {
				Thread.sleep(300);

				// synchronized 修饰,同步代码块,处理多线程的关键字
				synchronized (Singleton.class) {
					if (uniqueInstance == null) {
						uniqueInstance = new Singleton();
					}
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		return uniqueInstance;
	}
	// Other methods...
}
END

猜你喜欢

转载自blog.csdn.net/niaonao/article/details/79500198