Explore the pros and cons of various ways to implement the Singleton pattern, and learn how to use the pattern to create a database connection whose parameters can be updated after a Java application has been compiled.

 

This article discusses the following three topics about the Singleton design pattern:

 

  • The different ways to implement the Singleton pattern

  • How to create a class for a database connection using the Singleton pattern

  • How to update the class parameters for a database connection after the application has been compiled

 

Before delving into the subject, let's define the Singleton pattern.

 

The Singleton pattern is one of many design patterns. Its unique characteristic is that it allows the existence of only and only one instance of a class. To ensure the uniqueness of a singleton, it is very important to control the process of its instantiation. Declaring the constructor asprivate prevents any external script from using the keyword new to directly create an instance of the singleton class.

 

As a result, any code wanting to get an instance of the singleton should pass a specific method named getInstance instead of the usual constructor. This method should be static, and it is the method that can directly execute the instantiation of the singleton using the keyword new.

 

Different Ways to Implement a Singleton Class

 

Now, let's explore the various ways to implement a singleton.

 

Minimal Singleton

 

Listing 1 shows a minimal version of a singleton:

 

public class JavaSingleton {

     private JavaSingleton() {
        /* the body of the constructor here */
     }

     /* pre-initialized instance of the singleton */
     private static final  JavaSingleton INSTANCE = new JavaSingleton();

     /* Access point to the unique instance of the singleton */
     public static JavaSingleton getInstance() {
        return INSTANCE;
     }
}

 

Listing 1. Minimal singleton

 

The class in Listing 1 seems correct, but it has some imperfections because it immediately loads the instance of the class when the application starts even if the instance is not needed. In some cases, the best approach is to load the instance of the class in memory only when the method getInstance is called. There is an approach called lazy loading that allows a late loading of the instance of a singleton in memory.

 

Lazy-Loading Singleton

 

Listing 2 shows the singleton class declaration with lazy loading.

 

public class JavaSingleton {
  private JavaSingleton() {
        /* the body of the constructor here */
    }
    /* declaration of instance of the singleton */
    private static JavaSingleton INSTANCE;

    /* Access point to the unique instance of the singleton */
    public static JavaSingleton getInstance() throws Throwable {
        if (INSTANCE == null) {
            INSTANCE = new JavaSingleton();
        }
        return INSTANCE;
    }
}

 

Listing 2. Lazy-loading singleton

 

The code in Listing 2 seems correct, but it's inappropriate in a multithreaded environment and is susceptible to returning more than one instance of the singleton if the method getInstance() is not protected by a synchronization.

 

Synchronized Singleton

 

Listing 3 shows a synchronized singleton class:

 

public class JavaSingleton {
     /* Private constructor     */
     private JavaSingleton() {
           /* the body of the constructor here */
     }

     /* pre-initialized instance of the singleton */
     private static JavaSingleton INSTANCE;

     /* Access point to the unique instance of the singleton */
     public static JavaSingleton getInstance() {
       if (INSTANCE == null){
          synchronized(JavaSingleton.class){
               INSTANCE = new JavaSingleton();
          }
       return INSTANCE ;
       }
     }
}

 

Listing 3. Synchronized singleton

 

Two threads can enter the if block at the same time when INSTANCE is null. The first thread enters the synchronized block to initialize INSTANCE while the second thread is blocked. When the first thread gets out of the synchronized block, the second one, which was blocked, can then enter the synchronized block. But when the second thread enters the synchronized block, it does not verify whether INSTANCE is null before initializing it.

 

In this case, the simplest solution would be to synchronize the get method instance, but this would affect the performance of the program. If the singleton is called frequently in the program, this will make the program work very slowly.

 

So the way to resolve the problem is to have a second verification in the synchronized block. There is an idiom that does this verification: double-checked locking.

 

Double-Checked Locking Singleton

 

Listing 4 shows a singleton class declaration with double-checked locking.

 

public class JavaSingleton {
     /* Private constructor     */
     private JavaSingleton() {
        /* the body of the constructor here */
     }

     /* pre-initialized instance of the singleton */
     private static JavaSingleton INSTANCE ;

