java架构之路(多线程)大厂方式手写单例模式

上期回顾:

  上次博客我们说了我们的volatile关键字,我们知道volatile可以保证我们变量被修改马上刷回主存,并且可以有效的防止指令重排序,思想就是加了我们的内存屏障,再后面的多线程博客里还有说到很多的屏障问题。

  volatile虽然好用,但是别用的太多,咱们就这样想啊,一个被volatile修饰的变量持续性的在修改,每次修改都要及时的刷回主内存,我们讲JMM时,我们的CPU和主内存之间是通过总线来连接的,也就是说,每次我们的volatile变量改变了以后都需要经过总线,“道路就那么宽,持续性的通车”,一定会造成堵车的,也就是我们的说的总线风暴。所以使用volatile还是需要注意的。

单例模式:

  属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例),就是说每次我们创建的对象成功以后,在一个线程中有且仅有一个对象在正常使用。可以分为懒汉式和饿汉式。

  懒汉式就是什么意思呢,创建时并没有实例化对象,而是调用时才会被实例化。我们来看一下简单的代码。

public class LasySingletonMode {
    public static void main(String[] args) {
        LasySingleton instnace = LasySingleton.getInstnace();
    }
}

class LasySingleton {
    /**
     * 私有化构造方法,禁止外部直接new对象
     */
    private LasySingleton() {
    }

    /**
     * 给予一个对象作为返回值使用
     */
    private static LasySingleton instnace;

    /**
     * 给予一个获取对象的入口
     *
     * @return LasySingleton对象
     */
    public static LasySingleton getInstnace() {
        if (null == instnace) {
            instnace = new LasySingleton();
        }
        return instnace;
    }
}

  看起来很简单的样子,私有化构造方法,给予入口,返回对象,差不多就这样就可以了,但是有一个问题,如果是多线程呢?

