>  기사  >  Java  >  Java에서 싱글톤 구현의 어려움

Java에서 싱글톤 구현의 어려움

伊谢尔伦
伊谢尔伦원래의
2016-12-05 11:34:301031검색

싱글톤 패턴을 구현하는 간단하고 효율적인 방법이 있지만 모든 상황에서 싱글톤의 무결성을 보장할 수 있는 방법은 없습니다.

싱글턴 패턴은 클래스가 한 번만 인스턴스화되고 전역 또는 시스템 전체 구성 요소를 나타내는 데 사용된다는 의미입니다. 싱글톤 패턴은 일반적으로 로깅, 팩토리, 창 관리자, 플랫폼 구성 요소 관리 등에 사용됩니다. 싱글톤 패턴은 일단 구현하면 변경이나 과부하가 어렵고, 테스트 케이스 작성이 어렵고, 코드 구조가 좋지 않은 등의 문제를 일으키기 때문에 최대한 피해야 한다고 생각합니다. 또한 다음 문서의 싱글톤 패턴은 안전하지 않습니다.

사람들은 어떻게 하면 싱글톤 패턴을 더 잘 구현할 수 있을지 연구하는데 많은 에너지를 소비하는데, 간단하고 효율적인 구현 방법이 있습니다. 그러나 모든 상황에서 싱글톤의 무결성을 보장할 수 있는 유일한 방법은 없습니다. 아래를 읽고 동의하는지 확인하세요.

최종 필드

이 메서드는 생성자를 사유화하고 공개 정적 최종 개체를 제공합니다.

public class FooSingleton {    
    public final static FooSingleton INSTANCE = new FooSingleton();   
    private FooSingleton() { }    
    public void bar() { }}

클래스가 로드되면 정적 개체가 초기화됩니다. 개인 생성자가 처음이자 마지막으로 호출되는 지점입니다. 클래스가 초기화되기 전에 여러 스레드가 이 클래스를 호출하더라도 JVM은 스레드가 계속 실행될 때 클래스가 완전히 초기화되도록 보장할 수 있습니다. 그러나 리플렉션과 setAccessible(true) 메서드를 사용하면 다른 새 인스턴스를 생성할 수 있습니다.

Constructor[] constructors = FooSingleton.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton spuriousFoo = (FooSingleton) constructor.newInstance(new Object[0]);

생성자가 여러 번 호출되지 않도록 수정해야 합니다. 다시 비정상이라고 합니다. FooSingleton 생성자를 다음과 같이 수정하면 이러한 공격을 방지할 수 있습니다.

public class FooSingleton2 {    
private static boolean INSTANCE_CREATED;    
public final static FooSingleton2 INSTANCE = new FooSingleton2();    
private FooSingleton2() {        
    if (INSTANCE_CREATED) {            
        throw new IllegalStateException("You must only create one instance of this class");        
     } else {            
         INSTANCE_CREATED = true;        
     }    
  }    
      public void bar() { }
}

이것이 더 안전해 보일 수도 있지만 실제로는 새 인스턴스를 생성하는 것도 마찬가지로 쉽습니다. INSTANCE_CREATED 필드를 수정하고 동일한 트릭을 다시 수행하면 됩니다.

Field f = FooSingleton2.class.getDeclaredField("INSTANCE_CREATED");
f.setAccessible(true);
f.set(null, false);
Constructor[] constructors = FooSingleton2.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton2 spuriousFoo = (FooSingleton2) constructor.newInstance(new Object[0]);

우리가 취하는 모든 예방 조치가 우회될 수 있으므로 이 솔루션은 실현 가능하지 않습니다.

정적 팩토리

이 메서드를 사용하면 공용 멤버는 정적 팩토리와 유사합니다.

public class FooSingleton3 {    
  public final static FooSingleton3 INSTANCE = new FooSingleton3();    
  private FooSingleton3() { }    
  public static FooSingleton3 getInstance() { return INSTANCE; }    
  public void bar() { }
}

getInstance() 메서드는 항상 동일한 객체 참조를 반환합니다. 이 솔루션은 반사를 방지할 수는 없지만 여전히 몇 가지 장점이 있습니다. 예를 들어 API를 변경하지 않고도 싱글톤의 구현을 변경할 수 있습니다. getInstance()는 거의 모든 싱글톤 구현에 나타나며 이것이 실제로 싱글톤 패턴이라는 신호이기도 합니다.

지연 로딩 싱글턴 모드

(역자 주: 소프트웨어 엔지니어링에서 주문형 초기화 홀더라는 관용어는 지연 로딩 싱글턴 모드를 나타냅니다. Wikipedia 참조)

