Implémentation et analyse de divers modèles de singleton en JAVA

Cet article a participé à l'événement "Newcomer Creation Ceremony" pour commencer ensemble la route de la création d'or.

@ [table des matières]

Le modèle singleton est le modèle de conception le plus élémentaire dans le processus d'apprentissage des modèles de conception. Fondamentalement, vous apprendrez le modèle singleton dès que vous commencerez à apprendre. En fait, il existe de nombreuses façons d'écrire le modèle singleton dans java. question. Ainsi, quelles méthodes d'écriture peuvent être utilisées et quelles méthodes d'écriture ne peuvent pas être utilisées, ou dans quels scénarios différentes méthodes d'implémentation peuvent-elles être utilisées. Cet article analyse l'implémentation de neuf modèles singletons existants.

1. Mode singleton de style Hungry - utilisant des constantes statiques

code afficher comme ci-dessous:

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);
	}
}

复制代码

L'avantage de cette implémentation est que la méthode d'écriture est simple et que l'instanciation de l'objet requis est complétée dans le processus de chargement de classe, évitant ainsi les problèmes de sécurité des threads.

L'inconvénient est qu'en mode singleton chinois gourmand, peu importe si l'objet requis est utilisé ou non, l'objet sera créé en premier. Si l'objet n'est pas utilisé dans l'ensemble du processus métier, cela entraînera inévitablement un gaspillage de mémoire.

2. Mode singleton de style chinois affamé - utilisant des blocs de code statiques

code afficher comme ci-dessous:

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);
	}
}

复制代码

Le modèle singleton implémenté à la manière des blocs de code statiques est en fait équivalent au modèle singleton implémenté à la manière des constantes statiques, et l'instanciation de l'objet cible est réalisée dans le processus de chargement de classe. Cela évite les problèmes de sécurité des threads.

Ses défauts sont également compatibles avec le mode gourmand des constantes statiques, qui peut entraîner un gaspillage de mémoire.

3. Modèle singleton paresseux - implémentation de base

code afficher comme ci-dessous:

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();
		});
	}
}

复制代码

Le modèle de singleton paresseux le plus élémentaire est illustré ci-dessus. Nous avons seulement besoin de juger dans la méthode getInstance, si (INSTANCE == null), si c'est True, cela signifie que l'objet n'a pas été instancié, et maintenant il peut être instancié directement. Cependant, cette méthode introduit le problème de la sécurité des threads. Dans un environnement multithread, si un thread entre dans le jugement if, l'exécution n'est pas terminée et un autre thread entre également dans le jugement if. À ce stade, et entraînera le retour de plusieurs instances.
Cette approche n'est donc pas recommandée dans un environnement de production. Dans la méthode getInstance, le temps de veille est spécialement ajouté, et l'effet d'exécution multi-thread dans la méthode principale sera très évident. On peut constater que cela se traduira par une sortie de hashcodes différents à chaque fois.

Conclusion finale : bien que cette méthode d'implémentation réduise la surcharge mémoire inutile, elle entraînera des problèmes de sécurité des threads. Dans le cas de la concurrence, une nouvelle instance peut être créée pour chaque appel, cette méthode n'est donc pas recommandée.

4. Modèle de singleton paresseux - verrouillez la méthode

code afficher comme ci-dessous:

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反序列化之后实例化。因此这种写法是最完美的单例模式。

Acho que você gosta

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