HƯỚNG DẪN Singleton

Joe

Thành viên VIP
21/1/13
2,927
1,314
113
Hi
Time to talk about the most basic thing: SINGLETON object (or class).

First of all: What is it, the "singleton"? In plain English it is the so-called SINGLE ONE or SINGLETON or in other word: the UNIQUE ONE. In IT-Language Singleton is one of the 23 Pattern Designs defined by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides). More: click HERE.

Then: For what Singleton is good? Well, as its name says singleton is an unique object (or class) and stays unchanged within the entire application life. For example, Application properties. Java's Runtime object is a Singleton object (see codes). It must be the same within an app whenever and wherever the object Runtime is needed for the entire life circle of the app. Further, Singleton must be Thread-safe.
Java:
Runtime rt = Runtime.getRuntime();
The single line of codes tells you that Singleton CANNOT be instantiated directly by a conventional Constructor, but only by an available static method that delivers the instantiated Singleton object.

I remembered about someone who posted long ago a blog telling his awful undergoing during a job interview. His interviewer asked him to write down a simple Singleton API. Easy he thought and conjured out of his cuff the following piece of codes:
Java:
public class Singleton implements Serializable {
  private static Singleton instance = new Singleton();
  public static Singleton getInstance() {
    return instance;
  }
  private Singleton( ) { };
}
Look very good, isn't it? At the first glance it is. But for an experienced hacker the codes contain a "security hole" that could turn this Singleton to a Multipleton. How? With Reflection it is easily to invoke the private Constructor and to create another "Singleton".
Java:
public class TestSingleton {
  public static void main(String... a) throws Exception {
    TestSingleton ts = new TestSingleton();
    ts.test();
    System.exit(0);
  }
  private void test() throws Exception {
    try {
      Singleton instance1 = null;
      Singleton instance2 = Singleton.getInstance();
      Constructor[] constructors =  Singleton.class.getDeclaredConstructors();
      if (constructors == null || constructors.length == 0) return;
      // Below code will destroy the singleton pattern
      constructors[0].setAccessible(true);
      instance1 = (Singleton) constructors[0].newInstance();
      System.out.println("Singleton of Reflection "+instance1+"<>"+instance2);
    } catch (Exception exx) {
      exx.printStackTrace();
    }
  }
}
And the output proves:
Code:
C:\JFX\TestII>java TestSingleton
Singleton of Reflection Singleton@3f0ee7cb<>Singleton@725bef66

C:\JFX\TestII>
The hashcode of "reflected" instance1 is 3f0ee7cb while 725bef66 from getInstance().

There is a lot of ways to design a Singleton. But in the haste of an interview you cannot ponder and design a page-long codes. It must be simple, thread-safe and truly a simpleton. To do that you have to implicitly bar all kinds of Refection Modification. Example: We check the instance to make sure that the instance must be gotten by the specified method getInstance(). No reflection.

Java:
public class Singleton {
  private Singleton( ) throws Exception {
    if (instance != null) throw new Exception("Illegal Instantiation");  // <-- check the instance
  }
  private static Singleton instance = null;
  public static Singleton getInstance() {
    return instance;
  }
  static {
    try {
      instance = new Singleton();
    } catch (Exception ex) { }
  }
  public String toString() {
    return instance != null? "Singleton ID:"+hashCode():null;
  }
}
and the test confirms our approach:
Java:
public class TestSingleton {
  public static void main(String... a) throws Exception {
    TestSingleton ts = new TestSingleton();
    ts.test();
    System.exit(0);
  }
  private void test() throws Exception {
    try {
      Singleton s1 = Singleton.getInstance();
      Singleton s2 = Singleton.getInstance();
      System.out.println("Direct getInstance():\n"+s1.toString()+"\n"+s2.toString());
    } catch (Exception ex) {
      ex.printStackTrace();
    }
    // with diff. classloader
    URL[] clUrls = new URL[]{new URL("file:///C:\\JFX\\Browser\\testII\\simpleton.jar")};
    URLClassLoader urlCl1 = new URLClassLoader(clUrls);
    URLClassLoader urlCl2 = new URLClassLoader(clUrls);
    try {
      Method method = urlCl1.loadClass("Singleton").getMethod("getInstance", new Class[0]);
      Singleton s1 = (Singleton) method.invoke(null, new Object[0]);
      method = urlCl2.loadClass("Singleton").getMethod("getInstance", new Class[0]);
      Singleton s2 = (Singleton) method.invoke(null, new Object[0]);
      System.out.println("Indirect with getInstance() via ClassLoader:\n"+s1.toString()+"\n"+s2.toString());
    } catch (Exception ex) {
      ex.printStackTrace();
    }
    try {
      Constructor[] constructors =  Singleton.class.getDeclaredConstructors();
      if (constructors == null || constructors.length == 0) return;
      // Below code will destroy the singleton pattern
      constructors[0].setAccessible(true);
      Singleton s2 = (Singleton) constructors[0].newInstance();

      Singleton s1 = Singleton.getInstance();
      System.out.println("Refection with Constructor: "+s2.toString()+
                         "\nDirect with getInstance: "+s1.toString());
    } catch (Exception exx) {
      exx.printStackTrace();
    }
  }
}
The output:
Code:
C:\JFX\TestII>java TestSingleton
Direct getInstance():
Singleton ID:792791759
Singleton ID:792791759
Indirect with getInstance() via ClassLoader:
Singleton ID:792791759
Singleton ID:792791759
java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
        at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source)
        at TestSingleton.test(Unknown Source)
        at TestSingleton.main(Unknown Source)