If 가능한 한 싱글톤 생성을 지연시키려는 경우(지연 로딩) getInstance() 메서드가 처음 호출될 때 지연 초기화 메서드를 사용하여 스레드로부터 안전하게 싱글톤을 생성할 수 있습니다. 클래스가 처음 참조될 때 싱글톤을 생성하는 이전 솔루션(Hungry 스타일 로딩)과 비교하면 이는 개선된 것입니다. 다음과 같습니다.

public class FooSingleton4 {    
private FooSingleton4() {    
}    
public static FooSingleton4 getInstance() {       
 return FooSingleton4Holder.INSTANCE;    
 }    
private static class FooSingleton4Holder {       
 private static final FooSingleton4 INSTANCE = new FooSingleton4();    
 }
}

직렬화에 주의하세요

싱글턴이 직렬화를 구현하면 또 다른 위협에 직면하게 됩니다. 따라서 모든 필드를 임시 필드로 선언하고(직렬화되지 않도록) 고유한 인스턴스 INSTANCE에 대한 참조를 반환하는 사용자 정의 readResolve() 메서드를 제공해야 합니다.

열거

여기서 열거는 싱글톤 인스턴스의 컨테이너로 사용됩니다.

public enum FooEnumSingleton {
    INSTANCE;    
    public static FooEnumSingleton getInstance() { 
    return INSTANCE; 
    }    
    public void bar() { }
}

Java 언어 사양 8.9에 따르면 "Enum의 최종 복제 방법은 열거형은 영원히 지속됩니다. 복제할 수 없으며 특수한 직렬화 메커니즘을 통해 복사된 개체를 역직렬화할 수 없습니다. 동시에 열거형을 인스턴스화하기 위해 리플렉션을 사용하는 것도 금지하여 다음과 같은 네 가지 측면이 발생하지 않도록 합니다. 같은 유형의 다른 열거형 인스턴스가 있습니다."

이런 방식으로 직렬화, 복제 및 반사 공격으로부터 쉽게 보호되는 것 같습니다. 이 구절을 처음 봤을 때 나는 즉시 그것이 틀렸다는 것을 증명하고 싶었습니다. 다음 코드에 표시된 것처럼 이러한 보호를 우회하는 것은 쉽습니다.

Constructor con = FooEnumSingleton.class.getDeclaredConstructors()[0];
 Method[] methods = con.getClass().getDeclaredMethods();
 for (Method method : methods) {
     if (method.getName().equals("acquireConstructorAccessor")) {
         method.setAccessible(true);
         method.invoke(con, new Object[0]);
     }
  }
  Field[] fields = con.getClass().getDeclaredFields();
  Object ca = null;  for (Field field : fields) {
      if (field.getName().equals("constructorAccessor")) {
          field.setAccessible(true);
          ca = field.get(con);
      }
  }  Method method = ca.getClass().getMethod("newInstance", new Class[]{Object[].class});
  method.setAccessible(true);
  FooEnumSingleton spuriousEnum = (FooEnumSingleton) method.invoke(ca, new Object[]{new Object[]{"SPURIOUS_INSTANCE", 1}});
  printInfo(FooEnumSingleton.INSTANCE);
  printInfo(spuriousEnum);
}private static void printInfo(FooEnumSingleton e) {
    System.out.println(e.getClass() + ":" + e.name() + ":" + e.ordinal());
}

이 코드를 실행하면 다음과 같은 결과를 얻습니다.

class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:INSTANCE:0
class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:SPURIOUS_INSTANCE:1

열거형의 단점은 다음과 같습니다. 이미 java.lang.Enum에서 상속되었으므로 다른 기본 클래스 상속에서 복사할 수 없습니다. 이런 종류의 상속을 시뮬레이션하려면 내 다른 기사에서 소개한 믹스인 패턴을 참조하면 됩니다.

열거형의 장점 중 하나는 나중에 "dualton" 또는 "tringleton"을 원할 경우 새 열거형 인스턴스만 추가하면 된다는 것입니다. 예를 들어, 싱글톤 캐시를 보유한 후 캐시에 여러 레이어를 도입할 수도 있습니다.

결론

이러한 싱글톤 보호를 우회하는 것은 쉽지 않지만 실제로 완벽한 솔루션은 없습니다. 더 나은 해결책이 있다면 언제든지 알려주시기 바랍니다!

열거는 싱글톤 패턴을 구현하는 간단하고 효율적인 방법입니다. 상속이나 지연 로딩을 원한다면 지연 초기화가 좋은 선택입니다.

싱글남들에게 행운을 빕니다!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.