1. 饿汉模式——立即加载
public class MySingleObj {
private static MySingleObj mySingleObj = new MySingleObj();
public static MySingleObj getInstance() {
return mySingleObj;
}
// 将构造函数私有化,不让外界直接创建对象
private MySingleObj(){}
}
测试类: TestSingle.java
public class TestSingle {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(MySingleObj.getInstance());
}
};
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(runnable);
}
for (int i = 0; i < 100; i++) {
threads[i].start();
}
}
}
结论: 得到的始终是单例对象,饿汉模式可以直接用于多线程中
为什么饿汉模式多线程安全呢?
- 单例对象是类变量,随着类的加载而初始化,一次运行类只加载一次,类变量只会初始化一次,而且类加载前,先要获得Class对象锁,加载类和初始化类变量的过程中只允许一个线程进入过,故线程安全。
2. 懒汉模式——延迟加载
public class MySingleObj2 {
private static MySingleObj2 mySingleObj2;
private MySingleObj2(){}
public static MySingleObj2 getInstance() {
try {
if (mySingleObj2 == null) {
Thread.sleep(100); // 理解为创建对象前的初始化操作
mySingleObj2 = new MySingleObj2();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return mySingleObj2;
}
}
用TestSingle.java
进行测试,发现得到的不是单例对象,不多线程环境下不能保持单例状态,如何修正呢?
分析错误原因:
有多个线程同时进入到 判断语句 if (mySingleObj2 == null)
,故每个线程都会各自创建对象。
修正:
-
同步方法: 将
getInstance()
方法前加上synchronized
关键字,保证该方法同步,每一次只能由一个线程访问。优点:简单。缺点:效率太低
多个线程并发,这样的话线程只能顺序执行 -
synchronized
同步代码块
-
synchronized
在if (mySingleObj2 == null) {
前,即包含整个代码,这样做虽然能够保证只创建一个单例,但是效率和 同步方法一样差 -
synchronized
只包含创建对象的代码块, 缺点:仍然会导致创建多个对象
public static MySingleObj2 getInstance2() {
try {
if (mySingleObj2 == null) {
Thread.sleep(100);
// 仍然会出现线程不安全的问题
synchronized (MySingleObj2.class) {
mySingleObj2 = new MySingleObj2();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return mySingleObj2;
}
3. DCL 双重检查锁机制
public static MySingleObj2 getInstance3() {
try {
if (mySingleObj2 == null) {
Thread.sleep(1000);
synchronized (MySingleObj2.class) {
// 检查两次,获取最新值,防止之前锁中的线程已经对 obj 做出了改变
if (mySingleObj2 == null) {
mySingleObj2 = new MySingleObj2();
}
}
}
} catch (InterruptedException e) {
}
return mySingleObj2;
}
双重检查锁机制貌似解决了延迟初始化情况下多线程不安全的情况,但是其实并没有解决。
根源:instance = new Object();
创建对象可以理解为分解为下面三步
- 分配对象所需的内存空间
- 初始化对象
- 将引用指向对象分配的内存地址
如果程序底层的确按照这三步进行,就没问题。但是,JMM可能会将 2、3 重排序,即对象还未初始化,就已经将引用指向分配的内存地址了。这时线程判断instance 引用不为 null,如果这时候访问 instance 引用的对象(此时对象还未初始化),就等于访问一个未初始化的对象,导致错误。
解决: 禁止2/3 之间的重排序,如何禁止呢?声明 instance 为 volatile 变量即可
3. 静态内置类创建单例模式
public class MySingleObj {
private static class MySingletonHandler{
private static MySingleObj mySingleObj = new MySingleObj();
}
public static MySingleObj getInstance(){
return MySingletonHandler.mySingleObj;
}
private MySingleObj(){}
}
用 TestSingle.java 测试,适用于多线程环境下
原理:在访问内置类Class对象时,会获取一个锁,保证多线程环境下只允许一个线程能获取该锁,从而保证该类只被加载、初始化一次,其他线程等待锁释放后获得锁,就不必再加载类、初始化类变量了。从而保证内部类中的类变量始终为单例对象。
4. 序列化和反序列化的单例模式实现
静态内置类可以达到线程安全、创建单例的目的,但是当序列化对象、反序列化时,得到的对象是多例的。
public class MySingleObj implements Serializable{
private static final long serialVersionUID = 7130249135107591053L;
private MySingleObj(){}
private static class MySingleObjHandler{
private static MySingleObj mySingleObj = new MySingleObj();
}
public static MySingleObj getInstance() {
return MySingleObjHandler.mySingleObj;
}
/*
protected Object readResolve(){
System.out.println("调用了readResolve()方法");
return MySingleObjHandler.mySingleObj;
}
*/
}
序列化和反序列化测试类
public class SerializableTest {
public static void main(String[] args) {
serializable();
deserializable();
}
public static void serializable() {
MySingleObj mySingleObj = MySingleObj.getInstance();
ObjectOutputStream outputStream = null;
try {
outputStream = new ObjectOutputStream(new FileOutputStream(new File("a.txt")));
outputStream.writeObject(mySingleObj);
System.out.println(mySingleObj);
} catch (IOException e) {
e.printStackTrace();
}finally {
if(outputStream!=null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void deserializable(){
ObjectInputStream inputStream = null;
try {
inputStream = new ObjectInputStream(new FileInputStream(new File("a.txt")));
MySingleObj mySingleObj = (MySingleObj) inputStream.readObject();
System.out.println(mySingleObj);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if(inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
输出结果:
pattern4.MySingleObj@6d6f6e28
pattern4.MySingleObj@7cca494b
即序列化的对象,进行反序列化后得到的不是同一个实例
如何解决:
将MySingleObj
中注释的readResolve()
方法放开即可,反序列化时会调用 readResolve()
方法
再次运行
pattern4.MySingleObj@6d6f6e28
调用了readResolve()方法
pattern4.MySingleObj@6d6f6e28
结论: 静态内置类创建单例的方式不能直接用于序列化和反序列化,要加上readResolve()
方法供反序列化时调用,保证反序列化后创建的对象和序列化创建的对象为同一个
5. 使用 static 代码块实现单例模式
public class MySingleObj{
private static MySingleObj obj =null;
static{
obj = new MySingleObj();
}
private MySingleObj(){}
public static MySingleObj getInstance() {
return obj;
}
}
结论:类似于饿汉模式,适用于多线程环境
6. 使用enum 枚举数据类型实现单例模式
枚举enum 和静态代码块类似,使用枚举类时,其构造方法会自动调用,且只调用一次,利用该特性实现单例模式
public enum MySingleObj {
MY_SINGLE_OBJ;
private Object object;
private MySingleObj(){
object = new Object();
}
public Object getInstance() {
return object;
}
}
TestSingle.java
public class TestSingle {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(MySingleObj.MY_SINGLE_OBJ.getInstance());
}
};
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(runnable);
}
for (int i = 0; i < 100; i++) {
threads[i].start();
}
}
}
结果:正确,只创建了单例
完善 enum 类创建单例模式
上面的方式,将枚举类暴露,违反‘’职责单一原则’,修改MySingleObj.java
public class MySingleObj {
public enum MyEnumSingleton {
MY_SINGLE_OBJ;
private Object object;
private MyEnumSingleton() {
object = new Object();
}
public Object getInstance() {
return object;
}
}
public static Object getInstance() {
return MyEnumSingleton.MY_SINGLE_OBJ.getInstance();
}
}
网友评论