前言:
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。本文是23种设计模式系列的第一篇,意在深入浅出的给大家介绍设计模式。
目录
概念:
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。顾名思义一种只能有单个实例对象的类,我们称之为单例类。要想实现在内存中只能有一份实例,单例类必须封装构造方法,让调用者无法直接构造单例类的实例,同时为了让调用者获取到单例类的实例,因此单例类必须自己实例化本身,并且提供唯一的静态方法让调用者可以通过类名直接调用该方法从而获取到单例类的实例。
注意:
1:必须保证在内存中单例类只能有一份实例对象。
2:单例类必须负责单例类本身的实例化。
3:单例类必须提供唯一的静态方法供调用者调用。
实现方式
一:饿汉式
方式一:
public class Mgr01 {
private static final Mgr01 INSTANCE=new Mgr01();
private Mgr01(){};
public static Mgr01 getInstance(){
return INSTANCE;
}
}
方式二:
//和写法一本质上是一样的
public class Mgr02 {
private static Mgr02 INSTANCE;
static {
INSTANCE=new Mgr02();
}
private Mgr02(){};
public static Mgr02 getInstance(){
return INSTANCE;
}
}
💡:分析
饿汉式的单例模式,在类加载的时候单例类的实例被加载到内存中去。JVM会保证其内存安全,因为一个类只会被加载一次。
是否线程安全:线程安全。
缺点:无论是否使用到都会创建该单例类的实例,但是一般情况下我们在代码中写了这个单例类一般都要使用,所以这个缺点无关紧要。
推荐使用这种方法,简单易操作。
二:懒汉式
方法一的缺点是无论是否使用都会创建实例,而方法二解决了这个问题,懒汉式单例模式会在使用的时候再去实例化对象。
public class Mgr03 {
public static Mgr03 INSTANCE;
private Mgr03(){};
public static Mgr03 getInstance(){
if(INSTANCE==null){
INSTANCE=new Mgr03();
}
return INSTANCE;
}
}
💡:这种懒汉式的实现策略虽然在真正使用的使用在实例化对象,但是却造成了线程不安全的情况。即多个线程同时调用 getInstance() 方法时,会创建多个实例对象违背了单例模式的条件。
🔑:我们可以通过多线程的方式来测试。
public class Mgr03 {
public static Mgr03 INSTANCE;
private Mgr03(){};
public static Mgr03 getInstance(){
if(INSTANCE==null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new Mgr03();
}
return INSTANCE;
}
//测试线程不安全问题
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(()->
System.out.println(Mgr03.getInstance().hashCode())).start();
}
}
}
运行截图:
运行结果显示不同线程打印的hashcode不同,说明当前单例类创建了多个实例对象。
优点:单例类在真正被用到的时候再实例对象,减少了内存的开销。
缺点:弥补了懒汉式的不足,但带来的线程安全的问题。
三:通过加锁来解决懒汉式的线程不安全问题
我们可以通过给 getInstance() 方法上锁,就可以保证每次只有一个线程执行该方法,那么第一个给该方法上锁的线程会实例对象,等到后面所有的线程执行该方法的时候,由于对象已经实例化了,就不会重复实例对象,解决了懒汉式存在的问题。
代码演示:
public class Mgr04 {
public static Mgr04 INSTANCE;
private Mgr04(){};
public static synchronized Mgr04 getInstance(){
if(INSTANCE==null){
INSTANCE=new Mgr04();
}
return INSTANCE;
}
//测试线程是否安全
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(()->
System.out.println(Mgr04.getInstance().hashCode())).start();
}
}
}
运行截图:
由运行结果可以看出不同的线程打印的hashcode均相同,说明该单例类只实例的一个对象。
优点:在懒汉式的基础上,解决了其线程不安全的问题。
缺点: 如果getInstance() 方法存在大量被调用的情况,每次调用的过程中都会经历上锁和解锁的过程,这会降低代码的执行效率。
四:减少同步代码块来提高效率
解决懒汉式的线程不安全的时候我们采用了方法整体加锁,这会降低效率,于是我们想到能否对代码块加锁,通过减少同步代码块的形式提高效率。
代码演示:
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05(){};
public static Mgr05 getInstance(){
if(INSTANCE==null){
synchronized (Mgr05.class){
INSTANCE=new Mgr05();
}
}
return INSTANCE;
}
}
测试线程是否安全:
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05(){};
public static Mgr05 getInstance(){
if(INSTANCE==null){
synchronized (Mgr05.class){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new Mgr05();
}
}
return INSTANCE;
}
//测试线程是否安全
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(()->
System.out.println(Mgr05.getInstance().hashCode())).start();
}
}
}
运行截图:
不同线程打印出的hashcode不一致,说明不同线程创建了不同的单例对象,违背了单例类只能有一个实例的规则。
我们通过对局部代码块加锁来减少同步代码块从而提高效率结果失败了。
五: 双重检查来保证线程安全
上一个方法想要通过减少同步代码块的方式提高效率结果失败了,但能否有一种策略确实能够减少同步代码块同时又能保证线程安全呢?
这时我们就可以通过双重检查实现既减少同步代码块提高效率,又能保证线程安全。
代码演示:
//我们可以加入双检查来确保线程安全
//来确保减少同步代码块的方式可行
public class Mgr06 {
private static Mgr06 INSTANCE;
private Mgr06(){};
public static Mgr06 getInstance(){
if(INSTANCE==null){
synchronized (Mgr06.class){
if(INSTANCE==null){
INSTANCE=new Mgr06();
}
}
}
return INSTANCE;
}
}
测试线程是否安全:
//我们可以加入双检查来确保线程安全
//来确保减少同步代码块的方式可行
public class Mgr06 {
private static Mgr06 INSTANCE;
private Mgr06(){};
public static Mgr06 getInstance(){
if(INSTANCE==null){
synchronized (Mgr06.class){
if(INSTANCE==null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE=new Mgr06();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(()->
System.out.println(Mgr06.getInstance().hashCode())).start();
}
}
}
运行截图:
不同线程打印出的hashcode一致,说明不同线程使用的都是同一个对象,线程安全。
优点:减少同步代码块提高效率。线程安全。
六:静态内部类
我们可以通过静态内部类的形式来实现单例,既线程安全,又可以在使用的时候进行实例对象。
代码演示:
public class Mgr07 {
private Mgr07(){};
private static class Mgr07Holder{
private static final Mgr07 INSTANCE=new Mgr07();
}
public static Mgr07 getInstance(){
return Mgr07Holder.INSTANCE;
}
}
通过静态内部类的方式实现单例,JVM会保证单例类只有一份实例,同时又是线程安全的。
七:枚举单例
我们可以通过枚举对象的方式保证单例类只有一个实例对象。这种写法可以称作完美的单例写法,既可以解决线程安全,又可以解决同步问题。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式。
public enum Mgr08 {
INSTANCE;
public void f(){
/*
* 业务逻辑
* */
}
}
🔑:因为枚举类没有构造方法,即便是通过反射也无法在类外创建实例,绝对防止多次实例化,代码简洁优美,建议使用枚举单例。
总结:
在一般情况下建议使用饿汉式和枚举单例来实现单例模式。