Implementación y Análisis de Varios Patrones Singleton en JAVA

Este artículo ha participado en el evento "Ceremonia de creación de recién llegados" para comenzar juntos el camino de la creación de oro.

@ [toc]

El patrón singleton es el patrón de diseño más básico en el proceso de aprendizaje de patrones de diseño. Básicamente, aprenderá el patrón singleton tan pronto como comience a aprender. De hecho, hay muchas formas de escribir el patrón singleton en java. pregunta. Entonces, qué métodos de escritura se pueden usar y qué métodos de escritura no se pueden usar, o en qué escenarios se pueden usar diferentes métodos de implementación. Este artículo analiza la implementación de nueve patrones singleton existentes.

1. Modo singleton de estilo hambriento: usando constantes estáticas

el código se muestra a continuación:

package com.dhb.gts.javacourse.week5.singleton;

/**
*@author [email protected]
*@description 懒汉式单例模式  采用静态常量的方式实现。
 *   简单实用,线程安全。
 *   唯一缺点是不管用到与否,在类加载的时候都会进行实例化。
*@date  2021/8/30 13:44
*/
public class SingletonDemo1 {
	
	private final static SingletonDemo1 INSTANCE = new SingletonDemo1();
	
	private SingletonDemo1() {}
	
	public static SingletonDemo1 getInstance() {
		return INSTANCE;
	}

	public static void main(String[] args) {
		SingletonDemo1 singleton1 = SingletonDemo1.getInstance();
		SingletonDemo1 singleton2 = SingletonDemo1.getInstance();
		System.out.println(singleton1 == singleton2);
	}
}

复制代码

La ventaja de esta implementación es que el método de escritura es simple y la creación de instancias del objeto requerido se completa en el proceso de carga de clases, evitando así problemas de seguridad de subprocesos.

La desventaja es que en el modo singleton chino hambriento, no importa si el objeto requerido se usa o no, el objeto se creará primero. Si el objeto no se usa en todo el proceso comercial, inevitablemente causará una pérdida de memoria.

2. Modo singleton de estilo chino hambriento - usando bloques de código estáticos

el código se muestra a continuación:

package com.dhb.gts.javacourse.week5.singleton;

/**
*@author [email protected]
*@description 懒汉式单例模式  采用静态代码块的方式实现。
 *   实际上等价于静态常量的方式实现,都是在类加载过程中就实现了目标对象的实例化。两者是等价的 ,优缺点也一致。
 *   简单实用,线程安全。
 *   唯一缺点是不管用到与否,在类加载的时候都会进行实例化。
*@date  2021/8/30 13:55
*/
public class SingletonDemo2 {

	private final static SingletonDemo2 INSTANCE;
	
	static {
		INSTANCE = new SingletonDemo2();
	}

	private SingletonDemo2() {}

	public static SingletonDemo2 getInstance() {
		return INSTANCE;
	}

	public static void main(String[] args) {
		SingletonDemo2 singleton1 = SingletonDemo2.getInstance();
		SingletonDemo2 singleton2 = SingletonDemo2.getInstance();
		System.out.println(singleton1 == singleton2);
	}
}

复制代码

El patrón singleton implementado en forma de bloques de código estático es en realidad equivalente al patrón singleton implementado en forma de constantes estáticas, y la creación de instancias del objeto de destino se realiza en el proceso de carga de clases. Esto evita problemas de seguridad de subprocesos.

Sus defectos también son consistentes con el modo hambriento de constantes estáticas, lo que puede causar un desperdicio de memoria.

3. Patrón Lazy Singleton - implementación básica

el código se muestra a continuación:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author [email protected]
*@description 懒汉式单例模式--基本实现
 * 懒汉式单例模式虽然能起到懒加载的效果,达到节约内存空间的目的。
 * 但是在多线程的条件下,如果一个线程进入了if判断,还没有执行,而另外一个线程也进入if判断。
 * 此时并会导致返回多个实例。因此这种方式在生产环境是不可取的。
 * 在getInstance方法中,添加了sleep时间,通过main方法中多线程执行效果就会非常明显,可以发现这样会导致每次输出的hashcode都不相同。
 * 
*@date  2021/8/30 14:04
*/
public class SingletonDemo3 {
	
	private static SingletonDemo3 INSTANCE;

	public SingletonDemo3() {
	}

	public static SingletonDemo3 getInstance() {
		if(INSTANCE == null) {
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			INSTANCE = new SingletonDemo3();
		}
		return INSTANCE;
	}


