자바 팁 67 : 지연 인스턴스화

얼마 전까지 만해도 8 비트 마이크로 컴퓨터의 온보드 메모리가 8KB에서 64KB로 증가 할 것이라는 전망에 감격했습니다. 지금 우리가 사용하는 리소스를 많이 사용하는 응용 프로그램이 계속 증가하고있는 것으로 판단 할 때, 그 작은 양의 메모리에 맞는 프로그램을 작성한 사람이 있다는 사실은 놀랍습니다. 요즘 우리는 더 많은 기억을 가지고 있지만, 이러한 엄격한 제약 내에서 작동하도록 확립 된 기술에서 몇 가지 귀중한 교훈을 배울 수 있습니다.

더욱이 Java 프로그래밍은 개인용 컴퓨터 및 워크 스테이션에 배포하기위한 애플릿 및 응용 프로그램을 작성하는 것만이 아닙니다. 자바는 임베디드 시스템 시장에도 강력한 진출을 이루었습니다. 현재의 임베디드 시스템은 메모리 리소스와 컴퓨팅 능력이 상대적으로 부족하기 때문에 프로그래머가 직면 한 많은 오래된 문제가 디바이스 영역에서 작업하는 Java 개발자에게 다시 나타납니다.

이러한 요소의 균형을 맞추는 것은 매력적인 디자인 문제입니다. 임베디드 디자인 영역에서 완벽한 솔루션은 없다는 사실을 받아들이는 것이 중요합니다. 따라서 배포 플랫폼의 제약 조건 내에서 작업하는 데 필요한 미세한 균형을 달성하는 데 유용한 기술 유형을 이해해야합니다.

Java 프로그래머가 유용하다고 생각하는 메모리 보존 기술 중 하나는 지연 인스턴스화입니다. 지연 인스턴스화를 사용하면 프로그램은 리소스가 처음 필요할 때까지 특정 리소스 생성을 자제하여 귀중한 메모리 공간을 확보합니다. 이 팁에서는 Java 클래스 로딩 및 객체 생성의 지연 인스턴스화 기술과 Singleton 패턴에 필요한 특수 고려 사항을 검토합니다. 이 팁의 자료는 우리 책, Java in Practice : Design Styles & Idioms for Effective Java (참고 자료 참조)의 9 장에있는 작업에서 파생되었습니다 .

Eager vs. lazy 인스턴스화 : 예제

Netscape의 웹 브라우저에 익숙하고 버전 3.x와 4.x를 모두 사용했다면 의심 할 여지없이 Java 런타임이로드되는 방식에 차이가 있음을 알 수 있습니다. Netscape 3가 시작될 때 스플래시 화면을 보면 Java를 포함한 다양한 리소스가로드된다는 것을 알 수 있습니다. 그러나 Netscape 4.x를 시작하면 Java 런타임이로드되지 않고 태그가 포함 된 웹 페이지를 방문 할 때까지 대기합니다. 이 두 가지 접근 방식은 즉시 인스턴스화 (필요한 경우로드) 및 지연 인스턴스화 (불필요 할 수도 있으므로로드하기 전에 요청 될 때까지 대기) 기술을 보여줍니다.

두 가지 접근 방식에는 단점이 있습니다. 한편으로 리소스를로드하는 것은 해당 세션 동안 리소스를 사용하지 않으면 잠재적으로 소중한 메모리를 낭비합니다. 반면에로드되지 않은 경우 리소스가 처음 필요할 때로드 시간 측면에서 가격을 지불합니다.

지연 인스턴스화를 리소스 보존 정책으로 고려

Java의 지연 인스턴스화는 두 가지 범주로 나뉩니다.

  • 지연 클래스 로딩
  • 게으른 개체 생성

지연 클래스 로딩

Java 런타임에는 클래스에 대한 기본 제공 지연 인스턴스화가 있습니다. 클래스는 처음 참조 될 때만 메모리에로드됩니다. (먼저 HTTP를 통해 웹 서버에서로드 할 수도 있습니다.)

MyUtils.classMethod (); // 정적 클래스 메서드에 대한 첫 번째 호출 Vector v = new Vector (); // 연산자 new에 대한 첫 번째 호출

지연 클래스 로딩은 특정 상황에서 메모리 사용량을 줄일 수 있으므로 Java 런타임 환경의 중요한 기능입니다. 예를 들어, 프로그램의 일부가 세션 중에 실행되지 않으면 프로그램의 해당 부분에서만 참조되는 클래스가로드되지 않습니다.

게으른 개체 생성

지연 객체 생성은 지연 클래스 로딩과 밀접하게 결합됩니다. 이전에로드되지 않은 클래스 유형에 대해 처음으로 new 키워드를 사용하면 Java 런타임이이를로드합니다. 지연 객체 생성은 지연 클래스 로딩보다 메모리 사용량을 훨씬 더 줄일 수 있습니다.

