What is happens-before in Java Concurrency? An example
A couple of days ago, one of my readers messaged me on LinkedIn about a Java interview question he has recently faced — what is the happens-before relationship in Java concurrency? What is the benefit of it, and how exactly it works? He kind of has some ideas about that its related to the Java Memory Model and provides some sort of visibility guaranteed but couldn’t explain with conviction to his interviewer, particularly with a code example and was a bit disappointed. He then asked me if I can write an article about it. I said you should have read the Java concurrency in Practice book or join the Java Concurrency in Practice Bundle course by Heinz Kabutz before the interview, that would have helped, but nonetheless, I liked the idea to just provide a quick overview of what is the happens-before relationship between threads in Java.
To understand the happens-before relationship, you need to first understand what problems can occur if the same variable is accessed by multiple threads? particularly if one thread writes into the variable, and one thread read from it at the same time.
For example, let’s say we have the following code, which is executed by Thread T1, (note, integer variable y is initialized before x )
Now, we have another piece of code which is executed by another thread T2 where the value of both the variable is printed (note variable x is printed before y ) :
System.out.print(x); System.out.println(y);
What do you think? What should be printed on the screen? If this code is executed by the same thread, then it’s guaranteed that 2 and 1 will be printed, but with multiple threads, there is no guaranteed behavior. It’s entirely possible that T2 could not see the assignments made by T1 and just print 0 for both x and y .
It’s also possible that it can just see the initialization of either x or y and print accordingly.
This is scary and unpredictable, think what will happen if X is money in your bank account, you definitely want a predictable behavior. That’s where happens-before comes into play. let’s learn what is happens-before in Java and how it helps in writing predictable concurrent code.
What is Happens-Before Relationship in Java? Example
The Happens-before relationship provides some sort of ordering and visibility guarantee. There is a lot of rule concerning Happens-before (which you can read on Java Concurrency in Practice). Still, the most important one is if there is a synchronization like a synchronized block or a volatile variable then.
— «A volatile write will happen before another volatile read.»
— «An unlock on the synchronized block will happen before another lock.»
And, (this is most important)
All the changes which are visible to T1 before a volatile write or a synchronized unlock will be visible to thread T2 after a volatile read of the same variable or locking on the same monitor.
This statement is the most important one, and it can be best understood by the following diagram:
Image credit — Java Concurrency in Practice |
You can see that before unlock, thread T1 can see y=1 and x=1 , so after T2 gets a lock on the same object, it can also see y=1 and x=1 , even though y is not a volatile variable or read inside the lock or synchronized context. This is also true for volatile write and volatile read, which offers slightly less form of synchronization but a great alternative when it comes to visibility.
Now, let’s go back to our example if we modify our code like this:
T1: int y = 1; volatile int x = 2; T2: System.out.print(x); System.out.println(y)What do you think? What does the code print now? 2 and 1 or 2 and 0?
Well, if you apply the happens-before than when T1 does a volatile write, and T2 does a volatile read, then it will also see the value of y=1 , which is not volatile. Isn't behavior is now more predictable.
The critical thing to remember here is that even the value of a non-volatile variable is visible to thread T2. This is just one example of a happens-before relationship but very useful in analyzing the behavior of a multi-threaded program.
If you are serious about improving your understanding of Java concurrency knowledge and skills, I strongly suggest you should read the Java Concurrency in Practice book by Brian Goetz, Joshua Bloch, Doug Lea, and team, one of the best resources for Java developers.
But, If you find trouble understanding the book and need someone to explain to you those concepts with more examples, you can also join the Java Concurrency in Practice Bundle course by Heinz Kabutz which is based upon this book and makes it easy to understand those tricky concurrency concepts. It's like someone explaining your Java Concurrency in Practice book live.
Even if you have read the book, going through this course will help you to understand Java Concurrency better and help you to write robust concurrent code with fewer bugs. Sorry, it's almost impossible to write concurrent code without subtle bugs, at least in the first iteration.
The only thing is that some of you may find the course not affordable. There are options to make interest free part payments and if you can, you should that. But, if that's not the case then you can also check out the more affordable course on Udemy and Pluralsight they provide an excellent alternative to learn Java concurrency better.
- The Java Developer RoadMap (roadmap)
- 10 Java Multithreading and Concurrency Best Practices (article)
- Top 50 Multithreading and Concurrency Questions in Java (questions)
- Top 5 Books to Master Concurrency in Java (books)
- Difference between CyclicBarrier and CountDownLatch in Java? (answer)
- How to avoid deadlock in Java? (answer)
- Understanding the flow of data and code in Java program (answer)
- Is Java Concurrency in Practice still valid? (answer)
- How to do inter-thread communication in Java using wait-notify? (answer)
- 10 Tips to become a better Java Developer (tips)
- 5 Courses to Learn Java Multithreading in-depth (courses)
- 10 Advanced books for Experienced Programmers (books)
- 50+ Thread Interview Questions for Beginners (questions)
- Top 5 skills to Crack Coding interviews (article)
- 10 Advanced Core Java Courses for Experienced Programmers (courses)
P.S. - If you are new into Java Multithreading and looking for a free online training course to learn Multithreading and Concurrency basics then I also suggest you check out this Java Multithreading a free course on Udemy. It's completely free and all you need is a free Udemy account to join this course.
Java Memory Model
Модель памяти Java (Java Memory Model, JMM) описывает поведение потоков в среде исполнения Java. Модель памяти — часть семантики языка Java, и описывает, на что может и на что не должен рассчитывать программист, разрабатывающий ПО не для конкретной Java-машины, а для Java в целом.
Исходная модель памяти Java (к которой, в частности, относится “потоколокальная память”), разработанная в 1995 году, считается неудачной: многие оптимизации невозможно провести, не потеряв гарантию безопасности кода. В частности, есть несколько вариантов написать многопоточного “одиночку”:
- либо каждый акт доступа к одиночке (даже когда объект давно создан, и ничего уже не может измениться) будет вызывать межпоточную блокировку;
- либо при определенном стечении обстоятельств система выдаст недостроенного одиночку;
- либо при определенном стечении обстоятельств система создаст два одиночки;
- либо конструкция будет зависеть от особенностей поведения той или иной машины.
Поэтому механизм работы памяти был переработан. В 2005 году, с выходом Java 5 был презентован новый подход, который был еще улучшен с выходом Java 14.
В основе новой модели лежат три правила:
Правило № 1: однопоточные программы исполняются псевдопоследовательно. Это значит: в реальности процессор может выполнять несколько операций за такт, заодно изменив их порядок, однако все зависимости по данным остаются, так что поведение не отличается от последовательного.
Правило № 2: нет невесть откуда взявшихся значений. Чтение любой переменной (кроме не-volatile long и double, для которых это правило может не выполняться) выдаст либо значение по умолчанию (ноль), либо что-то, записанное туда другой командой.
И правило № 3: остальные события выполняются по порядку, если связаны отношением строгого частичного порядка “выполняется прежде” (happens before).
Happens before
Лесли Лэмпорт придумал понятие Happens before. Это отношение строгого частичного порядка, введенное между атомарными командами (++ и -- не атомарны) и не означающее “физически прежде”.
Оно говорит о том, что вторая команда будет “в курсе” изменений, проведенных первой.
Например, одно выполняется прежде другого для таких операций:
Синхронизация и мониторы:
- Захват монитора (метод lock , начало synchronized) и все, что происходит в том же потоке после него.
- Возврат монитора (метод unlock , конец synchronized) и все, что происходит в том же потоке перед ним.
- Возврат монитора и последующий захват другим потоком.
Запись и чтение:
- Запись в любую переменную и последующее чтение ее же в одном потоке.
- Все, что в том же потоке перед записью в volatile-переменную, и сама запись. volatile-чтение и все, что в том же потоке после него.
- Запись в volatile-переменную и последующее считывание ее же. Volatile-запись взаимодействует с памятью так же как и возврат монитора, а чтение как захват. Получается, что если один поток записал в volatile-переменную, а второй обнаружил это, все, что предшествует записи, выполняется раньше всего, что идет после чтения; смотри рисунок.
Обслуживание объекта:
- Статическая инициализация и любые действия с любыми экземплярами объектов.
- Запись в final-поля в конструкторе и все, что после конструктора. Как исключение – соотношение happens-before не соединяется транзитивно с другими правилами и поэтому может вызвать межпоточную гонку.
- Любая работа с объектом и finalize() .
Обслуживание потока:
- Запуск потока и любой код в потоке.
- Зануление переменных, относящихся к потоку, и любой код в потоке.
- Код в потоке и join() ; код в потоке и isAlive() == false .
- interrupt() потока и обнаружение факта остановки.
Нюансы работы Happens before
Освобождение (releasing) монитора happens-before происходит прежде, чем получение (acquiring) того же монитора. Стоит обратить внимание, что именно освобождение, а не выход, то есть за безопасность при использовании wait можно не беспокоиться.
Рассмотрим, как это знание поможет нам исправить наш пример. В данном случае все очень просто: достаточно убрать внешнюю проверку и оставить синхронизацию как есть. Теперь второй поток гарантированно увидит все изменения, потому что он получит монитор только после того, как другой поток его отпустит. А так как он его не отпустит, пока все не проинициализирует, мы увидем все изменения сразу, а не по отдельности:
public class Keeper < private Data data = null; public Data getData() < synchronized(this) < if(data == null) < data = new Data(); >> return data; > >
Запись в volatile переменную happens-before чтение из той же переменной. То изменение, которое мы внесли, конечно, исправляет некорректность, но возвращает того, кто написал изначальный код, туда, откуда он пришел — к блокировке каждый раз. Спасти может ключевое слово volatile. Фактически, рассматриваемое утверждение значит, что при чтении всего, что объявлено volatile, мы всегда будем получать актуальное значение.
Кроме того, как я говорил раньше, для volatile полей запись всегда (в том числе long и double) является атомарной операцией. Еще один важный момент: если у вас есть volatile сущность, имеющая ссылки на другие сущности (например, массив, List или какой-нибудь еще класс), то всегда “свежей” будет только ссылка на саму сущность, но не на все, в нее входящее.
Итак, обратно к нашим Double-locking баранам. С использованием volatile исправить ситуацию можно так:
public class Keeper < private volatile Data data = null; public Data getData() < if(data == null) < synchronized(this) < if(data == null) < data = new Data(); >> > return data; > >
Тут у нас по-прежнему есть блокировка, но только в случае, если data == null. Остальные случаи мы отсеиваем, используя volatile read. Корректность обеспечивается тем, что volatile store happens-before volatile read, и все операции, которые происходят в конструкторе, видны тому, кто читает значение поля.