Java中的懒惰实例化与急切实例化:哪个更好?

译文
开发 前端
当实例化在资源使用方面开销很大的Java对象时,用户不希望每次使用它们时都必须进行实例化。对于提高性能来说,拥有一个可以在整个系统中共享的现成对象实例要好得多。在这种情况下,懒惰实例化策略非常有效。

​译者 | 李睿

审校 | 孙淑娟

人们需要了解如何在Java程序中使用懒惰实例化和急切实例化。那么,哪种方法更好?这取决于场景。  

当实例化在资源使用方面开销很大的Java对象时,用户不希望每次使用它们时都必须进行实例化。对于提高性能来说,拥有一个可以在整个系统中共享的现成对象实例要好得多。在这种情况下,懒惰实例化策略非常有效。

懒惰实例化也有一些缺点,而在某些系统中,采用急切实例化方法更好。在急切实例化中,通常在应用程序启动后立即实例化对象一次。这两种方法是不同的。而在某些情况下,某种方法最有效。

本文将介绍这两种实例化Java对象的方法。首先看到代码示例,然后用Java代码挑战测试所学到的内容。此外,还将讨论懒惰实例化与急切实例化的优缺点。

1.懒惰实例化的简单方法

首先,了解创建单个实例并在系统中共享它的简单方法:

public static HeroesDB heroesDB;           // #A
private SingletonNaiveApproach() {} // #B

public HeroesDB getHeroesDB() { // #C
if (heroesDB == null) { // #D
heroesDB = new HeroesDB(); // #E
}

return heroesDB; // #F
}
static class HeroesDB { }
}

下面是代码中发生的情况:

