Накладные расходы на создание потоков Java

Традиционная мудрость говорит нам о том, что приложения большого объема предприятия Java должны использовать пул потоков, предпочитая создавать новые рабочие потоки. Использование java.util.concurrent делает это простым.

Однако существуют ситуации, когда пул потоков не подходит. Конкретным примером, с которым я сейчас борюсь, является использование InheritableThreadLocal, который позволяет передавать ThreadLocal переменные вниз на любые порожденные потоки. Этот механизм ломается при использовании пулов потоков, поскольку рабочие потоки обычно не генерируются из потока запросов, но уже существуют.

Теперь есть пути вокруг этого (локаторы потоков могут быть явно переданы), но это не всегда правильно или практично. Самое простое решение - порождать новые рабочие потоки по запросу и InheritableThreadLocal выполнять свою работу.

Это возвращает нас к вопросу - если у меня есть сайт с большим объемом, где потоки пользовательского запроса порождают полдюжины рабочих потоков каждый (т.е. не используют пул потоков), это приведет к тому, что JVM будет проблемой? Мы потенциально говорим о создании нескольких сотен новых потоков каждую секунду, каждая из которых длится менее секунды. Могут ли современные JVM оптимизировать это? Я помню дни, когда объединение объектов было желательно в Java, потому что создание объекта было дорогостоящим. С тех пор это стало ненужным. Мне интересно, относится ли это к пулу потоков.

Я бы оценил это, если бы знал, что измерить, но я боюсь, что проблемы могут быть более тонкими, чем можно измерить с помощью профилировщика.

Примечание: мудрость использования локаторов потоков здесь не проблема, поэтому, пожалуйста, не предлагайте, чтобы я их не использовал.

Ответ 1

Вот пример микрообъектива:

public class ThreadSpawningPerformanceTest {
static long test(final int threadCount, final int workAmountPerThread) throws InterruptedException {
    Thread[] tt = new Thread[threadCount];
    final int[] aa = new int[tt.length];
    System.out.print("Creating "+tt.length+" Thread objects... ");
    long t0 = System.nanoTime(), t00 = t0;
    for (int i = 0; i < tt.length; i++) { 
        final int j = i;
        tt[i] = new Thread() {
            public void run() {
                int k = j;
                for (int l = 0; l < workAmountPerThread; l++) {
                    k += k*k+l;
                }
                aa[j] = k;
            }
        };
    }
    System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms.");
    System.out.print("Starting "+tt.length+" threads with "+workAmountPerThread+" steps of work per thread... ");
    t0 = System.nanoTime();
    for (int i = 0; i < tt.length; i++) { 
        tt[i].start();
    }
    System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms.");
    System.out.print("Joining "+tt.length+" threads... ");
    t0 = System.nanoTime();
    for (int i = 0; i < tt.length; i++) { 
        tt[i].join();
    }
    System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms.");
    long totalTime = System.nanoTime()-t00;
    int checkSum = 0; //display checksum in order to give the JVM no chance to optimize out the contents of the run() method and possibly even thread creation
    for (int a : aa) {
        checkSum += a;
    }
    System.out.println("Checksum: "+checkSum);
    System.out.println("Total time: "+totalTime*1E-6+" ms");
    System.out.println();
    return totalTime;
}

public static void main(String[] kr) throws InterruptedException {
    int workAmount = 100000000;
    int[] threadCount = new int[]{1, 2, 10, 100, 1000, 10000, 100000};
    int trialCount = 2;
    long[][] time = new long[threadCount.length][trialCount];
    for (int j = 0; j < trialCount; j++) {
        for (int i = 0; i < threadCount.length; i++) {
            time[i][j] = test(threadCount[i], workAmount/threadCount[i]); 
        }
    }
    System.out.print("Number of threads ");
    for (long t : threadCount) {
        System.out.print("\t"+t);
    }
    System.out.println();
    for (int j = 0; j < trialCount; j++) {
        System.out.print((j+1)+". trial time (ms)");
        for (int i = 0; i < threadCount.length; i++) {
            System.out.print("\t"+Math.round(time[i][j]*1E-6));
        }
        System.out.println();
    }
}
}

Результаты на 64-разрядной Windows 7 с 32-разрядной клиентской виртуальной машиной Sun Java 1.6.0_21 на Intel Core2 Duo E6400 @2.13 ГГц выглядят следующим образом:

Number of threads  1    2    10   100  1000 10000 100000
1. trial time (ms) 346  181  179  191  286  1229  11308
2. trial time (ms) 346  181  187  189  281  1224  10651

Выводы. Два потока работают почти в два раза быстрее, чем один, как и ожидалось, так как мой компьютер имеет два ядра. Мой компьютер может генерировать почти 10000 потоков в секунду, т.е. е. затраты на создание потоков составляют 0,1 миллисекунды. Следовательно, на такой машине несколько сотен новых потоков в секунду создают незначительные накладные расходы (что также можно увидеть, сравнивая числа в столбцах для 2 и 100 потоков).

Ответ 2

Прежде всего, это, конечно же, будет сильно зависеть от того, какой JVM вы используете. ОС также будет играть важную роль. Предполагая, что Sun JVM (Hm, мы все еще называем это?):

Одним из основных факторов является память стека, выделенная для каждого потока, которую вы можете настроить с помощью параметра -Xssn JVM - вы захотите использовать самое низкое значение, которое вы можете избежать.

И это всего лишь предположение, но я думаю, что "несколько сотен новых потоков каждую секунду" определенно выше того, что JVM предназначен для удобного управления. Я подозреваю, что простой тест быстро выявит довольно непростые проблемы.

Ответ 3

  • для вашего теста вы можете использовать JMeter + профилировщик, который должен дать вам прямой обзор поведения в таких тяжелая среда. Просто позвольте ему работать в течение часа и следить за памятью, процессором и т.д. Если ничего не сломается и CPU не перегревается, это нормально:)

  • возможно, вы можете получить пул потоков или настроить (расширять) тот, который вы используете, добавив некоторый код, чтобы каждый из соответствующих InheritableThreadLocal устанавливался при создании Thread из потока -бассейн. Каждый Thread имеет эти свойства private-private:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.  
     */ 
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    

    Вы можете использовать их (ну, с отражением) в сочетании с Thread.currentThread(), чтобы иметь желаемое поведение. Однако это немного ад-скакалка, и, кроме того, я не могу сказать, не отразится ли она (с отражением) даже больших накладных расходов, чем просто создание потоков.

Ответ 4

Мне интересно, нужно ли создавать новые потоки для каждого пользовательского запроса, если их типичный жизненный цикл короче секунды. Не могли бы вы использовать какую-то очередь Notify/Wait, в которой вы порождаете определенное количество потоков (daemon), и все они ждут, пока не будет решена задача. Если очередь задач становится длинной, вы создаете дополнительные потоки, но не в соотношении 1-1. Скорее всего, это будет лучше, чем порождать сотни новых потоков, жизненные циклы которых настолько коротки.