设计模式——6.单例模式(包含多线程环境下的一些写法)

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()方法中的耗时操作。


谢谢。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值