Caused by: java.lang.Exception: Illegal Instantiation
        at Singleton.<init>(Singleton.java:5)
        ... 6 more

C:\JFX\TestII>
As you see, the hashcodes of Direct getInstance() and indirect via Classloader stay the same, but the exception "Illegal Instantiation" is caused by the line 5 of Singleton.java. And that confirms the uniqueness or the "single one".

Security hole is then closed? NO. As long as Oracle (or SUN) still allows us to run Reflection to modify the access to private objects or to change the final state of private objects the security hole still exists in other forms. Example: We modify the Object instance by setting it to null using Reflection, then invoke the private constructor via Reflection.
Java:
public class TestSingleton {
  public static void main(String... a) throws Exception {
    TestSingleton ts = new TestSingletonY();
    ts.test();
    System.exit(0);
  }
  private void test() throws Exception {
      String s1 = Singleton.getInstance().toString();
      // access the private object instance
      Field instance = Singleton.class.getDeclaredField("instance");
      instance.setAccessible(true);
      // get Field Modifiers
      Field mField = Field.class.getDeclaredField("modifiers");
      mField.setAccessible(true);
      mField.setInt(instance, instance.getModifiers() & ~Modifier.FINAL);
      // set instance to null
      instance.set(null, null);
      Constructor[] constructors =  Singleton.class.getDeclaredConstructors();
      if (constructors == null || constructors.length == 0) return;
      // Below code will destroy the singleton pattern
      constructors[0].setAccessible(true);
      String s2 = ((Singleton) constructors[0].newInstance()).toString();
      System.out.println("Refection with Constructor: "+s2+"\nDirect with getInstance: "+s1);
  }
}
and the output:
Code:
C:\JFX\TestII>java TestSingleton
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by TestSingleton (file:/C:/JFX/TestII/classes/) to field java.lang.reflect.Field.modifiers
WARNING: Please consider reporting this to the maintainers of TestSingleton
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Refection with Constructor: Singleton@17ed40e0
Direct with getInstance: Singleton@2f410acf

C:\JFX\TestII>
The hashcode with Constructor Refection is 17ed40e0 and with getInstance: 2f410acf. The security hole exists. But the warning
WARNING: All illegal access operations will be denied in a future release
makes it clear that hacking with Reflection could be dangerous in the future with new JAVA Releases.
 
Sửa lần cuối:

ngtheanh.dev

New Member
25/12/20
14
5
3
Hà Nội
Xin chào bác Joe :-h Chúc bác và mọi người có một ngày tốt lành
>:D<


Hồi mới biết tới bác Joe thì mình có tìm rất nhiều bài viết của bác trên google L-)
I remembered about someone who posted long ago a blog telling his awful undergoing during a job interview. His interviewer asked him to write down a simple Singleton API
Mình có đọc bài blog này của giaosucan phỏng vấn ở Sillicon valley
=D>
. Nếu như không xem comment của bác Joe thì mình cũng không thể biết là trước giờ mình hiểu sai về SINGLETON như nào X_X. Xin chia sẻ lại về đoạn code được bác Joe viết hồi đó:
Java:
import java.io.*;

// Joe Nartca (C)
public class Singleton implements Serializable {

    private static final String PATH = System.getProperty("user.dir") + "Singleton_1234567890L";
    private static Singleton instance;