지연 객체 생성의 개념을 소개하기 Frame위해 a MessageBox가 오류 메시지를 표시 하는 데 사용하는 간단한 코드 예제를 살펴 보겠습니다 .

public class MyFrame extends Frame {private MessageBox mb_ = new MessageBox (); //이 클래스에서 사용하는 개인 도우미 private void showMessage (String message) {// 메시지 텍스트 설정 mb_.setMessage (message); mb_.pack (); mb_.show (); }}

위의 예에서의 인스턴스 MyFrame가 생성되면 MessageBoxmb_ 인스턴스도 생성됩니다. 동일한 규칙이 재귀 적으로 적용됩니다. 따라서 클래스 MessageBox생성자 에서 초기화되거나 할당 된 인스턴스 변수 도 힙에서 할당됩니다. 의 인스턴스가 MyFrame세션 내에서 오류 메시지를 표시하는 데 사용되지 않으면 불필요하게 메모리를 낭비하는 것입니다.

이 다소 간단한 예에서 우리는 실제로 너무 많은 것을 얻지 못할 것입니다. 그러나 더 많은 다른 클래스를 사용하는 더 복잡한 클래스를 고려하면 더 많은 객체를 재귀 적으로 사용하고 인스턴스화 할 수 있으므로 잠재적 인 메모리 사용량이 더 분명해집니다.

지연 인스턴스화를 정책으로 고려하여 리소스 요구 사항을 줄입니다.

위의 예에 대한 게으른 접근 방식은 아래에 나열되어 있습니다. 여기서는 object mb_에 대한 첫 번째 호출에서 인스턴스화됩니다 showMessage(). (즉, 프로그램에서 실제로 필요할 때까지는 아닙니다.)

public final class MyFrame extends Frame {private MessageBox mb_; // null, 암시 적 //이 클래스에서 사용하는 개인 도우미 private void showMessage (String message) {if (mb _ == null) //이 메서드에 대한 첫 번째 호출 mb_ = new MessageBox (); // 메시지 텍스트 설정 mb_.setMessage (message); mb_.pack (); mb_.show (); }}

자세히 살펴보면 showMessage()먼저 인스턴스 변수 mb_가 null과 같은지 확인하는 것을 볼 수 있습니다. 선언 시점에서 mb_를 초기화하지 않았기 때문에 Java 런타임이이를 처리했습니다. 따라서 MessageBox인스턴스 를 생성하여 안전하게 진행할 수 있습니다 . 이후의 모든 호출 showMessage()은 mb_가 null과 같지 않다는 것을 알게되므로 객체 생성을 건너 뛰고 기존 인스턴스를 사용합니다.

실제 사례

이제 지연 인스턴스화가 프로그램에서 사용하는 리소스 양을 줄이는 데 중요한 역할을 할 수있는보다 현실적인 예를 살펴 보겠습니다.

사용자가 파일 시스템에서 이미지를 카탈로그 화하고 썸네일 또는 전체 이미지를 볼 수있는 기능을 제공 할 시스템을 작성하도록 클라이언트로부터 요청을 받았다고 가정합니다. 첫 번째 시도는 생성자에서 이미지를로드하는 클래스를 작성하는 것입니다.

공용 클래스 ImageFile {개인 문자열 파일 이름 _; 개인 이미지 image_; 공개 ImageFile (문자열 파일 이름) {파일 이름 _ = 파일 이름; // 이미지로드} public String getName () {return filename_;} public Image getImage () {return image_; }}

위의 예에서는 개체 ImageFile를 인스턴스화하는 데 지나치게 접근하는 방식을 구현 Image합니다. 유리하게이 디자인은를 호출 할 때 이미지를 즉시 사용할 수 있도록 보장합니다 getImage(). 그러나 이것은 고통스럽게 느릴 수있을뿐만 아니라 (많은 이미지를 포함하는 디렉토리의 경우),이 디자인은 사용 가능한 메모리를 고갈시킬 수 있습니다. 이러한 잠재적 인 문제를 방지하기 위해 즉각적인 액세스의 성능 이점을 메모리 사용량 감소와 교환 할 수 있습니다. 짐작 하셨겠지만 lazy 인스턴스화를 사용하여이를 달성 할 수 있습니다.

다음 ImageFile은 클래스 MyFrameMessageBox인스턴스 변수를 사용한 것과 동일한 접근 방식을 사용 하는 업데이트 된 클래스입니다 .

공용 클래스 ImageFile {개인 문자열 파일 이름 _; 개인 이미지 image_; // = null, 암시 적 public ImageFile (String filename) {// 파일 이름 만 저장합니다. filename_ = filename; } public String getName () {return filename_;} public Image getImage () {if (image _ == null) {// getImage ()에 대한 첫 번째 호출 // 이미지로드 ...} return image_; }}

이 버전에서 실제 이미지는에 대한 첫 번째 호출에서만로드됩니다 getImage(). 요약하자면, 여기서 절충점은 전체 메모리 사용량과 시작 시간을 줄이기 위해 이미지가 처음 요청 될 때 이미지를로드하는 대가를 지불하는 것입니다. 프로그램 실행의 해당 시점에 성능 저하가 발생합니다. 이것은 Proxy제한된 메모리 사용이 필요한 상황 에서 패턴 을 반영하는 또 다른 관용구입니다 .

The policy of lazy instantiation illustrated above is fine for our examples, but later on you'll see how the design has to alter in the context of multiple threads.

Lazy instantiation for Singleton patterns in Java

Let's now take a look at the Singleton pattern. Here's the generic form in Java:

public class Singleton { private Singleton() {} static private Singleton instance_ = new Singleton(); static public Singleton instance() { return instance_; } //public methods } 

In the generic version, we declared and initialized the instance_ field as follows:

static final Singleton instance_ = new Singleton(); 

Readers familiar with the C++ implementation of Singleton written by the GoF (the Gang of Four who wrote the book Design Patterns: Elements of Reusable Object-Oriented Software -- Gamma, Helm, Johnson, and Vlissides) may be surprised that we didn't defer the initialization of the instance_ field until the call to the instance() method. Thus, using lazy instantiation:

public static Singleton instance() { if(instance_==null) //Lazy instantiation instance_= new Singleton(); return instance_; } 

The listing above is a direct port of the C++ Singleton example given by the GoF, and frequently is touted as the generic Java version too. If you already are familiar with this form and were surprised that we didn't list our generic Singleton like this, you'll be even more surprised to learn that it is totally unnecessary in Java! This is a common example of what can occur if you port code from one language to another without considering the respective runtime environments.

For the record, the GoF's C++ version of Singleton uses lazy instantiation because there is no guarantee of the order of static initialization of objects at runtime. (See Scott Meyer's Singleton for an alternative approach in C++ .) In Java, we don't have to worry about these issues.

The lazy approach to instantiating a Singleton is unnecessary in Java because of the way in which the Java runtime handles class loading and static instance variable initialization. Previously, we have described how and when classes get loaded. A class with only public static methods gets loaded by the Java runtime on the first call to one of these methods; which in the case of our Singleton is

Singleton s=Singleton.instance(); 

The first call to Singleton.instance() in a program forces the Java runtime to load the class Singleton. As the field instance_ is declared as static, the Java runtime will initialize it after successfully loading the class. Thus guarantees that the call to Singleton.instance() will return a fully initialized Singleton -- get the picture?

Lazy instantiation: dangerous in multithreaded applications

Using lazy instantiation for a concrete Singleton is not only unnecessary in Java, it's downright dangerous in the context of multithreaded applications. Consider the lazy version of the Singleton.instance() method, where two or more separate threads are attempting to obtain a reference to the object via instance(). If one thread is preempted after successfully executing the line if(instance_==null), but before it has completed the line instance_=new Singleton(), another thread can also enter this method with instance_ still ==null -- nasty!

The outcome of this scenario is the likelihood that one or more Singleton objects will be created. This is a major headache when your Singleton class is, say, connecting to a database or remote server. The simple solution to this problem would be to use the synchronized key word to protect the method from multiple threads entering it at the same time:

synchronized static public instance() {...} 

However, this approach is a bit heavy-handed for most multithreaded applications using a Singleton class extensively, thereby causing blocking on concurrent calls to instance(). By the way, invoking a synchronized method is always much slower than invoking a nonsynchronized one. So what we need is a strategy for synchronization that doesn't cause unnecessary blocking. Fortunately, such a strategy exists. It is known as the double-check idiom.

The double-check idiom

Use the double-check idiom to protect methods using lazy instantiation. Here's how to implement it in Java:

public static Singleton instance() { if(instance_==null) //don't want to block here { //two or more threads might be here!!! synchronized(Singleton.class) { //must check again as one of the //blocked threads can still enter if(instance_==null) instance_= new Singleton();//safe } } return instance_; } 

The double-check idiom improves performance by using synchronization only if multiple threads call instance() before the Singleton is constructed. Once the object has been instantiated, instance_ is no longer ==null, allowing the method to avoid blocking concurrent callers.

Java에서 다중 스레드를 사용하는 것은 매우 복잡 할 수 있습니다. 사실 동시성에 대한 주제가 너무 방대해서 Doug Lea가 이에 대한 책인 Concurrent Programming in Java를 썼습니다 . 동시 프로그래밍을 처음 접하는 경우 여러 스레드에 의존하는 복잡한 Java 시스템을 작성하기 전에이 책의 사본을 구하는 것이 좋습니다.