单例模式
定义:保证一个类在运行期间仅有一个实例,并提供一个访问它的全局访问点。
Singleton:负责创建Singleton类自己的唯一实例,并提供一个getInstance的方法,让外部来访问这个类的唯一实例。
//单例示例-----懒汉式
public class Singleton {
//定义一个变量来存储创建好的类实例
private static Singleton uniqueInstance = null;
//私有化构造方法,好在内部控制创建实例的数目
private Singleton() {}
//定义一个方法来为客户端提供类实例
public static synchronized Singleton getInstance() {
//判断存储实例的变量是否有值
if (uniqueInstance == null) {
//如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
uniqueInstance = new Singleton();
}
//如果有值,那就直接使用
return uniqueInstance;
}
//示意方法,单例可以有自己的操作
public void singletonOperation() {
//功能处理
}
//示意属性,单例可以有自己的属性
private String singletonData;
//示意方法,让外部通过这些方法来访问属性的值
public String getSingletonData() {
return singletonData;
}
}
//单例示例-----饿汉式
public class Singleton {
//定义一个变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
private static Singleton uniqueInstance = null;
//私有化构造方法,好在内部控制创建实例的数目
private Singleton() {}
//定义一个方法来为客户端提供类实例
public static Singleton getInstance() {
uniqueInstance = new Singleton();
}
//如果有值,那就直接使用
return uniqueInstance;
}
//示意方法,单例可以有自己的操作
public void singletonOperation() {
//功能处理
}
//示意属性,单例可以有自己的属性
private String singletonData;
//示意方法,让外部通过这些方法来访问属性的值
public String getSingletonData() {
return singletonData;
}
}
体会单例模式
读取配置文件的内容
现在要读取配置文件的内容,该如何实现呢?不用模式的解决方案
AppConfig.properties
paramA=a
paramB=b
//读取应用配置文件
public class AppConfig {
//用来存放配置文件中参数A的值
private String parameterA;
//用来存放配置文件中参数B的值
private String parameterB;
public String getParameterA() {return parameterA;}
public String getParameterB() {return parameterB;}
//构造函数
public AppConfig() {
//调用读取配置文件的方法
readConfig();
}
//读取配置文件,把配置文件中的内容读出来设置到属性上
private void readConfig() {
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfig.class.getResourceAsStream("AppConfig.properties");
p.load(in);
//把配置文件中的内容读出来设置到属性上
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建读取应用配置的对象
AppConfig config = new AppConfig();
String paramA = config.getParameterA();
String paramB = config.getParameterB();
System.out.println("paramA="+paramA+",paramB="+paramB);
}
}
存在严重问题:在一次运行期间,配置文件配好的是固定的,假如系统有多处需要得到paramA和parmaB,那么就需要多次读取配置文件,而这是没有必要的,大大降低了系统效率。也就是说在系统运行期间,系统中会存在很多个APPConfig的实例对象,这会严重浪费系统资源。
把上面的描述进一步抽象一下,问题就出来了:在一个系统运行期间,某个类只需要一个类实例就可以了,那么应该怎么实现呢?
使用单例模式的方案:
//饿汉式
//读取应用配置文件
public class AppConfig {
//定义一个变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
private static AppConfig instance = new AppConfig();
public static AppConfig getInstance() {
return instance;
}
//用来存放配置文件中参数A的值
private String parameterA;
//用来存放配置文件中参数B的值
private String parameterB;
public String getParameterA() {return parameterA;}
public String getParameterB() {return parameterB;}
//构造函数
private AppConfig() {
//调用读取配置文件的方法
readConfig();
}
//读取配置文件,把配置文件中的内容读出来设置到属性上
private void readConfig() {
Properties p = new Properties();
InputStream in = null;
try {
in = AppConfig.class.getResourceAsStream("AppConfig.properties");
p.load(in);
//把配置文件中的内容读出来设置到属性上
this.parameterA = p.getProperty("paramA");
this.parameterB = p.getProperty("paramB");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//客户端
public class Client {
public static void main(String[] args) {
//创建读取应用配置的对象
AppConfig config = AppConfig.getInstance();
String paramA = config.getParameterA();
String paramB = config.getParameterB();
System.out.println("paramA="+paramA+",paramB="+paramB);
}
}
单例模式的功能
1.单例模式的功能是用来保证这个类在运行期间只会被创建一个类实例,并提供一个全局唯一访问这个类实例的访问点。
2.单例模式的范围
是一个ClassLoader及其子ClassLoader的范围
3.单例模式的命名
一般建议单例模式的方法命名为:getInstance()
单例模式额名称:单例、单件、单体等等,翻译的不同,都是指的同一个模式
从时间空间上看:
懒汉式以时间换空间,每次getInstance的时候都要判断一下
饿汉式是以空间换时间的做法,不需要判断
从线程安全上看:
饿汉式安全,懒汉式不安全
理解单例模式
延迟加载的思想
通俗点说,就是一开始不要加载资源或者数据,一直等到马上就要使用这个资源或者数据了,躲不过去了才加载,所以也称为Lazy Load,不是懒惰啊,是延迟加载,这在实际开发中是一种很常见的思想,尽可能的节约资源。懒汉式就采用了这种思想
缓存的思想
单例模式的懒汉式实现还体现了缓存的思想,缓存也是实际开发中非常常见的功能。简单讲就是,如果某些资源或者数据会被频繁的使用,可以把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,那么就直接使用,如果没有那么就获取它,并设置到缓存中,下一次访问的时候就可以直接从内存中获取了。从而节省大量的时间,缓存是一种典型的时间换空间的方案。
//java中缓存的基本实现示例
public class JavaCache {
//缓存数据的容器,定义成Map是方便访问,直接根据key就可以获取Value了
//key选用String是为了简单,方便演示
private Map<String, Ojbect> map = new HashMap<String, Object>();
//从缓存中获取值
public Object getValue(String key) {
//先从缓存里面取值
Object obj = map.get(key);
//判断缓存里面是否有值
if (obj == null) {
//如果没有,那么就去获取相应的数据,比如读取数据库或者文件
//这里只是演示,所以直接写个假的值
obj = key+",value";
//把获取的值设置回到缓存里面
map.put(key, obj);
}
//如果有值了,就直接返回使用
return obj;
}
}
为了方便理解,再举几个具体应用,一个是项目中的,一个是算法题
public class Test {
public static void main(String[] args) {
List<String> v_subList = new List<String>();
v_subList.add("Proxy123");
v_subList.add("Proxy456");
v_subList.add("static123");
v_subList.add("static456");
v_subList.add("public123");
v_subList.add("public456");
Map<String, List<String>> map = m1(v_subList);
//业务逻辑的话,需要哪个组就可以获取到哪个组的所有节点名
List<String> ProxyList = map.get("Proxy");
//业务逻辑是分组处理业务的,所有直接遍历map就行了,map里面已经用list分好了
}
public static Map<String, List<String>> m1(List<String> v_subList) {
//这个map的key是组名,就是v_Group,value就是所有以v_Group开头的节点名,用list存储
Map<String, List<String>> map = new HashMap<String, List<String>>();
//遍历所有节点
for (String v_Name : v_subList) {
//提取当前节点的组名
String v_Group = v_Name.substring(0, v_Name.length() - 3);
//看看map中有没有对应的list
List<String> v_GroupList = map.get(v_Group);
//如果没有的话就初始化一个,并且把v_Name扔进list中
if (v_GroupList == null) {
v_GroupList = new ArrayList<String>();
v_GroupList.add(v_Name);
map.put(v_Group, v_GroupList);
} else {
//如果有直接扔到list中
v_GroupList.add(v_Name);
}
}
return map;
}
}
//问题描述:给定一字符串,相同的字符只保留第一次出现的,比如qwwewee 123321-------------qwe 123
public class Main {
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
String input = s.nextLine();
Map<Character, Integer> map = new HashMap<Character, Integer>();
for (int i=0; i<input.length(); i++) {
char c = input.charAt(i);
if (map.get(c) == null) {
map.put(c, i);
System.out.print(c);
}
}
}
}
实现缓存机制的步骤:
1.定义一个存放缓存数据的容器
2.从缓存中获取数据的做法
先从缓存里面取值
判断缓存里面是否有值
如果有值了,就直接使用这个值
如果没有,那么就去获取相应的数据,或者创建相应的对象
把获取的值设置回到缓存里面
web开发Scope->就是数据的缓存范围
//<jsp:useBean name="aa" class="com.yhc.AModel" scope="request">
//思路应该是差不多的
Object obj = request.getAttribute("aa");
AModel am = null;
if (obje == null) {
am = new AModel();
request.setAttribute("aa", am);
} else {
am = (AModel)obj;
}
使用缓存来模拟实现单例
public class Singleton {
//定义一个缺省的key值,用来标识在缓存中的存放
private final static String DEFAULT_KEY = "One";
//缓存实例的容器
private static Map<String, Singleton> = new HashMap<String, Singleton>();
private Singleton() {}
public static Singleton getInstance() {
//先从缓存中获取
Singleton instance = (Singleton) map.get(DEFAULT_KEY);
//如果没有,就新建一个,然后设置回缓存中
if (instance == null) {
instance = new Singleton();
map.put(DEFAULT_KEY, instance);
}
//如果有就直接使用
return instance;
}
}
单例模式的优缺点
1.时间和空间:懒汉式是典型的以时间换空间,饿汉式是典型的以空间换时间
2.线程安全:不加同步的懒汉式是线程不安全的,需要加锁synchronized
3.饿汉式是线程安全的,因为虚拟机保证了只装载一次
4.双重检查加锁
所谓双重检查枷锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在java1.4及以前版本中,很多jvm对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此本机制只能用在Java5及以上的版本。
public class Singleton {
//对保存实例的变量添加volatile的修饰
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
//先检查实例是否存在,如果不存在才进入下面的同步块
if (instance == null) {
//同步块,线程安全的创建实例
synchronized(Singleton.class) {
//再次检查实例是否存在,如果不存在才真的创建实例
if (instance == null) {
instance = new Singleton():
}
}
}
return instance;
}
}
理解单例模式
在Java中一种更好的单例实现方式
Lazy initialization holder class模式,这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙的同时实现了延迟加载和线程安全。
public class Singleton {
/**
*类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系
*而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder {
//静态初始化器,由Java来保证线程安全
private static Singleton instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
单例和枚举
按照《高效Java 第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。
/**
*使用枚举来实现单例模式的实例
*/
public enum Singleton {
uniqueInstance;
//示意方法,单例可以有自己的操作
public void singletonOperation() {
//功能处理
System.out.println("aa="+Singleton.uniqueInstance.hashCode());
}
}
/**
*客户端
*/
public class Client {
public static void main(String[] args) {
for (int i=0; i<3;i++) {
Singleton.uniqueInstance.singletonOperation();
}
}
}
思考单例模式
单例模式的本质:控制实例数目
/**
*简单演示如何扩展单例模式,控制实例数目为3个,此为线程不安全
*/
public class OneExtend {
//定义一个缺省的key值的前缀
private final static String DEFAULT_PREKEY = "Cache";
//缓存实例的容器,存在实例调度的问题
private static Map<String, OneExtend> map = new HashMap<String, OneExtend>();
//用来记录当前正在使用第几个实例,到了控制的最大数目,就返回从1开始
private static int num = 1;
//定义控制实例的最大数目
private final static int NUM_MAX = 3;
private OneExtend() {}
public static OneExtend getInstance() {
String key = DEFAULT_PREKEY+num;
OneExtend oneExtend = map.get(key);
if (oneExtend == null) {
oneExtend = new OneExtend();
map.put(key, oneExtend);
}
//把当前实例的序号加1
num++;
if (num > NUM_MAX) {num = 1;}
return oneExtend;
}
public static void main(String[] args) {
OneExtend t1 = getInstance();
OneExtend t2 = getInstance();
OneExtend t3 = getInstance();
OneExtend t4 = getInstance();
OneExtend t5 = getInstance();
OneExtend t6 = getInstance();
System.out.println("t1=="+t1);
System.out.println("t2=="+t2);
System.out.println("t3=="+t3);
System.out.println("t4=="+t4);
System.out.println("t5=="+t5);
System.out.println("t6=="+t6);
}
}
何时选用单例模式
当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题