Java多线程案例之单例模式懒汉+饿汉+枚举

作者:??未见花闻???? 时间:2021-11-07 05:18:01 

前言:

本篇文章将介绍Java多线程中的几个典型案例之单例模式,所谓单例模式,就是一个类只有一个实例对象,本文将着重介绍在多线程的背景下,单例模式的简单实现。

1.单例模式概述

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例,即一个类只有一个对象实例。

单例模式有两种典型的实现,一是饿汉模式,二是懒汉模式,饿汉模式中的“饿”并不是真的表示“饿”,更加准确的来说应该是表示“急”,那就是一个单例类还没被使用,它的单例对象就已经创建好了,而懒汉模式,要等到使用这个单例类时才创建单例对象。

单例模式中的单例类,只能拥有一个实例对象,又static修饰的成员是属于类的,也就是只有一份,所以我们可以使用static修饰的成员变量保存实例对象的引用。

2.单例模式的简单实现

2.1饿汉模式

由于单例模式中,一个类只能拥有一个实例对象,所以需要将类构造方法封装,防止类被创建多个实例对象,但是在使用该类时必须要得到该类的实例对象,因此我们得创建一个获取该唯一实例对象的方法getInstance

而对于该类的实例对象,在类中我们可以使用属于类的成员变量来保存(即static成员变量)。

//单例模式 - 饿汉模式
class HungrySingleton {
   //1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
   private static final HungrySingleton instance = new HungrySingleton();

//2.封装构造方法,防止该类被实例出新的对象
   private HungrySingleton() {}

//3.获取该类的唯一实例对象
   public HungrySingleton getInstance() {
       return instance;
   }
}

多线程情况下,对于上述简单实现的饿汉式单例模式,只需要考虑getInstance方法是否线程安全即可,由于该方法就一句返回语句,即一次读操作,而读操作是线程安全的,所以getInstance方法也就是线程安全的,综上饿汉式单例模式是线程安全的。

2.2懒汉模式

懒汉模式相比于饿汉模式,区别就是实例对象创建时机不同,懒汉模式需要等到第一次使用时才创建实例对象,所以仅仅只需要修改获取对象的方法即可。

不考虑多线程情况,懒汉模式实现代码如下:

//单例模式 - 懒汉模式
class SlackerSingleton {
   //1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
   //懒汉单例模式是在使用的时候创建对象,因此初始时对象不应该被创建
   private static SlackerSingleton instance;

//2.封装构造方法,防止该类被实例出新的对象
   private SlackerSingleton() {}

//3.获取该类的唯一对象,如果没有就创建
   public SlackerSingleton getInstance() {
       if (instance == null) {
           instance = new SlackerSingleton();
       }
       return instance;
   }
}

多线程情况下,由于getInstance方法中存在两次读(一次判断一次返回)操作一次写操作(修改intsance变量的值),instance变量为初始化时(即instance=null)可能会存在多个线程进入判断语句,这样该类可能会被实例出多个对象,所以上述实现的懒汉式单例模式是线程不安全的。

造成线程不安全的代码段为if语句里面的读操作和instance的修改操作,所以我们需要对这段代码进行加锁,然后就得到了线程安全的懒汉模式:

//多线程情况下饿汉模式获取对象时只读不修改,所以是线程安全的
//多线程情况下懒汉模式获取对象时存在两次读操作,分别为判断instance是否为null和返回instance,除了读操作还存在修改操作,即新建对象并使instance指向该对象
//懒汉模式对象还未初始化的时候,可能会存在多个线程进入判断语句,会导致实例出多个对象,因此懒汉单例模式是线程不安全的。

//线程安全单例模式 - 懒汉模式
class SafeSlackerSingleton {
   //1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
   //懒汉单例模式是在使用的时候创建对象,因此初始时对象不应该被创建
   private static SafeSlackerSingleton instance;

//2.封装构造方法,防止该类被实例出新的对象
   private SafeSlackerSingleton() {}

//3.获取该类的唯一对象,如果没有就创建
   public SafeSlackerSingleton getInstance() {
       synchronized (SafeSlackerSingleton.class) {
           if (instance == null) {
               instance = new SafeSlackerSingleton();
           }
       }
       return instance;
   }
}

但是!上述线程安全问题只出现在instance没有初始化的时候,如果instance已经初始化了,那个判断语句就是个摆设,就和饿汉模式一样,就是线程安全的了,如果按照上面的代码处理线程安全问题,不论instance是否已经初始化,都要进行加锁,因此会使锁竞争加剧,消耗没有必要消耗的资源,所以在加锁前需要先判断一下instance是否已经初始化,如果为初始化就进行加锁。

按照上述方案得到以下关于获取对象的方法代码:

public SafeSlackerSingletonPlus getInstance() {
       //判断instance是否初始化,如果已经初始化了,那么该方法只有两个读操作,本身就是线程安全的,不需要加锁了,这样能减少锁竞争,提高效率
       if (instance == null) {
           synchronized (SafeSlackerSingletonPlus.class) {
               if (instance == null) {
                   instance = new SafeSlackerSingletonPlus();
               }
           }
       }
       return instance;
   }

到这里线程安全的问题是解决了,但是别忘了编译器它是不信任你的,它会对你写的代码进行优化! 上面所写的代码需要判断instance==null,而多线程情况下,很可能频繁进行判断,这时候线程不会去读内存中的数据,而会直接去寄存器读数据,这时候instance值变化时,线程完全感知不到!造成内存可见性问题,为了解决该问题需要使用关键字volatile修饰instance变量,防止编译器优化,从而保证内存可见性。

//线程安全优化单例模式 - 懒汉模式
class SafeSlackerSingletonPlus {
   //1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
   //懒汉单例模式是在使用的时候创建对象,因此初始时对象不应该被创建
   private static volatile SafeSlackerSingletonPlus instance;

//2.封装构造方法,防止该类被实例出新的对象
   private SafeSlackerSingletonPlus() {}

//3.获取该类的唯一对象,如果没有就创建
   public SafeSlackerSingletonPlus getInstance() {
       //判断instance是否初始化,如果已经初始化了,那么该方法只有两个读操作,本身就是线程安全的,不需要加锁了,这样能减少锁竞争,提高效率
       //如果线程很多,频繁进行外层或内层if判断,可能会引发内层可见性问题,因此要给instan变量加上volatile
       if (instance == null) {
           synchronized (SafeSlackerSingletonPlus.class) {
               if (instance == null) {
                   instance = new SafeSlackerSingletonPlus();
               }
           }
       }
       return instance;
   }
}

2.3枚举实现单例模式

除了使用饿汉和懒汉模式还可以使用枚举的方式实现,在《Effective Java》书中有这样一句话:单元素的枚举类型已经成为实现Singleton的最佳方法。 因为枚举就是一个天然的单例,并且枚举类型通过反射都无法获取封装的私有变量,非常安全。

//单元素的枚举类型已经成为实现Singleton的最佳方法
enum  EnumSingleton {
   INSTANCE;
   public void doSomething() {
       System.out.println("完成一些任务!");
   }
}

使用方式:

public class Singleton {
   public static void main(String[] args) {
       EnumSingleton.INSTANCE.doSomething();
   }
}

运行结果:

Java多线程案例之单例模式懒汉+饿汉+枚举

来源:https://juejin.cn/post/7104061773992591373

标签:Java,多线程,单例,模式
0
投稿

猜你喜欢

  • JSON复杂数据处理之Json树形结构数据转Java对象并存储到数据库的实现

    2023-09-17 17:03:59
  • 判断List和Map是否相等并合并List中相同的Map

    2022-12-29 02:11:10
  • Java获取e.printStackTrace()打印的信息方式

    2022-05-18 05:19:26
  • Java springboot yaml语法注解

    2023-06-17 08:13:35
  • Spring注解之@Lazy注解使用解析

    2023-08-28 23:12:23
  • Java实现企业员工管理系统

    2023-08-22 16:44:50
  • SpringBoot2之PUT请求接收不了参数的解决方案

    2023-08-23 01:32:07
  • 获取Java线程转储的常用方法(推荐)

    2023-05-15 02:30:19
  • java 使用foreach遍历集合元素的实例

    2022-11-17 09:24:58
  • Spring Security实现基于RBAC的权限表达式动态访问控制的操作方法

    2023-11-29 16:03:25
  • Java如何给Word文档添加多行文字水印

    2023-11-10 07:40:58
  • 解读List list=new ArrayList()是怎么回事

    2022-04-17 12:28:22
  • SpringCloud Edgware.SR3版本中Ribbon的timeout设置方法

    2023-03-07 10:45:46
  • Maven分模块开发执行指令失败的问题

    2021-07-10 19:00:15
  • Java使用新浪微博API通过账号密码方式登陆微博的实例

    2023-09-23 05:35:38
  • Android WindowManger的层级分析详解

    2023-08-05 23:51:40
  • Springboot轻量级的监控组件SpringbootAdmin

    2023-08-25 10:08:31
  • Java中String的JdbcTemplate连接SQLServer数据库的方法

    2022-09-05 00:34:12
  • flutter窗口初始和绘制流程详析

    2023-08-17 21:07:30
  • mybatis-plus使用问题小结

    2023-10-30 06:45:58
  • asp之家 软件编程 m.aspxhome.com