	public static void main(String[] args) {
		IntStream.range(0,100).forEach(i -> {
			new Thread(() -> {
				System.out.println(SingletonDemo3.getInstance().hashCode());
			}).start();
		});
	}
}

复制代码

El patrón de singleton perezoso más básico se muestra arriba. Solo necesitamos juzgar en el método getInstance, si (INSTANCE == null), si es True, significa que el objeto no ha sido instanciado, y ahora se puede instanciar directamente. Sin embargo, este método introducirá el problema de la seguridad de subprocesos.En un entorno de subprocesos múltiples, si un subproceso ingresa el juicio if, la ejecución no se ha completado y otro subproceso también ingresa el juicio if. En este punto, y hará que se devuelvan varias instancias.
Por lo tanto, este enfoque no es recomendable en un entorno de producción. En el método getInstance, el tiempo de suspensión se agrega especialmente, y el efecto de ejecución de subprocesos múltiples en el método principal será muy obvio. Se puede encontrar que esto dará como resultado una salida de códigos hash diferentes cada vez.

Conclusión final: aunque este método de implementación reducirá la sobrecarga de memoria innecesaria, generará problemas de seguridad de subprocesos.En el caso de concurrencia, se puede crear una nueva instancia para cada llamada, por lo que no se recomienda este método.

4. Patrón Lazy Singleton - bloqueo en el método

el código se muestra a continuación:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author [email protected]
*@description 懒汉式单例模式--方法上加锁
 * 考虑到基本的懒汉式单利模式的线程安全的问题,最简单粗暴的方式就是在getInstance方法上加锁。
 * 这种方式能解决线程安全的问题,但是,在方法上粗暴的使用synchronized,将并行的方式直接变成了串行化。极大的降低了效率。
 * 用在生产系统中该方法将成为系统的瓶颈所在,因此这种方式虽然可用,但是并不推荐使用。
 * 
*@date  2021/8/30 14:33
*/
public class SingletonDemo4 {

	private static SingletonDemo4 INSTANCE;

	public SingletonDemo4() {
	}

	public static synchronized SingletonDemo4 getInstance() {
		if(INSTANCE == null) {
			try {
				TimeUnit.MILLISECONDS.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			INSTANCE = new SingletonDemo4();
		}
		return INSTANCE;
	}


	public static void main(String[] args) {
		IntStream.range(0,100).forEach(i -> {
			new Thread(() -> {
				System.out.println(SingletonDemo4.getInstance().hashCode());
			}).start();
		});
	}
}

复制代码

通过在getInstance方法上加synchronized来实现的懒汉式单利模式。经过测试,这种写法能避免线程安全的问题,在mian函数中进行测试,全部的hashcode都相同。 但是这种写法的问题在于,直接将synchronized加锁在getInstance方法上,这样会导致,如果并行的请求getInstance方法,将不得不变成串行化操作。 这样在并发场景中使用将极大的影响系统的性能。因此虽然这种方式能实现单例模式,但是并不推荐在生产环境中来使用。

5.懒汉式单例模式--在方法内部加同步代码块

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author [email protected]
*@description 懒汉式单例模式--在方法中加同步代码块
 * 既然将synchronized加锁到getInstance方法中,这样会导致效率的下降,那么我们可以尝试将锁细化,将synchronized加锁在if判断之后。
 * 但是经过实验,我们发现,这种方式会带来线程安全的问题。
 * 当一个线程进入了if判断,还没执行同步块中的代码,此时另外一个线程也进入了if判断区域,那么只要if判断通过,虽然后面有synchronized保护,
 * 这也只能将这两个线程在new对象的过程中变成了顺序操作,从根本上来说,还是创建了两个实例。我们通过main函数执行可以很好的验证这一点。
 *
*@date  2021/8/30 14:47
*/
public class SingletonDemo5 {
	
	private static SingletonDemo5 INSTANCE;

	public SingletonDemo5() {
	}

