关键
如何使单例模式遇到多线程是安全的。
立即加载/"饿汉模式"
立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new实例化。而立即加载在中文语境看来,有"急"的意思,所以也叫"饿汉模式"。
public class MyObject {
// 立即加载方式==饿汉模式
/** Field myObject */
private static MyObject myObject = new MyObject();
/**
* Constructs MyObject
*
*/
private MyObject() {}
/**
* Method getInstance
*
*
* @return
*
* 此代码版本为立即加载,此版本代码的缺点是不能有其他实例变量,
* 因此getinstance()方法没有同步,所以可能出现非线程安全问题。
*/
public static MyObject getInstance() {
return myObject;
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
/**
* Method main
*
*
* @param args
*/
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
/*result:
1108767984
1108767984
1108767984
*/
运行结果显示hashCode为同一个值,说明对象时同一个对象,也就实现了立即加载型单例模式。
延迟加载/懒汉模式
延迟加载就是在调用get()方法是实例才被创建,常见的实现办法就是在get()方法中进行new实例化操作。延迟加载又叫懒汉模式。
饿汉模式时会出现多个实例,在多线程环境下这是与单例模式相背离的。
下面的代码是完全错误的,不能实现保持单例的状态。
public class MyObject {
/** Field myObject */
private static MyObject myObject;
/**
* Constructs MyObject
*
*/
private MyObject() {}
/**
* Method getInstance
*
*
* @return
*/
public static MyObject getInstance() {
try {
if (myObject != null) {}
else {
// 模仿在创建对象时的一些准备工作
Thread.sleep(3000);
myObject = new MyObject();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
/**
* Method main
*
*
* @param args
*/
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
/*result:
1127673581
1516995504
1905381423
*/
此实验会出现取出多个实例的情况,这就是错误的单例模式。
延迟加载的解决方案。
添加sybchronized关键字 ==pass==
在实例化方法中添加synchronized关键字,即可实现得到同一实例,但此方法运行效率非常低,是同步运行的。
public static synchronized MyObject getInstance() {
try {
if (myObject != null) { }
else {
// 模仿在创建对象时的一些准备工作
Thread.sleep(3000);
myObject = new MyObject();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
/*result:
1127673581
1127673581
1127673581
*/
使用同步代码块 ==pass==
此方法有所改进,但效率依旧比较低。
public static MyObject getInstance() {
try {
synchronized (MyObject.class) {
if (myObject != null) {}
else {
// 模仿在创建对象时的一些准备工作
Thread.sleep(3000);
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
/*result:
853747565
853747565
853747565
*/
针对某些重点代码单独同步 ==pass==
此方法运行效率较高。但多线程情况下无法实现只取一个实例对象。
public static MyObject getInstance() {
try {
if (myObject != null) { }
else {
// 模仿在创建对象时的一些准备工作
Thread.sleep(3000);
synchronized (MyObject.class) {
myObject = new MyObject();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
/*result:
1127673581
936272565
726367991
*/
DCL双检查锁机制。
最后步骤中使用DCL双检查锁机制来实现多线程环境下延迟加载的单例模式,正确且效率高。
public static MyObject getInstance() {
try {
if (myObject != null) { }
else {
// 模仿在创建对象时的一些准备工作
Thread.sleep(3000);
synchronized (MyObject.class) {
if (myObject == null) {
myObject = new MyObject();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myObject;
}
/*result:
1108767984
1108767984
1108767984
*/
使用静态内置类实现单例模式
DCL可以解决多线程单例模式的非线程安全问题,但是也有其他方法也能实现。
public class MyObject {
/**
* Constructs MyObject
*
*/
private MyObject() {}
/**
* Method getInstance
*
*
* @return
*/
public static MyObject getInstance() {
return MyObjectHandler.myObject;
}
/**
* Class MyObjectHandler
*
*
* @version 1.0, 18/05/05
* @author tz
*/
private static class MyObjectHandler {
/** Field myObject */
private static MyObject myObject = new MyObject();
}
}
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(MyObject.getInstance().hashCode());
}
}
public class Run {
/**
* Method main
*
*
* @param args
*/
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
/*result:
264320363
264320363
264320363
*/
上面代码证明使用内置内部类的方法也可以实现懒汉模式下多线程的单实例模式。
序列化和反序列化的单例模式实现
静态内置类可以达到线程安全的目的,但是如果遇到序列化对象时,使用默认的方法运行得到的结果还是多例的。
public class MyObject implements Serializable {
/** Field serialVersionUID */
private static final long serialVersionUID = 888L;
/**
* Constructs MyObject
*
*/
private MyObject() {}
/**
* Method getInstance
*
*
* @return
*/
public static MyObject getInstance() {
return MyObjectHandler.myObject;
}
/**
* Class MyObjectHandler
*
*
* @version Enter version here..., 18/05/05
* @author Enter your name here...
*/
private static class MyObjectHandler {
/** Field myObject */
private static final MyObject myObject = new MyObject();
}
}
public class SaveAndRead {
/**
* Method main
*
*
* @param args
*/
public static void main(String[] args) {
try {
MyObject myObject = MyObject.getInstance();
FileOutputStream fosRef = new FileOutputStream(new File("myObjectFile.txt"));
ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
oosRef.writeObject(myObject);
oosRef.close();
fosRef.close();
System.out.println(myObject.hashCode());
} catch (IOException e) {
e.printStackTrace();
}
try {
FileInputStream fisRef = new FileInputStream(new File("myObjectFile.txt"));
ObjectInputStream iosRef = new ObjectInputStream(fisRef);
MyObject myObject = (MyObject) iosRef.readObject();
iosRef.close();
fisRef.close();
System.out.println(myObject.hashCode());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/*result:
1836019240
2093631819
*/
上述程序反应了序列化对象的情况下,内置内部类不能实现单例模式,解决方法就是使用反序列化。
ublic class MyObject implements Serializable {
/** Field serialVersionUID */
private static final long serialVersionUID = 888L;
/**
* Constructs MyObject
*
*/
private MyObject() {}
/**
* Method getInstance
*
*
* @return
*/
public static MyObject getInstance() {
return MyObjectHandler.myObject;
}
protected Object readResolve() {
System.out.println("调用了readResolve方法!");
return MyObjectHandler.myObject;
}
/**
* Class MyObjectHandler
*
*
* @version Enter version here..., 18/05/05
* @author Enter your name here...
*/
private static class MyObjectHandler {
/** Field myObject */
private static final MyObject myObject = new MyObject();
}
}
/*result:
1836019240
调用了readResolve方法!
1836019240
*/
反序列化后即实现单例。
使用static代码块实现单例模式
静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式
public class MyObject {
/** Field instance */
private static MyObject instance = null;
static {
instance = new MyObject();
}
/**
* Constructs MyObject
*
*/
private MyObject() { }
/**
* Method getInstance
*
*
* @return
*/
public static MyObject getInstance() {
return instance;
}
}
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(MyObject.getInstance().hashCode());
}
}
}
public class Run {
/**
* Method main
*
*
* @param args
*/
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
/*result:
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
868591785
*/
使用enum枚举数据类型实现单例模式
枚举enum和静态代码块的特性相似,在使用枚举类时构造方法会自动被调用,也可以应用其这个特性实现单例设计模式。