     /* Access point to the unique instance of the singleton */
     public static JavaSingleton getInstance() {
        if (INSTANCE == null){
            synchronized(JavaSingleton.class){
               if(INSTANCE == null){
                    INSTANCE = new JavaSingleton;
               }
            }
            return INSTANCE ;
        }
     }
}

 

Listing 4. Singleton class declaration with double-checked locking

 

Intuitively, this algorithm seems like an effective solution to the problem. However, it raises subtle and difficult-to-trace problems. Consider the following sequence of events.

 

Thread A notices that the shared variable is not initialized. So it acquires the lock and begins to initialize the variable.

 

Because of the semantics of the programming language, the code generated by the compiler has the right to modify the shared variable before thread A's variable initialization has completed, so the reference variable is a partially initialized object (for example, because the thread begins to allocate memory and places the reference to the allocated memory block in the variable before finally calling the constructor, that will initialize the memory block).

 

Thread B notices that the shared variable has been initialized (or at least seems to be initialized), and it returns its value immediately without attempting to acquire a lock. If, then, the shared variable is used before thread A has had time to complete its initialization, the program is likely to crash.

 

One of the dangers of double-checked locking is that often it will appear to work. This depends on the compiler and how threads are scheduled by the operating system, along with other data-access competition management mechanisms. Reproducing the cause of a crash can be difficult, because crashes are highly unlikely when the code is running in a debugger. The use of double-checked locking must, therefore, be limited as much as possible.

 

Volatile Singleton

 

The volatile reserved word (see Listing 5) provides a solution to this problem by ensuring that different threads correctly handle concurrent access to the single instance of a singleton. However, this access security has a price: accessing a volatile variable is less efficient than accessing a normal variable.

 

Note: This functionality has been available since JDK 1.0, but it is deprecated now.

 

/* This doesn't work with Java 1.4 or earlier*/

class JavaSingleton {
     /* Private constructor     */
     private JavaSingleton() {
        /* the body of the constructor here */
     }

     /* instance of the singleton declaration */
     private volatile  JavaSingleton INSTANCE ;

     /* Access point to the unique instance of the singleton */
     public static JavaSingleton getInstance() {
        if (INSTANCE == null){
           synchronized(JavaSingleton.class){
              if(INSTANCE == null){
                 INSTANCE = new JavaSingleton;
              }
           }
        return INSTANCE ;
        }
     }
}

 

Listing 5. Example of code that uses volatile

 

Many versions of double-checked locking that do not use explicit synchronization or the volatile variable have been proposed. Apart from those based on the keyword enum pattern, all have proved to be incorrect.

 

Class Holder Singleton

 

The last technique of creating a singleton that we'll talk about is the class holder. It relies on the use of a private inner class, which is responsible for instantiating the single instance of the singleton, as shown in Listing 6.

 

public class JavaSingleton {
     /* Private constructor     */
     private JavaSingleton() {
        /* the body of the constructor here */
     }

     /* Access point to the unique instance of the singleton */
     public static JavaSingleton getInstance() {
         return INSTANCE;
     }

     private static class JavaSingletonHolder{
     private static final Singleton INSTANCE = new JavaSingleton()
     }
}

 

Listing 6. Example of a private inner class that instantiates a singleton

 

This type of singleton is recommended in any multithreaded environment.

 

Now let's explore the second point of this article.

 

How to Create a Database Connection Class Using a Singleton

 

A database connection class can be created by extending the Java Persistence API (JPA) or by creating a simple class with a method that returns the connection statement. Those who use JPA need not be concerned with this part of the article, because with JPA the persistence is based on the Singleton pattern.

 

From here to the end of the article the singleton class JavaSingleton will be called DbSingleton (this name is more appropriate for a database connection singleton).

 

Listing 7 shows a class that ensures that there will never be two instances of the database connection.

 

public class DbSingleton {
    private static String db_url;
    private static String db_port;
    private static String db_name;
    private static String db_user;
    private static String db_password;
    private static Statement connection;

