Javenue logo

Javenue

Программирование на Java

Информационные технологии

Сколько нужно потоков для обработки пользовательских запросов или крайности при использовании Singleton

Более трех лет назад (в 2005 году), когда я только перешел на Java, написал статью об использовании паттерна Singleton. Статья доступна по этой ссылке - Паттерн Singleton. Не смотря на прошествие значительного промежутка времени, статью до сих пор читают и комментируют. Меня заинтересовала одна ссылка из комментариев, в которой довольно неплохо рассказываются нюансы реализации паттерна Singleton.

В статье говорится о том, что следующая реализация с lazy инициализацией является не оптимальной при достаточно частом обращении к getInstance():

public final class Singleton {
    private static Singleton _instance = null;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (_instance == null)
            _instance = new Singleton();
        return _instance;
    }
}

Давайте попробуем определить, что же такое "достаточно часто". Так же в статье я хочу показать Вам одну достаточно смешную реализацию Singleton, идею которой рассказал мне мой друг.

Реализацию паттерна Singleton, которую я привел выше, ругают за то, что при большом количестве потоков много потоков будут висеть на мониторе и ждать своего часа. Для приблизительной оценки слова "много" я предлагаю Вам такой програмный код (импорт классов опущен):

public class TestServlet extends HttpServlet {
    private AtomicInteger counter = new AtomicInteger();

    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        counter.incrementAndGet();

        try { Thread.sleep(100); }
        catch (InterruptedException ignore) { }

        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().print("<html></html>");

        System.out.println(counter.get());
        counter.decrementAndGet();
    }

    public static void main(String[] args) throws Exception {
        Server server = new Server(8888);

        BoundedThreadPool threadPool = new BoundedThreadPool();
        threadPool.setMaxThreads(128);
        server.setThreadPool(threadPool);

        Context context = new Context();
        context.setContextPath("/");
        context.addServlet(new ServletHolder(new TestServlet()), "/");

        new Thread(new Runnable() {
            public void run() {
                try { Thread.sleep(1000); }
                catch (InterruptedException ignore) { }

                for (int i = 0; i < 500; i++) {
                    UserEmulator userEmulator = new UserEmulator();
                    userEmulator.setDaemon(true);
                    userEmulator.start();
                }
            }
        }).start();

        server.addHandler(context);
        server.start();
    }

    public static class UserEmulator extends Thread {
        public void run() {
            while (true) {
                try {
                    URL url = new URL("http://localhost:8888/");

                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.getContent();
                    connection.disconnect();

                    Thread.sleep(10000 + new Random().nextInt(5000));
                } catch (Exception e) { }
            }
        }
    }
}

Итак, мы имеем сервлет (servlet), который обрабатывает пользовательский запрос за время приблизительно равное 100 миллисекунд. Это достаточно адекватное время, которое годится для подавляющего большинства серьезных веб-проектов. Если Вам это время кажется не достаточным, можете его увеличить. Сервлет содержит поле counter, которое будет отображать количество параллельных запросов пользователей.

Кроме этого, мы имеем класс UserEmulator, который эмулирует работу пользователя на веб-ресурсе. Пользователи у меня получились слишком активные - выполняют одно действие в 10-15 секнуд. Но ничего страшного, это даже наоборот хорошо, - никто не будет говорить, что я упрощаю себе задачу.

Сервлет я запускаю под Jetty (http://www.mortbay.org/jetty/) - это очень хороший веб-сервер с сервлет конейнером, который, в отличии от Tomcat, легко интегрируется в любое приложение без всяких конфигурационных файлов.

После запуска веб-сервера создаются 500 пользовательских потоков (thread), которые периодически заходят на главную станицу нашего портала localhost:8888 :).

Число реально параллельных соединений на стороне веб-сервера выводится в консоль. После того, как пройдет некоторое время и работа программы нормализуется, мы увидим, что для обработки запросов от 500 пользователей требует не больше 15 параллельных потоков. То есть 100 потоков обрабатывают 3-5 тысячи активных пользователей одновременно. Лично мне известно мало приложений (и веб и не веб), на которых одновременно может активно работать 3-5 тысяч пользователей.

Теперь, возвращаясь к паттерну Singleton, а точнее к его реализации, я смело заявляю, что усложнять програмный код буду только в крайних случаях. Что и вам советую.

Да кстати, вот реализация, на которую предлагают заменить первоначальный вариант:

public final class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

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

Опять же, данный код не защищает от того, что потоки будут висеть на мониторе при создании экземпляра класса Singleton. Единственное преимущество - после конструирования объекта потоки будут получать доступ к инстансу Singleton без синхронизации. Но и тут не все - Allen Holub заметил, что использование volatile может привести к потерям производительности на мультипроцессорных системах.

Теперь обещанная смешная, но тем не менее заслуживающая внимания, реализация паттерна Singleton, идею которой предложил мне мой друг:

public final class Singleton {
    private static volatile Strategy strategy = new CreateAndReturnStrategy();
    private static Singleton instance;

    private static interface Strategy {
        Singleton getInstance();
    }
    
    private static class ReturnStrategy implements Strategy {
        public final Singleton getInstance() {
            return instance; 
        }
    }
    
    private static class CreateAndReturnStrategy implements Strategy {
        public final Singleton getInstance() {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                    strategy = new ReturnStrategy();
                } 
            }
            return instance; 
        }
    }
    
    private Singleton() {}

    public static Singleton getInstance() {
        return strategy.getInstance();
    }
}

По идее это будет даже быстрее работать, чем проверка с помощью оператора if, так как тут используется прямолиенйная диспетчеризация вызовов.

Желаю Всем удачи.



Комментариев: 3

  Выйти

  * для публикации комментариев нужно  

cleam:

Ещё один вариант:

public class Singleton {
  private static class InstanceHolder {
    private static Singleton instance = new Singleton();
  }

  public static Singleton getInstance() {
    return InstanceHolder.instance;
  }
}

Ну а вообще синглтоны зло, да.

artemv:

@cleam предложил лучший вариант*(только как-то робко и final забыл).

*потому что:

- он threadsafe(jvm гарантирует - только один поток загрузит внутренний класс и инициализирует статические поля)

- он lazy-loading(внутренний класс не загрузится пока к нему не обратишься)

- он прост.

Fox:

Так это-ж Jetty co своим NIO… а Tomcat то 5000 потоков создаст(нет не создаст :) в реальности) так причом тут сингельтон…

А если говорить о сингельтонах то с приведёнными выше примерами есть несколько проблем такие как - AccessibleObject.setAccessible и возможно проблема сериализации…

Следует просто писать

public enum Singleton { //Реальный сингельтон
  INSTANCE;
}

Удачи