	public static SingletonDemo5 getInstance() {
		if (INSTANCE == null) {
			synchronized(SingletonDemo5.class) {
				try {
					TimeUnit.MILLISECONDS.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				INSTANCE = new SingletonDemo5();
			}
		}
		return INSTANCE;
	}


	public static void main(String[] args) {
		IntStream.range(0, 100).forEach(i -> {
			new Thread(() -> {
				System.out.println(SingletonDemo5.getInstance().hashCode());
			}).start();
		});
	}
}

复制代码

考虑到将synchronized加锁在getInstance方法可能带来效率问题,因此,我们可以进一步尝试锁的细化。将synchronized的同步代码块加在if判断内部。 实验结果证明,这种方式不仅不会对效率有帮助,还导致线程的同步问题,每次输出的hashcode也都不一样了。导致创建了多个目标对象。 这是因为,如果有一个线程已经执行完了if判断,之后虽然进入了同步块,但是还没执行完成,Instance还是空的。那么此时再有另外一个线程执行getInstance. 那么if判断会判断其通过,从而执行其内部的同步代码块。这样虽然加锁导致了串行化,但是实例的对象还是会被创建多次。 因此,此种方法不是一个可用的单例模式的实现方式。我们在生产环境中不推荐使用。

6.懒汉式单例模式--Double Check

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author [email protected]
*@description 懒汉式单例模式--同步块double check
 * 考虑到同步代码块会存在线程安全问题,这个问题都是if判断引起的,那么一种解决方法就是在同步代码块中增加double check ,既实现双重判定检查。
 * 经过验证,这种方式能在大多数情况下都能很好的实现单例模式,执行main函数,基本上hashcode都相同。
 * 但是还是会在少数情况下,出现多个实例的问题。我们可以思考一下这个问题,这个问题正是jvm的可见性造成的。
 * 前面我们的判断都是线程还在执行中,没有对INSTANCE进行赋值,后续的线程就要进入if判断了,因此会造成目标对象被初始化多次,
 * 那么我们假设,如果第一个线程已经执行完了对INSTANCE的赋值,加锁结束,此时恰好有一个线程已经进入了第一个if判断,正在等待锁。
 * 拿到锁之后,进入第二个if判断,但是由于可见性问题,此时第二个线程还不能看到线程一的值已经被写入完毕。误以为还是空,因此再次实现一次实例化。
 * 
*@date  2021/8/30 15:02
*/
public class SingletonDemo6 {

	private static SingletonDemo6 INSTANCE;

	public SingletonDemo6() {
	}

	public static SingletonDemo6 getInstance() {
		if (INSTANCE == null) {
			synchronized(SingletonDemo6.class) {
				if(INSTANCE == null) {
					try {
						TimeUnit.MILLISECONDS.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					INSTANCE = new SingletonDemo6();
				}
			}
		}
		return INSTANCE;
	}


	public static void main(String[] args) {
		IntStream.range(0, 100).forEach(i -> {
			new Thread(() -> {
				System.out.println(SingletonDemo6.getInstance().hashCode());
			}).start();
		});
	}
}

复制代码

考虑到同步代码块会存在线程安全问题,这个问题都是if判断引起的,那么一种解决方法就是在同步代码块中增加double check ,既实现双重判定检查。 经过验证,这种方式能在大多数情况下都能很好的实现单例模式,执行main函数,基本上hashcode都相同。 但是还是会在少数情况下,出现多个实例的问题。我们可以思考一下这个问题,这个问题正是jvm的可见性造成的。 前面我们的判断都是线程还在执行中,没有对INSTANCE进行赋值,后续的线程就要进入if判断了,因此会造成目标对象被初始化多次, 那么我们假设,如果第一个线程已经执行完了对INSTANCE的赋值,加锁结束,此时恰好有一个线程已经进入了第一个if判断,正在等待锁。 拿到锁之后,进入第二个if判断,但是由于可见性问题,此时第二个线程还不能看到线程一的值已经被写入完毕。误以为还是空,因此再次实现一次实例化。

7.懒汉式单例模式--Double Check + volatile

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author [email protected]
*@description 懒汉式单例模式--同步块double check + volatile
 * 考虑到Double check实现的单例模式存在可见性问题,我们可以通过在INSTANCE上加上volatile来实现。
 * 这样就能避免在前面DoubleCheck实现的单例模式里的问题,由于INSTANCE具备了可见性,此时再通过DoubleCheck的方式来实现,就不会出现目标对象实例化多次的问题。
 *  但是这种方式还是存在一个问题就是,如果我们无法避免反序列化的问题。通过反序列化,仍然可以将这个类实例化多次。
*@date  2021/8/30 15:02
*/
public class SingletonDemo7 {

	private static volatile SingletonDemo7 INSTANCE;

	public SingletonDemo7() {
	}