    static {
        try (ObjectInputStream obj = new ObjectInputStream(new FileInputStream(PATH))) {
            instance = (Singleton) obj.readObject();
        } catch (Exception ex) {
            try (ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream(PATH, false))) {
                instance = new Singleton();
                obj.writeObject(instance);
                cleanup(); // clean up this file
            } catch (Exception ignore) {
            }
        }
    }

    private Singleton() throws Exception {
        if (instance != null) {
            throw new Exception("Illegal Instantiation.");
        }
    }

    // remove the file when application terminates
    private static void cleanup() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                (new File(PATH)).delete();
            } catch (Exception ignore) {
            }
        }));
    }

    public static Singleton getInstance() {
        return instance;
    }
}
Xin lỗi bác Joe vì đã chỉnh sửa lại code của bác một chút nhé ạ. Vì mình thấy đoạn code trên có vẻ tốt hơn nên muốn post lên cho mọi người cùng tham khảo. Bác Joe có thể so sánh kĩ hơn 2 cách impliment này của SINGLETON có ưu và nhược điểm gì trong thực tế được không ạ :bz
 

ngtheanh.dev

New Member
25/12/20
14
5
3
Hà Nội
À mình cũng bổ sung thêm cả phần TestSingleton để không bị exception cho mọi người tham khảo nữa nhé L-) Vẫn là của bác Joe thôi nhưng có edit lại chút xíu :-bd
Java:
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// Joe Nartca (C)
public class TestSingleton {

    private static final int MAX = 10;

    public static void main(String... a) throws Exception {
        TestSingleton t5 = new TestSingleton();
        t5.test();
        System.exit(0);
    }

    private void test() throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(MAX);
        for (int j = 0; j < MAX; j++) {
            pool.execute(() -> {
                try {
                    Singleton s1 = Singleton.getInstance();
                    Singleton s2 = Singleton.getInstance();
                    System.out.println("Singleton of Thread " + Thread.currentThread().getId() + ":" + s1 + "<>" + s2);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            });
        }
        // with diff. classloader  
        URL[] clUrls = new URL[]{new URL("file:///C:\\Browser\\test\\singleton.jar")};
        try (URLClassLoader urlCl1 = new URLClassLoader(clUrls);
             URLClassLoader urlCl2 = new URLClassLoader(clUrls)) {
            for (int j = 0; j < MAX; j++) {
                pool.execute(() -> {
                    try {
                        Method method = urlCl1.loadClass("Singleton").getMethod("getInstance");
                        Singleton s1 = (Singleton) method.invoke(null, new Object[0]);
                        method = urlCl2.loadClass("Singleton").getMethod("getInstance");
                        Singleton s2 = (Singleton) method.invoke(null, new Object[0]);
                        System.out.println("Singleton of Classloader " + Thread.currentThread().getId() + ":" + s1 + "<>" + s2);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                });
            }
        }
        pool.shutdown();
        try {
            if (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
                pool.shutdownNow(); // Cancel currently executing tasks
                if (!pool.awaitTermination(2, TimeUnit.SECONDS))
                    System.err.println("Pool did not terminate");
            }
        } catch (InterruptedException ie) {
            pool.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}
 

Joe

Thành viên VIP
21/1/13
2,927
1,314
113
Xin chào bác Joe :-h Chúc bác và mọi người có một ngày tốt lành
:D
Chào câu. Cám ơn, câu cũng vậy!
Yes I remembered this blog and I have deleted it because I was slandered as a piggyback advertiser. To your Question about Singleton:
There is a lot of ways to design a Singleton.
.....
Security hole is then closed? NO. As long as Oracle (or SUN) still allows us to run Reflection to modify the access to private objects or to change the final state of private objects the security hole still exists in other forms.
Xin lỗi bác Joe vì đã chỉnh sửa lại code của bác một chút nhé ạ
NO problem. The following anonymous method was
  1. too complicated for the newbies
  2. intended for a multiple-parallel application environment
Java:
static {
  try (ObjectInputStream obj = new ObjectInputStream(new FileInputStream(PATH))) {
    instance = (Singleton) obj.readObject();
   } catch (Exception ex) {
      try (ObjectOutputStream obj = new ObjectOutputStream(new FileOutputStream(PATH, false))) {
         instance = new Singleton();
         obj.writeObject(instance);
         cleanup(); // clean up this file
       } catch (Exception ignore) { }
   }
}
The test with ExecutorPool made sure that the threads were run independently (and parallelly in case of multiple cores: via newWorkStealingPool or ForkJoinPool).
 
Sửa lần cuối:
  • Like
Reactions: ngtheanh.dev