当前位置: 首页 > 后端技术 > Java

这几种单例模式写法你知道几种

时间:2023-04-01 21:39:21 Java

你知道怎么写这些单例模式吗?实例。当只需要一个对象来协调整个系统的操作时,这种模式非常有用。它描述了如何解决重复出现的设计问题,例如我们项目中的配置工具类、日志工具类等。如何设计单例模式?1、如何控制单例类的实例化2、如何保证只有一个实例通过以下措施解决这些问题:私有构造函数,类的实例不对外开放,javatrains自己完成这个内部操作,保证类永远不会从类外实例化,避免从类外新建实例。实例通常存储为私有静态变量,提供返回实例引用的静态方法。如果是在多线程环境下,使用锁或者内部类来解决线程安全问题。2、单例类有什么特点?私有构造函数将阻止从类外部实例化新对象。它应该只有一个实例。这是通过在类中提供一个实例来完成的,防止外部类或子类创建实例。这是通过在Java中将构造函数设为私有来实现的,这样任何类都无法访问构造函数,因此无法实例化它。单例应该是全局可访问的单例类的实例应该是全局可访问的,这样每个类都可以使用它。在Java中,它是通过使实例的访问说明符公开来完成的。节省内存,减少GC,因为全局最多只有一个实例,避免到处都是新对象,造成内存浪费,GC。使用单例模式,可以避免这些问题。3.单例模式的8种写法。下面给大家介绍一下单例模式的8种写法。每个都有自己的优点和缺点。我们在选择单例模式时的选择标准或者单例模式优劣的评价通常是根据以下两个因素来衡量的:1、行为在多线程环境下是否线程安全2、饥饿和懒惰3.编码是否优雅(是不是更直观理解)1.饥饿式线程安全publicclassSingleTon{privatestaticfinalSingleTonINSTANCE=newSingleTon();privateSingleTon(){}publicstaticSingleTongetInstance(){返回实例;}publicstaticvoidmain(String[]args){SingleToninstance1=SingleTon.getInstance();SingleToninstance2=SingleTon.getInstance();System.out.println(instance1==instance2);}}这种写法非常简单实用,值得推荐,唯一的缺点就是懒惰,也就是说,不管这个方法是否需要,加载类时将生成一个对象。另外,这种写法是线程安全的。类加载到内存后,实例化一个单例,JVM保证线程安全。2.饿了么中文线程安全(变体写法)。公共类SingleTon{privatestaticfinalSingleTonINSTANCE;static{INSTANCE=newSingleTon();}privateSingleTon(){}publicstaticSingleTongetInstance(){返回实例;}publicstaticvoidmain(String[]args){SingleToninstance1=SingleTon.getInstance();SingleToninstance2=SingleTon.getInstance();System.out.println(instance1==instance2);}}3.惰性线程不安全。publicclassSingleTon{privatestaticSingleToninstance;privateSingleTon(){}publicstaticSingleTongetInstance(){if(instance==null){instance=newSingleTon();}returninstance;}publicstaticvoidmain(String[]args){SingleToninstance1=SingleTon.getInstance();SingleToninstance2=SingleTon.getInstance();System.out.println(instance1==instance2);//开100个线程比较是否是同一个对象for(inti=0;i<100;i++){newThread(()->System.out.println(SingleTon.getInstance().hashCode()))。开始();这种写法虽然达到了按需初始化的目的,但是带来了线程不安全的问题。至于为什么上面的例子在并发的情况下不安全呢?//开100个线程比较是否是同一个对象for(inti=0;i<100;i++){newThread(()->System.out.println(SingleTon.getInstance().hashCode())).start();}为了让效果更直观,我们稍微修改一下getInstance方法,每个线程进入后,休眠一毫秒。这样做的目的是为每个线程获得尽可能多的CPU时间片来执行。代码如下publicstaticSingleTongetInstance(){if(instance==null){try{Thread.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}instance=newSingleTon();}returninstance;}执行结果如下。我们可以在上面提到的单例写法中创建多个实例。至于为什么,这里需要稍微解释一下。这就涉及到导致线程不安全的同步问题:并发访问时,第一个调用getInstance方法的线程t1,判断单例为null后,线程A进入if块,准备创建实例,但同时,另一个线程B在线程A创建实例之前执行单例。如果判断为null,此时singleton还是null,所以线程B也会进入if块创建实例。这时候,问题就出现了。两个线程进入if块创建实例,导致单例模式Notasingleton。注意:这里,线程挂起是通过休眠一毫秒来模拟的。为了在实例初始化后解决这个问题,我们可以采取加锁的措施,于是就有了如下的写法4.Lazy-stylethreadsafety(coarse-grainedSynchronized)。publicclassSingleTon{privatestaticSingleToninstance;privateSingleTon(){}publicstaticSingleTonsynchronizedgetInstance(){if(instance==null){instance=newSingleTon();}returninstance;}publicstaticvoidmain(String[]args){SingleToninstance1=SingleTon.getInstance();SingleToninstance2=SingleTon.getInstance();System.out.println(instance1==instance2);//开100个线程比较是否是同一个对象for(inti=0;i<100;i++){newThread(()->System.out.println(SingleTon.getInstance().hashCode()))。开始();}}}线程由于第三个方法出现Unsafe问题,所以在getInstance方法中加入synchronized来保证多线程环境下的线程安全。这种方式虽然解决了多线程的问题,但是效率比较低。因为整个方法是锁住的,其他传入的现金只能阻塞等待,会造成很多无谓的等待。那么可能有人会想,是不是可以把锁的粒度再细一点,只锁相关的代码块呢?于是就有了第五种写法。5、懒线程不安全(同步代码块)单身人士();}}returninstance;}publicstaticvoidmain(String[]args){SingleToninstance1=SingleTon.getInstance();SingleToninstance2=SingleTon.getInstance();System.out.println(instance1==instance2);//开100个线程比较是否是同一个对象for(inti=0;i<100;i++){newThread(()->System.out.println(SingleTon.getInstance().hashCode()))。开始();}}}并发访问时,第一个调用getInstance方法的线程t1,判断实例为null后,线程A进入if块,持有synchronized锁。但同时另一个线程t2在线程t1创建实例之前判断实例是否为null。此时实例还是null,所以线程t2也会进入if块创建实例,它会在synchronized代码中被阻塞,一直等到t1释放锁。这时候,问题就出来了。两个线程都实例化新对象。出现这个问题的原因是线程进入if块等待synchronized锁的过程中,之前的线程可能已经创建了实例,所以进入synchronized代码块后需要判断一次,所以就有了下面的doublechecklock的写法。6.惰性线程安全(双重检查和锁定)(实例==null){实例=newSingleTon();}}}returninstance;}publicstaticvoidmain(String[]args){SingleToninstance1=SingleTon.getInstance();SingleToninstance2=SingleTon.getInstance();System.out.println(instance1==instance2);//开100个线程比较是否是同一个对象for(inti=0;i<100;i++){newThread(()->System.out.println(SingleTon.getInstance().hashCode()))。开始();}}}这种写法基本完美,但可能需要说明以下几点:?第一个空判断(outerlayer)的作用??二次判断(内层)的作用是什么??为什么变量被修改为volatile?首层判断(外层)的作用首先想想是否可以去掉最外层的判断?答案是:是的,仔细观察你会发现,最外层的判断与能否安全正确地生成单例无关!!!它的作用是避免每次进来都加锁或者等待锁,加上同步代码块外的判断,省了很多工作。当我们的单例类实例化一个单例后,其他所有后续请求都不需要进入同步代码块后继续执行,只返回我们已经生成的实例即可,即实例还在只有在没有创建的时候才同步,否则直接返回,省去了很多不必要的线程等待时间,所以最外层的判断也算是对提高性能有帮助的。第二个空判断(内层)假设我们去掉了同步块是否为null的判断,有这样一种情况,A线程和B线程都判断同步块外的实例为null。结果,t1线程先获取到线程锁,进入同步块,然后t1线程会Create实例。这个时候实例已经赋值给实例了。t1线程退出同步块,直接返回到第一个创建的实例。此时t2线程获得线程锁,进入同步块。这个时候t1线程其实已经创建好了。instance,正常情况下t2线程应该直接返回,但是由于同步块中没有判断是否为null,是直接创建实例的语句,所以t2线程也会创建一个实例返回,并且这时,创建了多个实例Case。之所以将变量修改为volatile,是因为虚拟机在执行创建实例这一步的时候,实际上是把它分成了好几个步骤,也就是说创建一个新的对象并不是一个原子操作。在某些JVM中,上述做法没有问题,但在某些情况下会出现莫名其妙的错误。首先你要明白,JVM在创建一个新的对象时,主要经过三个步骤。1.分配内存2.初始化构造函数3.将对象指向分配内存的地址因为只有一个newinstance操作涉及三个子操作,所以生成一个对象的操作不是原子操作。实际情况是JVM会对以上三个指令进行调优,其中之一就是调整指令的执行顺序(这个操作是由JIT编译器完成的)。因此,指令排序时可能会出现问题。如果步骤2和步骤3颠倒,先将分配的内存地址指向实例,再初始化构造函数。这时下面的线程请求getInstance方法,它会认为实例对象已经实例化了,直接返回一个引用。如果此时构造函数还没有初始化,线程使用了instance,就会出现线程指向一个构造函数未初始化的对象的现象,就会出错。7、静态内部类的方式(基本完美)(String[]args){SingleToninstance1=SingleTon.getInstance();SingleToninstance2=SingleTon.getInstance();System.out.println(instance1==instance2);//开100个线程比较是否是同一个对象for(inti=0;i<100;i++){newThread(()->System.out.println(SingleTon.getInstance().hashCode()))。开始();}}}因为一个类的静态属性只有在该类第一次加载时才会被初始化,这是JVM为我们保证的,所以我们不用担心并发访问的问题。所以当初始化进行到一半的时候,其他线程是不能用的,因为JVM会帮我们强行同步这个过程。?另外,由于静态变量只初始化一次,所以单例仍然是单例。8.枚举类型的单例模式(完美到...)publicEnumSingleTon{INSTANCE;publicstaticvoidmain(String[]args){//开100个线程比较是否是同一个对象for(inti=0;i<100;i++){newThread(()->System.out.println(SingleTon.getInstance().hashCode())).start();}}}这种写法从语法上来说是完美的。解决了以上7种写法的问题。即我们可以通过反射生成新的实例。但是枚举的这种写法不能通过反射产生新的实例,因为枚举没有public构造方法。