Выполнение синхронизации раздела в Java
У меня был небольшой спор о производительности синхронизированного блока в Java. Это теоретический вопрос, который не влияет на приложение реальной жизни. Рассмотрим однопоточное приложение, в котором используются блокировки и синхронизация разделов. Этот код работает медленнее, чем тот же код без синхронизации разделов? Если да, то почему? Мы не обсуждаем concurrency, так как его единственное приложение с одним потоком
Upd
Нашел интересный тест, тестирующий его. Но это с 2001 года. В последней версии JDK ситуация может сильно измениться.
Ответы
Ответ 1
В HotSpot есть 3 типа блокировки
- Жир: JVM полагается на мьютексы OS для получения блокировки.
- Тонкий: JVM использует алгоритм CAS.
- Пристрастный: CAS - довольно дорогостоящая операция над некоторой архитектурой. Смещенная блокировка - это особый тип блокировки, оптимизированный для сценария, когда на объект работает только один поток.
По умолчанию JVM использует тонкую блокировку. Позже, если JVM определяет, что нет конкуренции, тонкая блокировка преобразуется в смещенную блокировку. Операция, которая изменяет тип блокировки, довольно дорога, поэтому JVM не применяет эту оптимизацию немедленно. Существует специальная опция JVM - XX: BiasedLockingStartupDelay = delay, которая сообщает JVM, когда такая оптимизация должна применяться.
После смещения этот поток может впоследствии заблокировать и разблокировать объект, не прибегая к дорогостоящим атомарным инструкциям.
Отвечайте на вопрос: это зависит. Но если предубежденный, однопоточный код с блокировкой и без блокировки имеет среднюю производительность.
Ответ 2
Однопоточный код будет работать медленнее при использовании блоков synchronized
. Очевидно, что вы не будете останавливать другие потоки, ожидая окончания других потоков, однако вам придется иметь дело с другими эффектами синхронизации, а именно с когерентностью кеширования.
Синхронизированные блоки используются не только для concurrency, но и для видимости. Каждый синхронизированный блок является барьером памяти: JVM может работать с переменными в регистрах вместо основной памяти, исходя из предположения, что несколько потоков не будут обращаться к этой переменной. Без блоков синхронизации эти данные могут храниться в кэше ЦП, а разные потоки на разных ЦП не будут видеть одни и те же данные. Используя блок синхронизации, вы вынуждаете JVM записывать эти данные в основную память для видимости для других потоков.
Таким образом, несмотря на то, что вы свободны от конкуренции за блокировку, JVM все равно придется делать домашнее хозяйство при промывке данных в основной памяти.
Кроме того, у этого есть ограничения оптимизации. JVM может свободно изменять порядок инструкций для обеспечения оптимизации: рассмотрим простой пример:
foo++;
bar++;
против
foo++;
synchronized(obj)
{
bar++;
}
В первом примере компилятор может одновременно загружать foo
и bar
, а затем увеличивать их оба, а затем сохранять их оба. Во втором примере компилятор должен выполнить загрузку/добавление/сохранение на foo
, затем выполнить команду load/add/save на bar
. Таким образом, синхронизация может повлиять на способность JRE оптимизировать инструкции.
(Отличная книга по модели памяти Java - Brian Goetz Java Concurrency На практике.)
Ответ 3
Есть некоторые накладные расходы при приобретении неконкурентоспособной блокировки, но на современных JVM это очень мало.
Ключевая оптимизация времени выполнения, относящаяся к этому случаю, называется "Предвзятое блокирование" и объясняется в Java SE 6 Performance White Paper.
Если вы хотите иметь некоторые номера производительности, которые имеют отношение к вашей JVM и аппаратной части, вы можете построить микро-бенчмарк, чтобы попытаться измерить эти накладные расходы.
Ответ 4
Использование блокировок, когда вам не нужно, замедлит ваше приложение. Это может быть слишком мало для измерения, или это может быть удивительно высоким.
IMHO Часто лучший подход заключается в использовании кода, свободного от блокировки, в однопоточной программе, чтобы он очистил этот код, который не предназначен для совместного использования в потоке. Это может быть более важным для обслуживания, чем любые проблемы с производительностью.
public static void main(String... args) throws IOException {
for (int i = 0; i < 3; i++) {
perfTest(new Vector<Integer>());
perfTest(new ArrayList<Integer>());
}
}
private static void perfTest(List<Integer> objects) {
long start = System.nanoTime();
final int runs = 100000000;
for (int i = 0; i < runs; i += 20) {
// add items.
for (int j = 0; j < 20; j+=2)
objects.add(i);
// remove from the end.
while (!objects.isEmpty())
objects.remove(objects.size() - 1);
}
long time = System.nanoTime() - start;
System.out.printf("%s each add/remove took an average of %.1f ns%n", objects.getClass().getSimpleName(), (double) time/runs);
}
печатает
Vector each add/remove took an average of 38.9 ns
ArrayList each add/remove took an average of 6.4 ns
Vector each add/remove took an average of 10.5 ns
ArrayList each add/remove took an average of 6.2 ns
Vector each add/remove took an average of 10.4 ns
ArrayList each add/remove took an average of 5.7 ns
С точки зрения производительности, если для вас важно 4 нс, вам необходимо использовать несинхронизированную версию.
Для 99% случаев использования ясность кода важнее производительности. Ясный, простой код часто также неплохо работает.
BTW: Я использую 4.6GHz i7 2600 с Oracle Java 7u1.
Для сравнения, если я делаю следующее, где perfTest1,2,3 идентичны.
perfTest1(new ArrayList<Integer>());
perfTest2(new Vector<Integer>());
perfTest3(Collections.synchronizedList(new ArrayList<Integer>()));
Я получаю
ArrayList each add/remove took an average of 2.6 ns
Vector each add/remove took an average of 7.5 ns
SynchronizedRandomAccessList each add/remove took an average of 8.9 ns
Если я использую общий метод perfTest
, он не может встроить код как оптимально, и все они медленнее
ArrayList each add/remove took an average of 9.3 ns
Vector each add/remove took an average of 12.4 ns
SynchronizedRandomAccessList each add/remove took an average of 13.9 ns
Подмена порядка тестов
ArrayList each add/remove took an average of 3.0 ns
Vector each add/remove took an average of 39.7 ns
ArrayList each add/remove took an average of 2.0 ns
Vector each add/remove took an average of 4.6 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.5 ns
ArrayList each add/remove took an average of 2.3 ns
Vector each add/remove took an average of 4.4 ns
ArrayList each add/remove took an average of 2.4 ns
Vector each add/remove took an average of 4.6 ns
по одному за раз
ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 3.0 ns
ArrayList each add/remove took an average of 2.3 ns
ArrayList each add/remove took an average of 2.2 ns
ArrayList each add/remove took an average of 2.4 ns
и
Vector each add/remove took an average of 28.4 ns
Vector each add/remove took an average of 37.4 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
Vector each add/remove took an average of 7.6 ns
Ответ 5
Этот примерный код (с 100 потоками, делающими 1,000,000 итераций каждый) демонстрирует разницу в производительности между тем, чтобы избежать и не избегать синхронизированного блока.
Вывод:
Total time(Avoid Sync Block): 630ms
Total time(NOT Avoid Sync Block): 6360ms
Total time(Avoid Sync Block): 427ms
Total time(NOT Avoid Sync Block): 6636ms
Total time(Avoid Sync Block): 481ms
Total time(NOT Avoid Sync Block): 5882ms
код:
import org.apache.commons.lang.time.StopWatch;
public class App {
public static int countTheads = 100;
public static int loopsPerThead = 1000000;
public static int sleepOfFirst = 10;
public static int runningCount = 0;
public static Boolean flagSync = null;
public static void main( String[] args )
{
for (int j = 0; j < 3; j++) {
App.startAll(new App.AvoidSyncBlockRunner(), "(Avoid Sync Block)");
App.startAll(new App.NotAvoidSyncBlockRunner(), "(NOT Avoid Sync Block)");
}
}
public static void startAll(Runnable runnable, String description) {
App.runningCount = 0;
App.flagSync = null;
Thread[] threads = new Thread[App.countTheads];
StopWatch sw = new StopWatch();
sw.start();
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(runnable);
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
do {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (runningCount != 0);
System.out.println("Total time"+description+": " + (sw.getTime() - App.sleepOfFirst) + "ms");
}
public static void commonBlock() {
String a = "foo";
a += "Baa";
}
public static synchronized void incrementCountRunning(int inc) {
runningCount = runningCount + inc;
}
public static class NotAvoidSyncBlockRunner implements Runnable {
public void run() {
App.incrementCountRunning(1);
for (int i = 0; i < App.loopsPerThead; i++) {
synchronized (App.class) {
if (App.flagSync == null) {
try {
Thread.sleep(App.sleepOfFirst);
} catch (InterruptedException e) {
e.printStackTrace();
}
App.flagSync = true;
}
}
App.commonBlock();
}
App.incrementCountRunning(-1);
}
}
public static class AvoidSyncBlockRunner implements Runnable {
public void run() {
App.incrementCountRunning(1);
for (int i = 0; i < App.loopsPerThead; i++) {
// THIS "IF" MAY SEEM POINTLESS, BUT IT AVOIDS THE NEXT
//ITERATION OF ENTERING INTO THE SYNCHRONIZED BLOCK
if (App.flagSync == null) {
synchronized (App.class) {
if (App.flagSync == null) {
try {
Thread.sleep(App.sleepOfFirst);
} catch (InterruptedException e) {
e.printStackTrace();
}
App.flagSync = true;
}
}
}
App.commonBlock();
}
App.incrementCountRunning(-1);
}
}
}
Ответ 6
Предполагая, что вы используете виртуальную машину HotSpot, я считаю, что JVM может признать, что нет никаких претензий к каким-либо ресурсам в блоке synchronized
и рассматривать его как "нормальный" код.