2010-01-22 4 views
44

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

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

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

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

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

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

+0

Я собирался предположить, что упаковка ThreadLocal в методе доступа, вероятно, решит ваши проблемы с InheritableThreadLocal, но вы, похоже, не хотите этого слышать. Кроме того, кажется, что вы используете InheritableThreadLocal в качестве внеполосного кадра вызова, что, если честно, похоже на запах кода. – kdgregory

+0

Что касается пулов потоков, основное преимущество - контроль: вы знаете, что вы не будете внезапно пытаться развернуть 100 000 потоков за секунду. – kdgregory

+2

@kdgregory: для вашей первой точки, ThreadLocals, о которой идет речь, используются в области подсчета компонентов Spring. Так работает Весна, а не то, что я контролирую. Для вашей второй точки потоки входящих запросов ограничены пулом потоков tomcat, поэтому ограничение присуще этому. – skaffman

ответ

36

Вот пример microbenchmark:

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 Client VM на E6400 Intel Core2 Duo @ 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 потоков).

9

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

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

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

+2

Я нахожу, что понятие «новая тема()» означает быть интересным , В современной JVM новый объект() 'не всегда выделяет новую память, он повторно использует ранее собранные мусором объекты. Интересно, есть ли причина, почему JVM не может иметь скрытый внутренний пул многоразовых потоков, так что 'new Thread()' не обязательно создает новый поток ядра. Вы получите эффективный пул потоков, не требуя для этого API. – skaffman

+2

Если это так, его нужно найти в некоторых JSR. Может быть 133 http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf – Bozho

+1

@skaffman Ваша гипотеза, похоже, соответствует тому, что я наблюдал, по крайней мере, в osx/jdk1.6. Несколько раз за последние несколько месяцев я участвовал в пуле ниток + «новый runnable» против семафора с подобным размером + «новый поток», и никогда не было какой-либо измеримой разницы. Семафорный подход, по-видимому, иногда ограничивает подход к пулу, но разница настолько крошечная и настолько редкая, что на самом деле просто подчеркивает, как они выглядят так, как вам тяжело работать, чтобы получить какую-либо разницу между ними. –

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

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

    /* 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() иметь желаемое поведение. Однако это немного ад-скакалка, и, кроме того, я не могу сказать, не отразится ли это (с отражением) даже большие накладные расходы, чем просто создание потоков.

+0

Транскрипция threadlocals - это то, что я действительно рассматривал. В моем конкретном случае, однако, я использую '@ Async' в Spring 3, которые отделяют механику« Callable »от бизнес-логики. Это очень круто, но означает, что вы не получаете доступ к самому исполнителю или к задачам, которые создаются. – skaffman

+1

Вы проверили, нет ли пружины какого-либо подключаемого механизма для замены иммерментации исполнителя? Если нет, то для дальнейшего взлома вы можете попытаться создать класс с тем же квалифицированным именем, что и тот, где вы в конечном итоге поместите свой собственный код, и пусть он будет загружен вместо исходного. Но это последнее средство. – Bozho

+0

Хммм, да, Spring позволяет вам указать исполнителя, используемого для @Async, так что да, есть способ пройти через threadlocals, хотя, как вы сказали, это все равно будет довольно уродливо. – skaffman

0

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

+1

То, что вы описываете, представляет собой пул потоков, который я уже описал в вопросе. – skaffman

+0

Если каждый поток запроса действует как ThreadPool, я думаю, я просто не понимаю, почему у вас не может быть 'private ThreadLocal local;', который вы инициируете каждый раз, когда поток запроса просыпается, и при обработке каждого рабочего потока, вы используете 'local.set()'/'local.get()', но, скорее всего, я неправильно понимаю вашу проблему. – Terje

Смежные вопросы