Единственное решение, которое я нашел, - это скопировать контекст в конечную переменную вне потока и применить его для каждой отдельной итерации:
Map<String, String> contextMap = MDC.getCopyOfContextMap();
Stream.iterate(0, i -> i + 1).parallel()
.peek(i -> MDC.setContextMap(contextMap))
// ...logic...
// in case you're using a filter, you need to use a predicate and combine it with a clear step:
filter(yourPredicate.or(i -> {
MDC.clear();
return false;
}))
// clear right before terminal operation
.peek(i -> MDC.clear())
.findFirst();
// since the initial thread is also used within the stream and the context is cleared there,
// we need to set it again to its initial state
MDC.setContextMap(contextMap);
Стоимость этого решения составляет 1) несколько микросекундна 100 итераций и 2) хуже читаемость, но оба приемлемы:
- Это тест, сравнивающий
IntStream.range(0, 100).parallel().sum()
(= базовый уровень) с тем же потоком, который использует эту логику копирования MDC:
Benchmark Mode Cnt Score Error Units
MDC_CopyTest.baseline thrpt 5 0,038 ± 0,005 ops/us
MDC_CopyTest.withMdc thrpt 5 0,024 ± 0,001 ops/us
MDC_CopyTest.baseline avgt 5 28,239 ± 1,308 us/op
MDC_CopyTest.withMdc avgt 5 40,178 ± 0,761 us/op
Чтобы улучшить читабельность, его можно заключить в небольшой вспомогательный класс:
public class MDCCopyHelper {
private Map<String, String> contextMap = MDC.getCopyOfContextMap();
public void set(Object... any) {
MDC.setContextMap(contextMap);
}
public void clear(Object... any) {
MDC.clear();
}
public boolean clearAndFail() {
MDC.clear();
return false;
}
}
Потоковый код выглядит тогда немного лучше:
MDCCopyHelper mdcHelper = new MDCCopyHelper();
Optional<Integer> findFirst = Stream.iterate(0, i -> i + 1)
.parallel()
.peek(mdcHelper::set)
// ...logic...
// filters predicates should be combined with clear step
.filter(yourPredicate.or(mdcHelper::clearAndFail))
// before terminal call:
.peek(mdcHelper::clear)
.findFirst();
mdcHelper.set();