    private DbSingleton() {
/* 
       *Default database parameters 
*/
        db_url = "jdbc:mysql://localhost";
        db_port = "3306";
        db_name = "mysql";
        db_user = "root";
        db_password = "admin";
/* Creation of an instance of the connection statement*/
        connection = setConnection();
    }
/* Private method charge to set the connection statement*/
    private static Statement setConnection() {
        try {
            String url = "" + db_url + ":" + db_port + "/" + db_name + "";
            java.sql.Connection conn = DriverManager.getConnection(url, db_user, db_password);

            //Creation of the Statement object
            java.sql.Statement state = conn.createStatement();
            return (Statement) state;
        } catch (SQLException ex) {
            Logger.getLogger(DbSingleton.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

/* Private inner class responsible for instantiating the single instance of the singleton */
    private static class DbSingletonManagerHolder {
        private final static DbSingleton instance = new DbSingleton();
    }

    /**
     * @return
Public method, which is the only method allowed to return an instance of 
the singleton (the instance here is the database connection statement)
     */
    public static DbSingleton getInstance() {
        try {
            return DbSingletonManagerHolder.instance;
        } catch (ExceptionInInitializerError ex) {

        }
        return null;
    }
    public static Statement getStatement() {
        return connection;
    }
}

 

Listing 7. Class that prevents two instances of the database connection

 

With this class, you can be sure there will never be two instances of the database connection. The way to use the database statement is shown in Listing 8:

 

/* Creation of the statement instance */
DbSingleton single = DbSingleton.getInstance();
Statement state = DbSingleton.getStatement();
/* put your SQL code in the variable sqlString */
String sqlString = "" ;
ResultSet result = state.executeQuery(sqlString);

 

Listing 8. Example of how to use the database connection

 

Now let's examine the last topic of this article.

 

How to Update the Parameters for a Database Connection After the Application Has Been Compiled

 

When the application will be compiled it will be impossible to change the database parameters manually directly in the source code, but we'll see a trick now for how to do this programmatically.

 

First, in the application, we create a form into which the new database parameters will be typed. The form should have the following fields:

 

  • Server address

  • Server port

  • Database username

  • Database password

  • Database name

 

When a user submits this form, the parameters that were typed in should be stored in an XML or plain text file of your choice.

 

Add the Parameters class shown in Listing 9 to manage the updating of the database parameters.

 

public class Parameters {
    private static String url;
    private static String port;
    private static String dbName;
    private static String user;
    private static String pass;
    private static final String storeFilePath = "/home/path/to/txt/file";
    public Parameters() {
        String parameters[];
        try {
            parameters = txtReader();
            url = parameters[0]; port = parameters[1]; dbName = parameters[2];
                  user = parameters[3]; pass = parameters[4];
        } catch (IOException ex) {
            Logger.getLogger(Parameters.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    /* This method returns an array containing the database parameters in the following order: url,port,dbname,user,pass */
    public static String[] txtReader() throws IOException {
        BufferedReader read;
        String line = "";    String[] params = null;
        try {
            read = new BufferedReader(new FileReader(storeFilePath));
            line = line + read.readLine();
            /* Split the content of the file*/
            params = line.split(";");
        } catch (NullPointerException a) {
            System.out.println("Error : pointer null");
        } catch (IOException a) {
            System.out.println("I/O Problem");
        } finally {
            System.out.println("I/O Problem");
        }
        return params;
    }
    public void txtWriter(String user, String pass, String address, String port, String dbname) {
        /* Specify the path to the file where the database parameters will be stored */
        String path = storeFilePath;
        final File file = new File(path);
        try {
            // Creation of the file
            file.createNewFile();
            // creation of the writer
            final FileWriter writer = new FileWriter(file);
            try {
                /*Save database parameters, separated by a semi-colon (;), in a text file*/
                writer.write(address + ";" + port + ";" + dbname + ";" + user + ";" + pass);
            } finally {
                // Whatever happens, the file should be closed
                writer.close();
            }
        } catch (Exception e) {
            System.out.println("Impossible to create the file");
        }
    }

    public static String getUrl() {
        return url;
    }
    public static String getPort() {
        return port;
    }
    public static String getDbName() {
        return dbName;
    }
    public static String getUser() {
        return user;
    }
    public static String getPass() {
        return pass;
    }
    public void setUrl(String dburl) {
        url = dburl;
    }
    public void setPort(String dbport) {
        port = dbport;
    }
    public void setDbName(String db_name) {
        dbName = db_name;
    }
    public void setUser(String dbuser) {
        user = dbuser;
    }
    public void setPass(String dbpass) {
        pass = dbpass;
    }
}

 

Listing 9. Code for managing the updating of the database parameters

 

Now we will update the DbSingleton by calling the method getParameters() and overriding the values of the default parameters with values that we get from the external file.

 

First, update DbSingleton by overriding the private constructor using the script shown in Listing 10:

 

private DbSingleton() {
/* 
   * Overriding database parameters 
*/
        Parameters params = new Parameters() ;
        db_url = params.getUrl();
        db_port = params.getPort() ;
        db_name = params.getName() ;
        db_user = params.getUser() ;
        db_password = params.getPass() ;
/* Creation of an instance of the connection statement*/
        connection = setConnection();
}

 

Listing 10. Script for overriding the private constructor

 

To override the current database parameters, the current instance of DbSingleton should be cleared. To do this you might think of setting INSTANCE to null, but this will never work because INSTANCE is a final variable so no assignment could ever be done to it.

 

The way to override the singleton very simply is to programmatically restart the application and create a new instance of DbSingleton with the new database parameters.

 

First, update DbSingleton to add the class shown in Listing 11 to make your application programmatically restartable.

 

public class Restarter {
    /**
     * Sun property pointing the main class and its arguments. Might not be
     * defined on non-Java HotSpot VM implementations.
     */
    public static final String SUN_JAVA_COMMAND = "sun.java.command";
    /**
     * Restart the current Java application
     *
     * @param runBeforeRestart some custom code to be run before restarting
     * @throws IOException
     */
    public static void restartApplication(Runnable goRestart) throws IOException {
        try {
            // Java binary
            String java = System.getProperty("java.home") + "/bin/java";
            /*  Java virtual machine arguments */
            List<String> jvmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
            StringBuffer jvmArgsOneLine = new StringBuffer();
            for (String arg : jvmArgs) {
              /* if it's the agent argument it is ignored; otherwise; 

              the address of the old application and the new one will be 
              in conflict
             */
                if (!arg.contains("-agentlib")) {
                    jvmArgsOneLine.append(arg);
                    jvmArgsOneLine.append(" ");
                }
            }
            // init the command to execute
            final StringBuffer cmd = new StringBuffer("" + java + " " + jvmArgsOneLine);

            // program main and program arguments
            String[] mainCommand = System.getProperty(SUN_JAVA_COMMAND).split(" ");
            // if program has a JAR as main
            if (mainCommand[0].endsWith(".jar")) {
                // if it's a JAR, add -jar mainJar
                cmd.append("-jar " + new File(mainCommand[0]).getPath());
            } else {
            // else if it's a class, add the classpath and mainClass to the command
                cmd.append("-cp " + System.getProperty("java.class.path") + " " + mainCommand[0]);
            }
            /* finally add program arguments */
            for (int i = 1; i < mainCommand.length; i++) {
                cmd.append(" ");
                cmd.append(mainCommand[i]);
            }
            /* execute command in shutdown hook to be sure that all the 
             resources have been disposed before restarting the application */
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    try {
                        Runtime.getRuntime().exec(cmd.toString());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            if (goRestart != null) {
                goRestart.run();
                System.exit(0);
            }
        } catch (Exception e) {
            // something went wrong
            throw new IOException("Error while trying to restart the application", e);
        }
    }
}

 

Listing 11. Class that makes the application programmatically restartable

 

After adding the class shown in Listing 11, connect the script shown in Listing 12 to the Submit button of the form we previously discussed.

 

/* 
* Here, put code to recover the database parameters from the form
* into the following variables:
* dbUrl, dbName, dbPort, dbAddress, dbPassword
*/

Parameters param = new Parameters() ;
params.txtWriter(dbUrl, dbName, dbPort, dbAddress, dbPassword) ;
 Runnable r = new Runnable() {
            @Override
            public void run() {
                JOptionPane.showMessageDialog(null, "Application restarted successfully ", 
                "Restarting", JOptionPane.INFORMATION_MESSAGE);
            }

 };

 try {
            restartApplication(r);
 } catch (IOException ex) {
            Logger.getLogger(NewJFrame.class.getName()).log(Level.SEVERE, null, ex);
 }

 

Listing 12. Script for getting database parameters from the form

 

See Also

 

 

参考文献:https://community.oracle.com/docs/DOC-918906