public static LasySingleton getInstnace() {
  if (null == instnace) {
    instnace = new LasySingleton();
  }
  return instnace;
}

  我们假想两个线程,要一起运行这段代码,线程A进来了,看到instnace是null的,ε=(´ο`*)))唉,线程B进来看见instnace也是null的(因为线程A还没有运行到instnace = new LasySingleton()这个代码),这时就会造成线程A,B创建了两个对象出来,也就不符合我们的单例模式了,我们来改一下代码。

public static LasySingleton getInstnace() {
    if (null == instnace) {
        synchronized (LasySingleton.class){
            instnace = new LasySingleton();
        }
    }
    return instnace;
}

  这样貌似就可以了,就算是两个线程进来,也只有一个对象可以拿到synchronized锁,就不会产生new 两个对象的行为了,其实不然啊,我们还是两个线程来访问我们的这段代码,线程A和线程B,两个线程来了一看,对象是null的,需要创建啊,于是线程A拿到锁,开始创建,线程B继续等待,线程A创建完成,返回对象,将锁释放,这时线程B可以获取到锁(因为null == instnace判断已经通过了,在if里面进行的线程等待),这时线程B还是会创建一个对象的,这显然还是不符合我们的单例模式啊,我们来继续改造。

public static LasySingleton getInstnace() {
    if (null == instnace) {
        synchronized (LasySingleton.class){
            if (null == instnace) {
                instnace = new LasySingleton();
            }
        }
    }
    return instnace;
}

  这次基本就可以了吧,回想一下我们上次的volatile有序性,难道真的这样就可以了吗?instnace = new LasySingleton()是一个原子操作吗?有时候你面试小厂,这样真的就可以了,我们来继续深挖一下代码。看一下程序的汇编指令码,首先找我们的class文件。运行javap -c ****.class。

E:\IdeaProjects\tuling-mvc-3\target\classes\com\tuling\control>javap -c LasySingleton.class
Compiled from "LasySingletonMode.java"
class com.tuling.control.LasySingleton {
  public static com.tuling.control.LasySingleton getInstnace();
    Code:
       0: aconst_null
       1: getstatic     #2                  // Field instnace:Lcom/tuling/control/LasySingleton;
       4: if_acmpne     17
       7: new           #3                  // class com/tuling/control/LasySingleton
      10: dup
      11: invokespecial #4                  // Method "<init>":()V
      14: putstatic     #2                  // Field instnace:Lcom/tuling/control/LasySingleton;
      17: getstatic     #2                  // Field instnace:Lcom/tuling/control/LasySingleton;
      20: areturn
}

   不是很好理解啊,我们只想看instnace = new LasySingleton()是不是一个原子操作,我们可以这样来做,创建一个最简单的类。

public class Demo {
    public static void main(String[] args) {
        Demo demo = new Demo();
    }
}

然后我们运行javap -c -v ***.class

E:\IdeaProjects\tuling-mvc-3\target\classes>javap -c -v Demo.class
Classfile /E:/IdeaProjects/tuling-mvc-3/target/classes/Demo.class
  Last modified 2020-1-13; size 389 bytes
  MD5 checksum f8b222a4559c4bf7ea05ef086bd3198c
  Compiled from "Demo.java"
public class Demo
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#19         // java/lang/Object."<init>":()V
   #2 = Class              #20            // Demo
   #3 = Methodref          #2.#19         // Demo."<init>":()V
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               LDemo;
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               args
  #15 = Utf8               [Ljava/lang/String;
  #16 = Utf8               demo
  #17 = Utf8               SourceFile
  #18 = Utf8               Demo.java
  #19 = NameAndType        #5:#6          // "<init>":()V
  #20 = Utf8               Demo
  #21 = Utf8               java/lang/Object
{
  public Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LDemo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Demo
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
            8       1     1  demo   LDemo;
}
SourceFile: "Demo.java"

E:\IdeaProjects\tuling-mvc-3\target\classes>

结果是这样的,我们来分析一下代码,先看这个

 0: new           #2                  // class Demo

就是什么意思呢?我们要给予Demo对象在对空间上开辟一个空间,并且返回内存地址,指向我们的操作数栈的Demo对象

3: dup

是一个对象复制的过程。

 4: invokespecial #3                  // Method "<init>":()V

见名知意,init是一个初始化过程,我们会把我们的刚才开辟的栈空间进行一个初始化,

7: astore_1

  这个就是一个赋值的过程,刚才我们有个复制的操作对吧,这时会把我们复制的一个对象赋值给我们的栈空间上的Demo,是不是有点蒙圈了,别急,后面的简单。

  这是一个对象的初始化过程,在我的JVM系列博客简单的说过一点,后面我会详细的去说这个,总结起来就是三个过程。

1.开辟空间
2.初始化空间
3.给引用赋值

  这个代码一般情况下,会按照123的顺序去执行的,但是超高并发的场景下,可能会变为132,考虑一下是不是,我们的as-if-serial,132的执行顺序在单线程的场景下也是合理的,如果真的出现了132的情况,会造成什么后果呢?回到我们的单例模式,所以说我们上面单例模式代码还需要改。

public class LasySingletonMode {
    public static void main(String[] args) {
        LasySingleton instnace = LasySingleton.getInstnace();
    }
}

class LasySingleton {

    /**
     * 私有化构造方法,禁止外部直接new对象
     */
    private LasySingleton() {
    }

    /**
     * 给予一个对象作为返回值使用
     */
    private static volatile LasySingleton instnace;

    /**
     * 给予一个获取对象的入口
     *
     * @return LasySingleton对象
     */
    public static LasySingleton getInstnace() {
        if (null == instnace) {
            synchronized (LasySingleton.class) {
                if (null == instnace) {
                    instnace = new LasySingleton();
                }
            }
        }
        return instnace;
    }
}

  这样来写,就是一个满分的单例模式了,无论出于什么样的考虑,都是满足条件的。也说明你真的理解了我们的volatile关键字。

  饿汉式相当于懒汉式就简单很多了,不需要考虑那么多了。

package com.tuling.control;

public class HungrySingletonMode {
    public static void main(String[] args) {
        String name = HungrySingleton.name;
        System.out.println(name);
    }
}

class HungrySingleton {

    /**
     * 私有化构造方法,禁止外部直接new对象
     */
    private HungrySingleton() {
    }

    private static HungrySingleton instnace =  new  HungrySingleton();

    public static String name = "XXX";

    static{
        System.out.println("我被创建了");
    }

    public static HungrySingleton getInstance(){
        return instnace;
    }
}

  很简单,也不是属于我们多线程范畴该说的,这里就是带着说了一下,就是当我们调用内部方法时,会主动触发对象的创建,这样就是饿汉模式。

原文地址:https://www.cnblogs.com/cxiaocai/p/12189488.html

时间: 2024-10-10 05:05:59

java架构之路(多线程)大厂方式手写单例模式的相关文章

Java面试必备:手写单例模式

面试官:请手写下几种常见的单例模式 我:好的(面带微笑),心里暗喜(送分题). 没成想提笔便写出了如此豪放的代码,不堪回首,请原谅我的不羁! 此篇整理了几种常见的单例模式代码示例,再有面试官让手撕单例模式,便能心中有码,下笔有神. 为什么要有单例模式 实际编程应用场景中,有一些对象其实我们只需要一个,比如线程池对象.缓存.系统全局配置对象等.这样可以就保证一个在全局使用的类不被频繁地创建与销毁,节省系统资源. 实现单例模式的几个要点 首先要确保全局只有一个类的实例. 要保证这一点,至少类的构造器

java架构之路(多线程)AQS之ReetrantLock显示锁的使用和底层源码解读

锁的粗化和锁的消除 这个本来应该是在synchronized里面去说的,忘记了,不是很重要,但是需要知道有这么一个东西啦. 我们先来演示一下锁的粗化: StringBuffer sb = new StringBuffer(); public void lockCoarseningMethod(){ //jvm的优化,锁的粗化 sb.append("1"); sb.append("2"); sb.append("3"); sb.append(&qu

图灵学院java架构之路-VIP(五)nginx的安装和基本配置

Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和Unix的多用户.多任务.支持多线程和多CPU的操作系统.它能运行主要的Unix工具软件.应用程序和网络协议.它支持32位和64位硬件.Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统. 一,安装环境准备: 1.linux 内核2.6及以上版本:使用uname -a查看即可. 2.GCC编辑器. GCC可以用来编译C语言程序.Nginx没有直接提供二级制可执行程序,只能下载源码进行编译

java架构之路-(Redis专题)SpringBoot连接Redis超简单

上次我们搭建了Redis的主从架构,哨兵架构以及我们的集群架构,但是我们一直还未投入到实战中去,这次我们用jedis和springboot两种方式来操作一下我们的redis 主从架构 如何配置我上次已经讲过了,https://www.cnblogs.com/cxiaocai/p/11711377.html.我们这次主要看如何用java来操作redis,先来复习一下上次的配置,准备三台服务器,安装redis,保证互通,两台改为slave,配置replicaof IP 端口,主从复制是通过rdb文件

java架构之路-(Redis专题)Redis的高性能和持久化

上次我们简单的说了一下我们的redis的安装和使用,这次我们来说说redis为什么那么快和持久化数据 在我们现有的redis中(5.0.*之前的版本),Redis都是单线程的,那么单线程的Redis为什么还会有那么高的效率呢?因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换中性能损耗的问题,正因为Redis是单线程,所以我们要小心使用Redis指令,对于那些耗时的指令(比如keys),我们一定要谨慎使用. 在并发环境中,我们Redis的单线程并不是线程1请求

java架构之路-(面试篇)JVM虚拟机面试大全

下文连接比较多啊,都是我过整理的博客,很多答案都在博客里有详细说明,理解记忆是最扎实的记忆.而且我的答案不一定是最准确的,但是我的答案不会让你失望,而且几乎每个答案都是问题的扩展答案. 1.JVM内存模型 答:https://www.cnblogs.com/cxiaocai/p/11483629.html 先经过类装载子系统装载,经由验证,准备,解析,初始化四个过程.方法被调用,方法进栈,对象放在堆,栈内存的引用指向我们的对象,方法逐行执行,由我们的程序计数器来控制.元空间放置静态方法,不占用j

java架构之路-(面试篇)Mysql面试大全

说一下mysql比较宏观的面试,具体咋写sql的这里就不过多举例了.后面我还会给出一个关于mysql面试优化的试题,这里主要说的索引和B+Tree结构,很少提到我们的集群配置优化方案. 1.索引是什么?有什么作用以及缺点 答:索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息.也可以理解为索引就是一本书的目录,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加.索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建

java架构之路-(MQ专题)RabbitMQ安装和基本使用

RabbitMQ安装 我这里安装是使用阿里云的CentOS7.5来安装的,使用CentOS版本低于7的可能会报错. 1.安装rabbitmq所需要的依赖包 输入$ yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make gcc gcc- c++ kernel-devel m4 ncurses-devel tk tc xz 2.下载安装包 $ wget www.rabbitmq.com/relea

手写单例模式

/** * 单例模式 * @author gy2018 * */public class SingletonDemo {    /**     * 所谓的单例模式,就是整个程序有且仅有一个实例.     * 该类负责创建自己的对象 并确保只有一个对象被创建.     * 在java中,一般常用在工具类的实现或创建对象 需要消耗资源     *      * 特点:     * 类构造器私有     * 持有自己类型的属性     * 对外提供获取实例的静态方法     */