目录
1. 概念
2. 分类
2.1 饿汉式单例模式
2.2 懒汉式单例模式
2.3 兼顾懒汉式和饿汉式
2.4 注册式单例模式
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
饿汉式单例模式在类加载的时候就初始化并创建单例对象,它是绝对的线程安全,没有加任何锁、执行效率也比较高,但不管用或不用都必须占用一定空间,Spring的IOC容器ApplicationContext就是饿汉式的。
它的代码如下,很好看懂:
/**
* 写法一:静态代码块初始化
*/
public class Singleton2 {
private static final Singleton2 singleton2;
static {
singleton2 = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getSingleton2(){
return singleton2;
}
}
/**
* 写法二:直接初始化
*/
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getSingleton(){
return singleton;
}
}
懒汉式单例在被外部调用时内部类才会被加载,我们的研究也是主要针对懒汉式开展。
2.2.1 存在线程安全隐患的懒汉式
先看一个单例实现:
public class One {
private One(){}
private static One one = null;
public static One getOne(){
if (one == null){
return one = new One();
}
return one;
}
public static void main(String[] args) {
Thread thread1 = new Thread(new ExectorThread(), "one");
Thread thread2 = new Thread(new ExectorThread(), "two");
thread1.start();
thread2.start();
System.out.println("end");
}
}
class ExectorThread implements Runnable{
public void run() {
One one = One.getOne();
System.out.println(Thread.currentThread().getName()+":"+one);
}
}
大家可能已经看出来了,这个程序两个线程可能输出不同one对象,这个情况我们可以用线程模式的debug模拟出来:
首先左键打三个断点:
右键断点勾选Thread(三个都选):
然后以debug模式运行:
切换到one线程使用step into调试:
使one线程执行到以下程序语句:
继续切换到two线程使之执行完毕后发现控制台打印:
切换到one线程使之执行完发现控制台打印:
此时就模拟出了此种单例模式的bug情况。我们可以通过synchronized关键字来解决这种情况。
2.2.2 线程安全但性能堪忧的懒汉式
代码如下:
/**
* 无安全隐患的单例
* 但高并发下程序性能很差
*/
public class Two {
private Two(){}
private static Two two = null;
public synchronized static Two getTwo(){
if (two == null){
two = new Two();
}
return two;
}
public static void main(String[] args) {
Thread thread1 = new Thread(new ExectorThread1(), "one");
Thread thread2 = new Thread(new ExectorThread1(), "two");
thread1.start();
thread2.start();
System.out.println("end");
}
}
class ExectorThread1 implements Runnable{
public void run() {
Two two = Two.getTwo();
System.out.println(Thread.currentThread().getName()+":"+two);
}
}
我们可以再次照着上次的模拟方式测试此种单例模式,发现没有持有synchronized锁的线程在访问getTwo()时会进入MONITOR状态:
当持有synchronized锁的线程运行完getTwo()方法后,另一个线程才会进入RUNNING状态继续运行,这样就保证了单例的唯一性,但是在高并发的情况下,不管单例存在不存在,每一个线程都需要等待锁的持有,所以对程序性能造成很大麻烦。
2.2.3 双重检查锁(DCL)的懒汉式
代码如下:
public class Three {
private Three(){}
private static volatile Three three = null;
public static Three getThree(){
if (three == null){
synchronized (Three.class){ //因为单例模式是全局的,所以使用类锁
if (three == null){
three = new Three();
}
}
}
return three;
}
public static void main(String[] args) {
Thread thread1 = new Thread(new ExectorThread2(), "one");
Thread thread2 = new Thread(new ExectorThread2(), "two");
thread1.start();
thread2.start();
System.out.println("end");
}
}
class ExectorThread2 implements Runnable{
public void run() {
Three three = Three.getThree();
System.out.println(Thread.currentThread().getName()+":"+three);
}
}
使用了双重检查锁后,如果该对象已经存在则不需要进行锁的争用,极大的降低了高并发下程序性能损耗。
代码如下:
public class Four {
private Four(){}
public static final Four getFour(){
return LazyHolder.Lazy;
}
public static class LazyHolder{
private static final Four Lazy = new Four();
}
public static void main(String[] args) {
Four.getFour();
}
}
当Four类加载时,代码中的LazyHolder静态内部类默认是不加载的(JVM类加载机制),只有调用getFour()时才会new一个Four()对象,而static和final保证了单例的唯一性和不变性。
但此代码还有一个问题,如果使用反射强行创建多个对象,这个单例的唯一性还是会被破坏,如下:
public static void main(String[] args) {
try {
Class fourClass = Four.class;
Constructor declaredConstructor = fourClass.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//强制创建了两次对象
Four four1 = declaredConstructor.newInstance();
Four four2 = declaredConstructor.newInstance();
System.out.println(four1);
System.out.println(four2);
}catch (Exception e){
e.printStackTrace();
}
}
运行发现:
创建了两个不同的实例对象,那么如何避免?可以改良一下私有的构造方法:
public class Four {
private Four(){
if(LazyHolder != null){
throw new RuntimeException("不允许创建多个Four单例");
}
}
public static final Four getFour(){
return LazyHolder.Lazy;
}
public static class LazyHolder{
private static final Four Lazy = new Four();
}
public static void main(String[] args) {
Four.getFour();
}
}
详细说说这个过程,先看一段代码(只是上面的加上了一些输出断点):
public class Four {
private Four(){
System.out.println("内部类加载前:"+System.currentTimeMillis());
System.out.println(LazyHolder.Lazy+":"+System.currentTimeMillis());
if (LazyHolder.Lazy != null){
throw new RuntimeException("不允许创建多个Four实例");
}
System.out.println("创建了一个单例实例");
}
public static final Four getFour(){
return LazyHolder.Lazy;
}
public static class LazyHolder{
static {
System.out.println("内部类加载:"+System.currentTimeMillis());
}
private static final Four Lazy = new Four();
}
public static void main(String[] args) {
try {
Class fourClass = Four.class;
Constructor declaredConstructor = fourClass.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
System.out.println("第一次创建对象:"+System.currentTimeMillis());
Four four1 = declaredConstructor.newInstance();
System.out.println("第二次创建对象:"+System.currentTimeMillis());
Four four2 = declaredConstructor.newInstance();
System.out.println(four1);
System.out.println(four2);
}catch (Exception e){
e.printStackTrace();
}
}
}
首先打两个断点:
debug运行,使用step over(折线)达到如下所示:
使用step into(指向下的箭头)进入Four的构造方法:
step over两次,此时到达第8行并输出:
再一次step over,此时输出:
由于输出语句调用了LazyHolder.Lazy触发了静态内部类的类加载,所以静态变量Lazy的初始化也就是Four的构造方法又执行了一次,所以会有第二句"内部类加载前"的语句输出,此时判断LazyHolder.Lazy是否为空,由于已经处于类加载期间,所以直接判断Lazy为null,结束创建过程,返回到原先输出Lazy的语句(输出刚才创建的Lazy对象)。发现Lazy不为空,再step over之后就抛出了"不允许创建多个Four实例"异常:
当对象序列化后写入磁盘,再反序列化将其转化为内存对象时,反序列化的对象会重新分配内存并重新创建,当这个对象是单例对象的话,以上所述的单例模式就保证不了单例的唯一性了。
测试代码如下:
public class Five implements Serializable {
private Five(){}
private final static Five five = new Five();
public static Five getFive(){
return five;
}
public static void main(String[] args) {
Five f1 = Five.getFive();
Five f2 = null;
try {
FileOutputStream fos = new FileOutputStream("Five.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(f1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Five.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
f2 = (Five) ois.readObject();
ois.close();
System.out.println(f1);
System.out.println(f2);
}catch (Exception e){
e.printStackTrace();
}
}
}
输出:
其实增加一个readResolve方法就可以解决这个问题:
如下:
public class Five implements Serializable {
private Five(){}
private final static Five five = new Five();
public static Five getFive(){
return five;
}
private Object readResolve(){
return five;
}
}
输出:
原因是在JDK的ObjectStreamClass源码中有这样一个赋值代码,它就是通过反射找到无参的readResolve方法:
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
而ObjectInputStream中判断了如果readResolveMethod这个方法存在则调用invokeReadResolve:
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
......
invokeReadResolve就通过反射调用了readResolve方法:
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
......
虽然解决了问题,但事实上还是创建了两个对象,只不过调用了readResolve方法返回的是同一个对象。注册式单例可以解决这个问题。
注册式单例模式就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例,它分为枚举式单例模式和容器式单例模式。
2.4.1 枚举式单例模式
先看枚举式单例模式的写法:
public enum Six {
SIX;
private Object data;
public Object getData(){
return data;
}
public void setData(Object data){
this.data = data;
}
public static Six getSix(){
return SIX;
}
public static void main(String[] args) {
Six s1 = Six.getSix();
s1.setData(new Object());
Six s2 = null;
try {
FileOutputStream fos = new FileOutputStream("Six.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Six.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s2 = (Six) ois.readObject();
ois.close();
System.out.println(s1.getData());
System.out.println(s2.getData());
}catch (Exception e){
e.printStackTrace();
}
}
}
运行后:
双击Target中的class文件:
点击View->show ByteCode可以看到Six反编译的字节码,其中有:
// access flags 0x8
static ()V
L0
LINENUMBER 12 L0
NEW cn/xupt/设计模式/单例模式/注册式/Six
DUP
LDC "SIX"
ICONST_0
INVOKESPECIAL cn/xupt/设计模式/单例模式/注册式/Six. (Ljava/lang/String;I)V
PUTSTATIC cn/xupt/设计模式/单例模式/注册式/Six.SIX : Lcn/xupt/设计模式/单例模式/注册式/Six;
表示枚举式单例模式在静态代码块中就给SIX进行了赋值,是饿汉式的实现,那它如何不被序列化影响,答案是readObject0()方法中的readEnum()方法:
private Object readObject0(boolean unshared) throws IOException {
......
case TC_ENUM:
return checkResolve(readEnum(unshared));
......
}
readEnum():
private Enum> readEnum(boolean unshared) throws IOException {
......
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum> en = Enum.valueOf((Class)cl, name);//*********
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
枚举类型是通过类名和类对象找到一个唯一的枚举对象,这就解释了它为什么不会被序列化影响。
2.4.2 容器化单例模式
public class ContainerSingleton {
private static Map ioc = new ConcurrentHashMap();
private ContainerSingleton(){}
public static Object getBean(String className){
synchronized (ioc){
if (!ioc.containsKey(className)) {
Object obj = null;
try{
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
}catch (Exception e){
e.printStackTrace();
}
return obj;
}
return ioc.get(className);
}
}
}
容器式单例模式适用于单例对象非常多的情况,使用它可以便于管理。
感谢观看。