when and which is better? / Hebrew

when and which is better? / Hebrew

Hello, Habre!

Pattern Singleton guarantees the existence of only one instance of a class and provides a global access point to it. This pattern has become almost synonymous with code cleanliness in many Java scenarios where only one instance of an object is required. But no less interesting and flexible pattern is this Multiton. Less well-known, but no less powerful, it allows you to create multiple instances of a class and control their number and lifecycle via conditional keys.

In this article, we will look at these patterns and their differences.

Singleton

The point of Singleton is not just to limit the instantiation of a class to a single object, but also to provide a universal access point to this instance.

Monday helps control access to resources that by their very nature should be unique. That is: access to a database, file system or any shared resource that requires strict control over its state and availability.

Singleton works according to the principle delayed initialization — an instance of a class is created only when it is needed for the first time. That is, resources are used economically and efficiently, since initialization occurs only on demand.

From an implementation point of view, the main points of Singleton are:

  • Private designerwhich prevents the direct creation of an object of the class.

  • Static variable, which stores an instance of the Singleton class.

  • public static method which provides global access to this instance. If the instance has not yet been created, the method initializes it; if already created – returns a link to the existing one.

However, the use of Singleton is not always justified. In particular, its use can make it difficult to test the code due to difficulties with dependency mapping and can lead to unwanted coupling of system components. Also, in multi-threaded environments, it is required additionally. care to ensure Singleton thread safety, which is often achieved at the expense of synchronization, which in turn can negatively impact performance.

Code examples

Let’s consider five classic options:

The most basic variant of Singleton includes a private constructor and a static method for obtaining an instance:

public class ClassicSingleton {
    private static ClassicSingleton instance;

    private ClassicSingleton() {}

    public static ClassicSingleton getInstance() {
        if (instance == null) {
            instance = new ClassicSingleton();
        }
        return instance;
    }
}

To provide flow safety in a multithreaded environment, synchronization is used:

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton() {}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}

Lazy Holder uses a nested static class for deferred instance initialization:

public class LazyHolderSingleton {
    private LazyHolderSingleton() {}

    private static class LazyHolder {
        static final LazyHolderSingleton INSTANCE = new LazyHolderSingleton();
    }

    public static LazyHolderSingleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

Using listings for the Singleton implementation guarantees against serialization problems:

public enum EnumSingleton {
    INSTANCE;

    public void someMethod() {
        // Реализация метода
        System.out.println("Log message: " + message);
    }
}

Double-checked locking for lazy initialization reduces synchronization overhead by checking the instance twice:

public class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;

    private DoubleCheckedLockingSingleton() {}

    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

We implement logging and database connection management

Logging systems are a classic example of Singleton use, as a single logger instance is typically required for the entire application. That is, with the pattern, all parts of the program use the same instance of the logger:

public class LoggerSingleton {
    private static LoggerSingleton instance;
    private LoggerSingleton() {}

    public static synchronized LoggerSingleton getInstance() {
        if (instance == null) {
            instance = new LoggerSingleton();
        }
        return instance;
    }

    public void log(String message) {
        // примитивная реализация записи сообщения в лог
        System.out.println(System.currentTimeMillis() + ": " + message);
    }
}

Singleton is also often used to manage database connections, ensuring that the entire system uses a single connection, or to manage a pool of connections through an olin instance:

public class DatabaseConnectionSingleton {
    private static DatabaseConnectionSingleton instance;
    private Connection connection;

    private DatabaseConnectionSingleton() {
        try {
            // инициализация подключения к БД
            this.connection = DriverManager.getConnection("jdbc:example:database:url", "user", "password");
        } catch (SQLException e) {
            // обработка исключения
        }
    }

    public static DatabaseConnectionSingleton getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnectionSingleton.class) {
                if (instance == null) {
                    instance = new DatabaseConnectionSingleton();
                }
            }
        }
        return instance;
    }

    public Connection getConnection() {
        return connection;
    }
}

Multiton

Multiton it generative pattern design that provides controlled creation of class objects using an associative array for storing and accessing instances by unique keys.

The pattern is based on the idea that some classes may require not one but several instances, each of which is associated with a specific key. This avoids the global state associated with a Singleton.

Examples

Database connection pool:

public class DatabaseConnection {
    private static final Map<String, DatabaseConnection> instances = new HashMap<>();

    private DatabaseConnection() {
        // инициализация подключения к базе данных
    }

    public static synchronized DatabaseConnection getInstance(String dbName) {
        if (!instances.containsKey(dbName)) {
            instances.put(dbName, new DatabaseConnection());
        }
        return instances.get(dbName);
    }
}

Object caching:

public class ObjectCache {
    private static final Map<String, Object> cache = new HashMap<>();

    private ObjectCache() {
        // инициализация кэша
    }

    public static synchronized Object getInstance(String key) {
        if (!cache.containsKey(key)) {
            cache.put(key, new Object());
        }
        return cache.get(key);
    }
}

The log of various program modules:

public class Logger {
    private static final Map<String, Logger> loggers = new HashMap<>();
    private String moduleName;

    private Logger(String moduleName) {
        this.moduleName = moduleName;
        // инициализация логгера
    }

    public static synchronized Logger getInstance(String moduleName) {
        if (!loggers.containsKey(moduleName)) {
            loggers.put(moduleName, new Logger(moduleName));
        }
        return loggers.get(moduleName);
    }

    public void log(String message) {
        System.out.println("[" + moduleName + "] " + message);
    }

Let’s compare these patterns

Singleton:

  1. Description: ensures that there is only one instance of a class and provides a global access point to it.

  2. Instance management: one instance of a class.

  3. Limitation: there is no way to create multiple instances of a class.

  4. Identification: a single instance is identified by a static method or variable.

  5. Application: used for accessing shared resources, object caching, logging, etc.

Multitone:

  1. Description: similar to Singleton, but allows you to create and manage multiple instances of a class with unique keys.

  2. Instance management: many instances of a class, each of which is identified by a unique key.

  3. Limitation: limited number of copies identified by keys.

  4. Identification: each instance is identified by a unique key.

  5. Application: resource pool management, size-limited caching, database connection management, etc. are used.

For clarity, I made a table:

Parameter

Singleton

Multiton

Management

One instance of a class

Many copies by keys

Limitation

One copy

Limited quantity

Identification

By static method or variable

By a unique key

Application

General resources, caching, logging

Resource pools, limited caching, database connection management, etc.


So, if a task requires only one instance of a class to exist in an application, for example to access shared resources or manage global settings, Singleton is a good choice. If you need to have multiple instances of a class with different characteristics or options, for example to manage resource pools of limited size or to work with different data sources, then Multiton is of course much better.

Finally, I want to invite you to a free webinar where you will learn what a memory dump is, how to collect it and what tools exist for these purposes. Next, you’ll learn about the Eclipse Memory Analyzer tool, which you can use to investigate memory dumps, especially if you’re experiencing OutOfMemory.

Related posts