	public static SingletonDemo7 getInstance() {
		if (INSTANCE == null) {
			synchronized(SingletonDemo7.class) {
				if(INSTANCE == null) {
					try {
						TimeUnit.MILLISECONDS.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					INSTANCE = new SingletonDemo7();
				}
			}
		}
		return INSTANCE;
	}


	public static void main(String[] args) {
		IntStream.range(0, 100).forEach(i -> {
			new Thread(() -> {
				System.out.println(SingletonDemo7.getInstance().hashCode());
			}).start();
		});
	}
}

复制代码

为了解决DoubleCheck创建的单例模式中的可见性问题,我们在INSTANCE上增加了volatile,通过happens-before 原则,避免指令重排序,保障了INSTANCE的可见性。 这样在生产环境中,如果我们不考虑反序列化方式可以将这个类创造多个实例之外,这种方式是目前我们在上述所有当例模式的最优写法。

不过需要注意的是,如果通过反序列化,或者反射,那么可能就可能绕开DoubleCheck,造成目标对象被实例化多次。

8.懒汉式单利模式--静态内部类

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
*@author [email protected]
*@description 懒汉式单例模式--静态内部类
 * 结合饿汉模式的优点,既然饿汉模式可以完美而又简单的实现单例模式,而且还能保证线程安全。那么可以参考饿汉模式,结合懒汉模式懒加载的优点。
 * 在其内部建立一个静态的内部类,这个类只有调用getInstance的时候才会被加载,而利用classLoader,从而保证只有一个实例会被实例化。
 * 这种实现方式同样是不能防止反序列化的。如果要解决这个问题,可以通过Serializable、transient、readResolve()实现序列化来解决。
*@date  2021/8/30 15:02
*/
public class SingletonDemo8 {

	public SingletonDemo8() {
	}

	private static class SingletonDemo8Holder{
		private static final SingletonDemo8 INSTANCE = new SingletonDemo8();
	}
	
	public static SingletonDemo8 getInstance() {
		return SingletonDemo8Holder.INSTANCE;
	}


	public static void main(String[] args) {
		IntStream.range(0, 100).forEach(i -> {
			new Thread(() -> {
				System.out.println(SingletonDemo8.getInstance().hashCode());
			}).start();
		});
	}
}

复制代码

结合饿汉模式的优点,既然饿汉模式可以完美而又简单的实现单例模式,而且还能保证线程安全。那么可以参考饿汉模式,结合懒汉模式懒加载的优点。 在其内部建立一个静态的内部类,这个类只有调用getInstance的时候才会被加载,而利用classLoader,从而保证只有一个实例会被实例化。 这种实现方式同样是不能防止反序列化的。如果要解决这个问题,可以通过Serializable、transient、readResolve()实现序列化来解决。

9.懒汉式单利模式--利用枚举

代码如下:

package com.dhb.gts.javacourse.week5.singleton;

import java.util.stream.IntStream;

/**
 * @author [email protected]
 * @description 懒汉式单例模式--枚举
 * 在《effective java》中还有一种更简单的写法,那就是枚举。也是《effective java》作者最为推崇的方法。
 * 这种方法不仅可以解决线程同步问题,还可以防止反序列化。
 * 枚举类由于没有构造方法(枚举是java中约定的特殊格式,因此不需要构造函数。),因此不能够根据class反序列化之后实例化。因此这种写法是最完美的单例模式。
 * 
 * @date 2021/8/30 15:02
 */
public class SingletonDemo9 {

	public SingletonDemo9() {
	}

	public static SingletonDemo9 getInstance() {
		return Sigleton.INSTANCE.getInstance();
	}

	public static void main(String[] args) {
		IntStream.range(0, 100).forEach(i -> {
			new Thread(() -> {
				System.out.println(SingletonDemo9.getInstance().hashCode());
			}).start();
		});
	}


	private enum Sigleton {
		INSTANCE;

		private final SingletonDemo9 instance;

		Sigleton() {
			instance = new SingletonDemo9();
		}

		public SingletonDemo9 getInstance() {
			return instance;
		}
	}
}

复制代码

在《effective java》中还有一种更简单的写法,那就是枚举。也是《effective java》作者最为推崇的方法。 这种方法不仅可以解决线程同步问题,还可以防止反序列化。 枚举类由于没有构造方法(枚举是java中约定的特殊格式,因此不需要构造函数。),因此不能够根据class反序列化之后实例化。因此这种写法是最完美的单例模式。

Supongo que te gusta

Origin juejin.im/post/7084997972403945503
Recomendado
Clasificación