一,单例模式
1.1 定义
定义:确保一个类最多只有一个实例,并提供一个全局访问点。
而单例模式又可以分为预加载模式和懒加载模式。
1.2 预加载模式
预加载模式(饿汉模式):就是在系统加载时,就将该单例对象加载到内存中了,不论该单例对象是否被使用。
【代码示例】:
public class PreloadSingleton {
public static PreloadSingleton instance = new PreloadSingleton();
//其他的类无法实例化单例类的对象
private PreloadSingleton() {
};
public static PreloadSingleton getInstance() {
return instance;
}
}
很明显,没有使用该单例对象但该对象就已经被加载到了内存中,可能会造成内存的浪费(如果这个对象未被使用)。
1.3 懒加载模式
懒加载模式(懒汉模式):单例对象只有在被用到时才会被创建,解决了预加载模式(饿汉模式)浪费内存的问题。
【代码示例】
public class Singleton {
// 程序启动时未创建实例
private static Singleton instance=null;
private Singleton(){
};
public static Singleton getInstance()
{
// 程序调用时才创建实例
if(instance==null)
{
instance=new Singleton();
}
return instance;
}
}
1.4 单例模式的线程安全问题
由上述可知:
(1)预加载模式(懒汉模式)中只有一条 return instance 语句,这保证了线程安全问,但是可能会浪费内存资源。
(2)懒加载模式(懒汉模式)不存在浪费资源问题,但是却无法保障线程安全问题。首先是 if() 和 内部执行的代码都不是原子操作,其次是 new Singleton() 无法保证执行顺序。
不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:
memory=allocate();//1:初始化内存空间
ctorInstance(memory);//2:初始化对象
instance=memory();//3:设置instance指向刚分配的内存地址
但是由于 jvm 的优化机制,可能会将 2,3 步骤的顺序给颠倒,这样就可能造成后面线程得到了一个什么信息都没有的对象进行使用,发生错误。
例如:A,B线程都进行对象的创建,此时A线程先创建对象,但由于 jvm 的优化机制,导致了 2,3 步骤发送交换(1,3,2),先分配的了内存地址,再初始化了对象,当A线程创建对象步骤执行到(1,3)时,B线程进行对象的创建,结果进来发现对象已经被创建好了(instance 被线程A赋予了地址,此时已经不为null但是内部的信息还未被初始化)就直接拿去使用,此时,线程B将会访问到一个还未初始化的对象(线程不安全)。
1.5 懒加载线程安全问题解决办法
我们首先想到的就是使用synchronized关键字。synchronized加载getInstace()函数上确实保证了线程的安全。
【代码示例】
public class Singleton {
private static Singleton instance = null;
private Singleton() {
};
// 1.将锁加在方法上
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// 2.将锁加在代码片段上
// 减小锁的粒度,在代码还有其他业务情况下,能够起到优化程序性能的作用
//public static Singleton getInstance() {
// synchronized(Singleton.class) {
// if (instance == null) {
// instance = new Singleton();
// }
// }
// return instance;
//}
}
但是上述代码有一个问题(限于将锁加在代码片段上这一方式上),如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。所以我们把sychronized外面在套一层if(instance==null)判断语句,保证instance未实例化的时候才加锁,解决是否对象被实力成功都来竞争锁的问题。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
};
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
经过前面的讨论知道new一个对象的代码是无法保证顺序性的(指令重排序),因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
};
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
到此,我们就保证了懒加载的线程安全。
二,小结
单例模式的实现方法有两种:
- 预加载模式(饿汉模式):就是在系统加载时,就将该单例对象加载到内存中了,不论该单例对象是否被使用。
优点:线程安全。
缺点:可能会浪费内存空间。 - 懒加载模式(懒汉模式):单例对象只有在被用到时才会被创建加载到内存中。
优点:解决了预加载模式浪费内存空间问题。
缺点:线程不安全。 - 懒加载线程不安全的解决实现。
1)加锁(synchronized)保证线程线程安全。
2)双重 if() 判断减少锁竞争问题,优化性能(加锁操作是需要消耗程序资源的);