开始(#A),声明一个静态内部类HeroesDB。将变量声明为静态的变量,它可以在应用程序中共享。

下一步(#B),创建一个私有构造函数,以避免从类外部直接实例化。因此,必须使用getHeroes()方法来获取一个实例。

在下一行(#C),看到了有效地从HeroesDB返回实例的方法。

接下来(#D),检查heroesDB实例是否为空。如果是空,将创建一个新实例。否则什么也不做。

最后(#F),返回heroesDB对象实例。

这种方法适用于小型应用程序。然而,在有许多用户的大型多线程应用程序中,很可能会出现数据冲突。在这种情况下,对象可能会被多次实例化,即使检查是否为空。以下进一步探讨为什么会发生这种情况的原因。

2.理解竞态条件

竞态条件是指两个或多个线程并发竞争同一个变量的情况,这可能会导致意外的结果。

在大型多线程应用程序中,许多进程并行并发地运行。在这种类型的应用程序中,有可能在另一个线程实例化一个空对象的同时,一个线程正在询问一个对象是否为空。在这种情况下,有一个竞态条件,这可能导致重复的实例。

可以通过使用synchronized关键字来修复这个问题:

public class SingletonSynchronizedApproach {

public static HeroesDB heroesDB;
private SingletonSynchronizedApproach() {}

public synchronized HeroesDB getHeroesDB() {
if (heroesDB == null) {
heroesDB = new HeroesDB();
}

return heroesDB;
}

static class HeroesDB { }

}

这段代码解决了线程在getHeroesDB()中存在冲突的问题。然而正在同步整个方法。这可能会影响性能,因为每次只有一个线程能够访问整个方法。

以下看看如何解决这个问题。

3.优化的多线程懒惰实例化

要同步getHeroesDB()方法中的策略点,需要在该方法中创建同步块。以下是一个例子:

public class ThreadSafeSynchronized {

public static volatile HeroesDB heroesDB;

public static HeroesDB getHeroesDB() {
if(heroesDB == null) {
synchronized (ThreadSafeSynchronized.class) {
if(heroesDB == null) {
heroesDB = new HeroesDB();
}
}
}
return heroesDB;
}

static class HeroesDB { }
}

左右滑动查看完整代码

在这段代码中,只在实例为空时同步对象的创建。否则,将返回对象实例。

还要注意的是,同步了ThreadSafeSynchronized类,因为使用的是静态方法。然后再次检查,以确保heroesDB实例仍然为空,因为可能有另一个线程已将其实例化。如果不进行双重检查,可能会得到多个实例。

另一个重要的问题是,变量heroesDB是不稳定的。这意味着不会缓存变量的值。当线程更改这一变量时,它将始终具有最新更新的值。

4.何时使用急切实例化

对于可能从未使用过的开销大的对象,最好使用懒惰实例化。然而,如果所处理的对象知道在应用程序每次启动时都将被使用,并且就使用的系统资源来说,如果创建对象开销很大,那么最好使用急切实例化。

假设必须创建一个开销非常大的对象,例如人们总是需要的数据库连接。等待该对象被使用可能会降低应用程序的运行速度。在这种情况下,急切实例化更有意义。

5.实现急切实例化的简单方法

实现急切实例化的简单方法如下:

public class HeroesDatabaseSimpleEager {

public static final HeroesDB heroesDB = new HeroesDB();

static HeroesDB getHeroesDB() {
return heroesDB;
}

static class HeroesDB {
private HeroesDB() {
System.out.println("Instantiating heroesDB eagerly...");
}

@Override
public String toString() {
return "HeroesDB instance";
}
}

public static void main(String[] args) {
System.out.println(HeroesDatabaseSimpleEager.getHeroesDB());
}
}
The output from this code would be:

Instantiating heroesDB eagerly...
HeroesDB instance

这段代码的输出是:

Instantiating heroesDB eagerly...
HeroesDB instance

注意,在本例中没有进行是为空的检查。当HeroesDB被声明为HeroesDatabaseSimpleEager中的实例变量时,它就被实例化了。因此,每次访问HeroesDatabaseSimpleEager类时,都会从HeroesDB获得一个实例。还重写了toString()方法,以简化HeroesDB实例的输出。

现在看看使用枚举实现急切实例化的更健壮的方法。

6.使用枚举创建急切实例化

使用枚举是创建急切实例化对象的一种更健壮的方法。尽管实例只会在枚举被访问时被创建,但要注意在下面的代码中,没有对对象创建进行是否为空的检查:

public enum HeroesDatabaseEnum {

INSTANCE;
int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public static void main(String[] args) {
System.out.println(HeroesDatabaseEnum.INSTANCE);

}

这段代码的输出将是:

Creating instance...
INSTANCE

这段代码是线程安全的。它保证只创建一个实例,并且序列化对象,这意味着可以更容易地传输它。另一个细节是,对于枚举有一个隐式的私有构造函数,这保证了不会不必要地创建多个实例。枚举被认为是使用急切实例化的最佳方法之一,因为它简单而有效。

7.懒惰实例化vs.急切实例化

当不总是需要实例化一个对象时,采用懒惰实例化更好。当知道总是需要实例化对象时,急切实例化更好。以下是每种方法的优缺点:

(1)懒惰实例化

优点:对象只会在需要的时候被实例化。

缺点:

  • 需要同步才能在多线程环境中工作。
  • 由于if检查和同步,性能会变慢。
  • 当需要该对象时,应用程序可能会有明显的懒惰。

(2)急切实例化

优点:

  • 在大多数情况下,对象将在应用程序启动时被实例化。
  • 使用对象时没有延迟,因为它已经被实例化了。
  • 它在多线程环境中工作良好。

缺点:使用这种方法可能会不必要地实例化对象。

8.Lazy Homer beer创作挑战

在下面的Java代码挑战中,将看到多线程环境中发生的懒惰实例化。

要注意的是,正在使用ThreadPool可以直接使用Thread类,但最好使用Java并发API。

根据在本文中学到的知识,人们会认为在运行以下代码时最可能发生什么情况?

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class LazyHomerBeerCreationChallenge {

public static int i = 0;
public static Beer beer;

static void createBeer() {
if (beer == null) {
try {
Thread.sleep(200);
beer = new Beer();
i++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(LazyHomerChallenge::createBeer);
executor.submit(LazyHomerChallenge::createBeer);

executor.awaitTermination(2, TimeUnit.SECONDS);
executor.shutdown();
System.out.println(i);
}

public static class Beer {}
}

以下是应对这一挑战的选项。仔细查看代码并选择其中一个:

1.A) 1

2.B) 0

3.C) 2

4.D)抛出InterruptedException

9.发生了什么?懒惰实例化解释

这个代码挑战的关键概念是,当两个线程访问同一个进程时,会出现并行性。因此,既然有一个线程。在实例化beer之前休眠,很可能会创建两个beer实例。

线程不并发运行的可能性非常小,这取决于JVM实现。但是由于线程的原因,很有可能最终得到两个Thread.sleep的方法。

现在再次查看代码,注意正在使用线程池创建两个线程,然后对这些线程运行createBear方法。

因此,这个代码挑战的正确答案是:C,或2的值。

10.结论

对于使用开销大的对象优化性能来说,懒惰实例化和急切实例化是很重要的概念。以下是关于这些设计策略需要记住的要点:

  • 懒惰实例化需要在实例化之前进行是否为空的检查。
  • 在多线程环境中同步对象以实现懒惰实例化。
  • 急切实例化不需要对对象进行是否为空的检查。
  • 使用枚举是一种有效且简单的紧急实例化方法。

原文链接:https://www.infoworld.com/article/3675954/lazy-vs-eager-instantiation-in-java-which-is-better.htm

责任编辑:武晓燕 来源: 51CTO技术栈
相关推荐

2012-05-23 12:55:39

Java实例化

2009-07-10 09:30:41

SwingWorker

2011-12-31 15:57:21

Java

2009-09-18 16:32:51

Linq委托实例化

2019-07-18 11:00:45

自动化运维测试

2013-03-04 11:10:03

JavaJVM

2020-10-22 08:52:52

Python数据集可视化

2024-01-02 10:54:07

Rust结构体元组

2009-09-25 11:06:38

Hibernate实例

2013-02-20 15:01:59

JSONAndroid开发

2018-01-24 09:00:00

2009-07-09 00:25:00

Scala参数化

2009-07-20 15:08:41

Spring实例化Be

2020-10-21 08:05:45

Scrapy

2010-04-20 16:10:09

虚拟化

2009-09-09 15:54:48

C# XML序列化

2024-04-11 13:18:26

C++泛型编程

2011-07-11 16:13:01

Java静态变量实例变量

2009-08-28 11:09:35

C#数组初始化

2009-09-09 15:47:27

XML序列化和反序列化
点赞
收藏

51CTO技术栈公众号