java ключевое слово synchronized
есть следующий код:
public class App {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int n = 40;
System.out.println("Start " + Thread.currentThread());
Long start = System.currentTimeMillis();
System.out.println(fibonachi(n));
System.out.println(n + " : " + (System.currentTimeMillis() - start) / 1000.0 + " сек");
System.out.println("End " + Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int n = 31;
System.out.println("Start " + Thread.currentThread());
Long start = System.currentTimeMillis();
System.out.println(fibonachi(n));
System.out.println(n + " : " + (System.currentTimeMillis() - start) / 1000.0 + " сек");
System.out.println("End " + Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int n = 45;
System.out.println("Start " + Thread.currentThread());
Long start = System.currentTimeMillis();
System.out.println(fibonachi(n));
System.out.println(n + " : " + (System.currentTimeMillis() - start) / 1000.0 + " сек");
System.out.println("End " + Thread.currentThread());
}
}).start();
}
static long fibonachi(long n){
if (n == 0){
return 0;
}
if (n == 1){
return 1;
}
else{
return fibonachi(n - 1) + fibonachi(n - 2);
}
}
}
Если его запустить будут запущены 3 потока и они начнут выполняться параллельно. Например:
Start Thread[Thread-0,5,main]
Start Thread[Thread-1,5,main]
1346269
31 : 0.006 сек
End Thread[Thread-1,5,main]
Start Thread[Thread-2,5,main]
102334155
40 : 0.722 сек
End Thread[Thread-0,5,main]
1134903170
45 : 6.841 сек
End Thread[Thread-2,5,main]
Но, если к методу fibonachi добавить ключевое слово synchronized То выполняется максимум 1 поток и то не всегда
Start Thread[Thread-1,5,main]
Start Thread[Thread-2,5,main]
Start Thread[Thread-0,5,main]
1346269
31 : 0.155 сек
End Thread[Thread-1,5,main]
А дальше программа "зависает". Как понимаю это происходит из-за того, что потоки не могут понять кто следующий будет исполнять этот метод?
Ответы (2 шт):
Добавление ключевого слова synchronized в метод fibonachi() по сути означает, что поток который "захватил монитор" (начал использовать) метод fibonachi() не будет давать возможности другим потокам его захватывать и остальные потоки будут ожидать завершения (освобождения монитора) захватившим потоком.
Небольшой трюк есть в самом методе fibonachi() - поскольку он рекурсивный, но фокус только кажется фокусом. В Java многократный захват монитора одним и тем же потоком разрешен, поэтому поток сам себя не будет ждать.
Остальные потоки не зависают. Они выполняются, но, во-первых, гораздо медленнее, а во-вторых из-за synchronized не параллельно, а по очереди.
То что монитор освободился и программа продолжает работать легко увидеть, если запустить ее в отладчике и приостановить после того как первый поток завершил выполнение.
Ну или если подождать:
Start Thread[Thread-0,5,main]
Start Thread[Thread-1,5,main]
Start Thread[Thread-2,5,main]
102334155
40 : 10.557 сек
End Thread[Thread-0,5,main]
1134903170
45 : 129.391 сек
End Thread[Thread-2,5,main]
1346269
31 : 129.528 сек
End Thread[Thread-1,5,main]
synchronized
Это ключевое слово на методе означает, что только один поток может вызвать данную функцию. Если несколько потоков пытаются делать это параллельно, то только первый выполняют функцию, а остальные ждут, пока первый на закончит. После того, как первый поток полностью завершит выполнение функции fibonachi (включая все рекурсивные вызовы), какой-то один из ожидающих потоков, сможет продолжить. А уже после него третий.
Замедление выполнения
Дело в том, что операции которые выполняются JVM, для того чтобы метод с synchronized работал так как должен, далеко не бесплатные. Они приводят к ощутимому уменьшению производительности.
Это видно даже по скорости выполнения одного потока.
Вот что показывают замеры времени вызова fibonachi для таких трех случаев:
- без синхронизации вообще (
a_nosynchronize) - с синхронизацией только самого внешнего вызова (
b_outersynchronize) - с синхронизацией всех рекурсивных вызовов (
c_allsynchronize)
Benchmark Mode Cnt Score Error Units
MonitorReentryBenchmark.a_nosynchronize thrpt 6 262.763 ± 16.314 ops/s
MonitorReentryBenchmark.b_outersynchronize thrpt 6 260.950 ± 17.297 ops/s
MonitorReentryBenchmark.c_allsynchronize thrpt 6 53.909 ± 3.928 ops/s
Как видим добавление синхронизации для рекурсивных вызовов существенно замедляет скорость работы. Тест конечно синтетический, но все же определенная закономерность видна. И нужно учитывать, что в этом тесте все происходит в одном потоке и не учитываются никакие эффекты связанные с собственно многопоточностью (как то синхронизация памяти), которыми, по-видимому, объясняется гораздо более сильное замедление нежели в этом тесте.
Тестировал с помощью такого jmh теста:
@State(Scope.Benchmark)
@Fork(value = 1)
@Warmup(iterations = 1, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 6, time = 2000, timeUnit = TimeUnit.MILLISECONDS)
public class MonitorReentryBenchmark {
@Benchmark
public void a_nosynchronize(Blackhole bh) {
bh.consume(nosync(30));
}
@Benchmark
public void b_outersynchronize(Blackhole bh) {
bh.consume(outersync(30));
}
@Benchmark
public void c_allsynchronize(Blackhole bh) {
bh.consume(allsync(30));
}
static long nosync(long n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
} else {
return nosync(n - 1) + nosync(n - 2);
}
}
synchronized static long outersync(long n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
} else {
return nosync(n - 1) + nosync(n - 2);
}
}
synchronized static long allsync(long n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
} else {
return allsync(n - 1) + allsync(n - 2);
}
}
}