1.前言
单例(单件)模式,可以说是这么多设计模式中,最简单的一种模式,在整个系统生命之中,它是独一无二的,它只能有一个实例的对象。(忠贞不二的爱,永远只有一个对象,哈哈哈)
2.定义(引用head first设计模式)
3.缘由
凡事有因,让我们一起来聊聊为什么会有单例模式这一个设计模式吧。很多时候,我们都会用到一些很重要共享资源(有一部资源甚至还是重量级资源,会占用很多的内容),比如:数据库连接池,注册表,线程池,众多驱动程序的对象等等。这些都是一些重要的共享资源,甚至其中不乏占用很多资源的对象,数据库连接池就是其中之一,我想也是我们实际开发中,接触得最多的吧。
就拿数据库连接池来说,它本身就占据众多了资源,而且如果管理不当的话,就很容易使程序出现各种问题。对于这种对象,我们当然是不希望别人可以随随便便就new出来,造成资源等不必要的浪费。所以,这个时候,我们就要用到单例模式了。
4.从单一线程环境下说起
要怎么样才能让保持由始至终,都是只有一个对象呢?要知道,我们无法控制别人的啊。所以,最好的方法,就是将构造函数私有化。那将构造函数私有化之后,别人又如何能拿到你的对象呢?请看下面一个简单的例子:
下面这种方式,叫做饿汉模式(并不是另外一种设计模式,只是基于单例模式下的一种叫法)
package singleThread;
public class Test1 {
public static void main(String[] args) {
TestSingle1.getInstance();
}
}
class TestSingle1 {
private static TestSingle1 test = new TestSingle1();
private TestSingle1() {
}
public static TestSingle1 getInstance() {
return test;
}
}
由于只有一个无参构造函数,并且修饰符是private,所以别人是无法直接new出这个对象的,别人要想使用这个对象的话,就必须要调用getInstance()来获取这个类的对象实例。
当然,看到上面的代码,可能就有人说,你一开始就将那个对象在内部new出来,那不是很占资源吗?万一这个对象,我很长时间都用不到呢?那不是要浪费资源很长时间?说得没错,因此,我们就有下面这个写法。
下面这种方式,叫做饿汉模式(并不是另外一种设计模式,只是基于单例模式下的一种叫法)
class TestSingle1 {
private static TestSingle1 test = null;
private TestSingle1() {
}
public static TestSingle1 getInstance() {
if (test == null) {
test = new TestSingle1();
}
return test;
}
}
到这里, 单例模式,我们可以说是学习完毕了。但是,这个模式就是这么简单吗?是的,没错,就是这么的简单哦。但是,这仅仅是在单线程的环境下,如果遇上了多线程的话,这个模式就没有那么简单啦。因为,它分分钟会“出轨”哦。在多线程环境下,如果我们不加以处理的话,它基本上就不是仅仅只有一个实例对象啦。
5.说说多线程环境下
public class Test1 {
public static void main(String[] args) {
new Thread() {
public void run() {
System.out.println(TestSingle1.getInstance().hashCode());
};
}.start();
new Thread() {
public void run() {
System.out.println(TestSingle1.getInstance().hashCode());
};
}.start();
new Thread() {
public void run() {
System.out.println(TestSingle1.getInstance().hashCode());
};
}.start();
}
}
class TestSingle1 {
private static TestSingle1 test = null;
private TestSingle1() {
}
public static TestSingle1 getInstance() {
if (test == null) {
// 模拟一些耗时的准备操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test = new TestSingle1();
}
return test;
}
}
上面这些代码,就是用于模拟多线程环境的,并且为了体验效果,特别在getInstance()方法中,睡眠了10毫秒。
输出结果为:
从三个不同hashCode可以看出,此时系统创建了三个对象,不再是单例了。其实,原因很简单,就是三个线程在获取到这个对象之前,都同时通过了if (test == null)这个判断,进入到了if语句里面。
5.1解决方法1
修改一下getInstance()方法:
public synchronized static TestSingle1 getInstance() {
if (test == null) {
// 模拟一些耗时的准备操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test = new TestSingle1();
}
return test;
}
再或者(效率上,和上一种几乎是一样的):
public static TestSingle1 getInstance() {
synchronized (TestSingle1.class) {
if (test == null) {
// 模拟一些耗时的准备操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test = new TestSingle1();
}
}
return test;
}
使用上面的手段后,运行的结果如下:
很明显,又变回了单一一个对象了。但是,这样的写法,是多种解决方法中,效率最低的。
5.2解决方法2
提高效率的关键在于,让synchronized代码块包裹住的代码块尽量小,最重要的,是将耗时操作,移出同步代码块中,不然是无法提高多少效率的。很自然而然的,就会想到下面的伪解决方法:
public static TestSingle1 getInstance() {
if (test == null) {
// 模拟一些耗时的准备操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (TestSingle1.class) {
test = new TestSingle1();
}
}
return test;
}
可惜的是,运行结果是:
之所以会造成这样,就是因为三个线程到达同步代码块之后,都同时停下了,其中一个线程获得锁,然后执行了同步代码块里面的代码:new语句,执行结束后也解放了锁。此时另外一个线程获得了锁,所以它又再一次执行了new语句。如此类推,所以最后三个线程拿到的,都不是同一个对象。
真解决方法:
public static TestSingle1 getInstance() {
if (test == null) {
// 模拟一些耗时的准备操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (TestSingle1.class) {
if (test == null) {
test = new TestSingle1();
}
}
}
return test;
}
运行结果:
这种方法,外界也叫做DCL双检查锁机制,是大多数多线程配合单例模式所使用的解决方法。
5.3解决方法3
class TestSingle1 {
private TestSingle1() {
}
public static TestSingle1 getInstance() {
// 模拟一些耗时的准备操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return TestSingle1Builder.test;
}
private static class TestSingle1Builder {
private static TestSingle1 test = new TestSingle1();
}
}
public class Test1 {
<span style="white-space:pre"> </span>public static void main(String[] args) {
<span style="white-space:pre"> </span>new TestSingle1();
<span style="white-space:pre"> </span>}
}
class TestSingle1 {
<span style="white-space:pre"> </span>static{
<span style="white-space:pre"> </span>System.out.println("执行了外部类的静态代码块");
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>public TestSingle1() {
<span style="white-space:pre"> </span>System.out.println("执行了外部类的构造方法");
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>private static class TestSingle1Builder {
<span style="white-space:pre"> </span>static {
<span style="white-space:pre"> </span>System.out.println("内部类加载了没呢1111?");
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>private static TestSingle1 test = new TestSingle1();
<span style="white-space:pre"> </span>static {
<span style="white-space:pre"> </span>System.out.println("内部类加载了没呢22222?");
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
}
不过,这个虽然是一个简洁的并且线程安全的实现方法,但是它也有它的弊端,如果遇到序列化对象时,如果不加以处理的话,最终得到的结果,还是多个对象。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Test1 {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
"D:\\1.txt"));
TestSingle1 single1 = TestSingle1.getInstance();
oos.writeObject(single1);
System.out.println("对象通过对象流写出去之后的hashCode:" + single1.hashCode());
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"D:\\1.txt"));
single1 = (TestSingle1) ois.readObject();
System.out.println("从文件中,通过对象流读进来之后的hashCode:" + single1.hashCode());
oos.close();
ois.close();
}
}
class TestSingle1 implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private TestSingle1() {
}
public static TestSingle1 getInstance() {
// 模拟一些耗时的准备操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return TestSingle1Builder.test;
}
private static class TestSingle1Builder {
private static TestSingle1 test = new TestSingle1();
}
}
解决的方法,是在TestSingle1类中,加入下面这个方法:
public Object readResolve() {
return TestSingle1Builder.test;
}
然后,从对象流中读取到对象后,先执行以下这个方法。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
"D:\\1.txt"));
single1 = (TestSingle1) ois.readObject();
single1 = (TestSingle1) single1.readResolve();
System.out.println("从文件中,通过对象流读进来之后的hashCode:" + single1.hashCode());
通过上面这样的解决,得到的又是同一个对象了。当然,如果是使用DCL双重校验的话,也是会遇到这个问题的,解决的方法,就是先调用一次getInstance()方法,即可解决。
再说多两句就是,这里或许有人不懂,为什么对于静态内部类,我们要另外写一个readResolve() 方法呢?而不是再一次去调用getInstance()方法就好呢。其实这是为了避过getInstance()方法中的耗时操作。
谢谢。