1.单例模式概念
单例模式(Singleton Pattern)负责创建自己的对象,同时确保程序中该类只有一个对象被创建。
2.单例模式作用
确保程序中一个类最多只有一个实例,提供访问这个类的全局点。
3.优点和缺点
优点
1)在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2)避免对资源的多重占用(比如写文件操作)。
缺点
不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
4.注意事项
1)在JAVA中实现单例模式,需要一个私有的构造器,一个静态方法和一个静态变量。
2)双重检查加锁不适用于JDK1.4及以前的版本。
3)如果使用多个类加载器,可能导致单例失效,而产生多个实例。
4)如果使用JVM1.2或之前的版本,你必须建立单例注册表,以免垃圾收集器将单例回收。
5.例子解析
线程不安全,懒汉模式
/**
* @author Administrator
* 线程不安全
*/
public class Singleton {
//利用static来记录Singleton唯一的实例
private static Singleton singleton = null;
//将构造方法声明为私有,只有Singleton类内部才能调用构造器
private Singleton() {
}
//用getInstance方法实例化对象,并返回这个实例
public static Singleton getInstance()
{
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
线程安全,饿汉模式
这里的懒汉和饿汉主要指的是singleton 是在什么时候实例化
//这种在声明变量的时候就实例化的就是饿汉模式
private static Singleton singleton = new Singleton();
//这种要先判断singleton == null,就是懒汉模式
public static Singleton getInstance()
{
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
/**
* @author Administrator
* 使用synchronized实现了线程安全,但是效率不高,会造成程序执行效率降低
*/
public class Singleton {
//利用static来记录Singleton唯一的实例
private static Singleton singleton = new Singleton();
//将构造方法声明为私有,只有Singleton类内部才能调用构造器
private Singleton() {
}
//用getInstance方法实例化对象,并返回这个实例
//关键字synchronized实现线程安全
public static synchronized Singleton getInstance()
{
return singleton;
}
}
双重检查加锁
/**
* @author Administrator
* 双重检查加锁
* JDK大于1.4的版本,双重检查加锁才能生效
*/
public class Singleton {
//利用static来记录Singleton唯一的实例
//volatile确保singleton变量被初始化成为Singleton实例后,多个线程能够正确的处理singleton变量
private volatile static Singleton singleton = null;
//将构造方法声明为私有,只有Singleton类内部才能调用构造器
private Singleton() {
}
//用getInstance方法实例化对象,并返回这个实例
public static Singleton getInstance()
{
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
6.总结
1)volatile的作用,关于这个网上有很多资料,每个我都会先看了评论,然后觉得有一篇文章(文章链接会在参考文章里附上)讲得比较仔细,而且应该也没有什么大问题。它总结了volatile的作用:
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
<1>保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
<2>禁止进行指令重排序。
它里面有个例子,阐述了volatile的使用场景。首先,需要知道的是当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
举个例子,
int i = i+1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
当在多线程的情况下,假设有两个线程,i初始值是0。初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程一进行加1操作,然后把i的最新值1写入到内存。此时线程二的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程二把i的值写入内存,最终i的值是等于1,而不是2。这不是我们想要的结果。
那么这时候就可以用到volatile了。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
但是,我们还是需要明白,volatile只是保证两个线程都能从主内存中读取到最新的变量值,但是却不能保证线程安全。
通过上面的例子说一种可能发生的情况,假设当前的i在主内存中的值是3,当线程一和线程二同时读取主内存中的i的值进行操作的时候,它们拿到的都是i=3,然后进行各自的操作,加1后,两个线程得到的结果都是4,线程一会将结果4写回到主内存中,而线程二也会将结果4写到主内存中,这样就导致i的结果是4,而不是5。
2)JAVA主内存和工作内存
在查找关于volatile的资料的时候,涉及到了主内存和工作内存的概念,所以也在网上查找了一下。
主内存主要对应于java堆中对象的实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。
从更底层来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存。
这个概念没有认真验证是对得还是错的,可以搜索JAVA内存模型可以了解得更具体一点。
3)类加载器
在注意事项里提到了如果使用多个类加载器,可能导致单例失效,而产生多个实例。可以搜索JAVA自定义类加载器和查看
http://www.codeweblog.com/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E4%B8%8E%E5%8D%95%E4%BE%8B/
7.源码地址
http://download.csdn.net/detail/lgywsdy/9748795
8.参考文章
http://blog.csdn.net/sunxianghuang/article/details/51920794
http://www.cnblogs.com/dolphin0520/p/3920373.html
http://www.runoob.com/design-pattern/singleton-pattern.html