JavaRush /Java Blog /Random EN /Flow management. The volatile keyword and the yield() met...

Flow management. The volatile keyword and the yield() method

Published in the Random EN group
Hello! We continue to study multithreading, and today we will get acquainted with a new keyword - volatile and the yield() method. Let's figure out what it is :)

Keyword volatile

When creating multi-threaded applications, we can face two serious problems. Firstly, during the operation of a multi-threaded application, different threads can cache the values ​​of variables (we will talk more about this in the lecture “Using volatile” ). It is possible that one thread changed the value of a variable, but the second did not see this change because it was working with its own cached copy of the variable. Naturally, the consequences can be serious. Imagine that this is not just some kind of “variable”, but, for example, the balance of your bank card, which suddenly began to randomly jump back and forth :) Not very pleasant, right? Secondly, in Java, read and write operations on fields of all types except longand doubleare atomic. What is atomicity? Well, for example, if you change the value of a variable in one thread int, and in another thread you read the value of this variable, you will get either its old value or a new one - the one that turned out after the change in thread 1. No “intermediate options” will appear there Maybe. However, this does not work with longand . doubleWhy? Because it's cross-platform. Do you remember how we said at the first levels that the Java principle is “written once, works everywhere”? This is cross-platform. That is, a Java application runs on completely different platforms. For example, on Windows operating systems, different versions of Linux or MacOS, and everywhere this application will work stably. longand double- the most “heavy” primitives in Java: they weigh 64 bits. And some 32-bit platforms simply do not implement the atomicity of reading and writing 64-bit variables. Such variables are read and written in two operations. First, the first 32 bits are written to the variable, then another 32. Accordingly, in these cases a problem may arise. One thread writes some 64-bit value to a variableХ, and he does it “in two steps.” At the same time, the second thread tries to read the value of this variable, and does it right in the middle, when the first 32 bits have already been written, but the second ones have not yet been written. As a result, it reads an intermediate, incorrect value, and an error occurs. For example, if on such a platform we try to write a number to a variable - 9223372036854775809 - it will occupy 64 bits. In binary form it will look like this: 100000000000000000000000000000000000000000000000000000000000000001 The first thread will start writing this number to a variable, and will first write the first 32 bits: 1000000000000000000000000000 00000 and then the second 32: 00000000000000000000000000000001 And a second thread can wedge into this gap and read the intermediate value of the variable - 100000000000000000000000000000000, the first 32 bits that have already been written. In the decimal system, this number is equal to 2147483648. That is, we just wanted to write the number 9223372036854775809 into a variable, but due to the fact that this operation on some platforms is not atomic, we got the “left” number 2147483648, which we don’t need, out of nowhere. and it is unknown how it will affect the operation of the program. The second thread simply read the value of the variable before it was finally written, that is, it saw the first 32 bits, but not the second 32 bits. These problems, of course, did not arise yesterday, and in Java they are solved using just one keyword - volatile . If we declare some variable in our program with the word volatile...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…it means that:
  1. It will always be atomically read and written. Even if it's 64-bit doubleor long.
  2. The Java machine will not cache it. So the situation when 10 threads work with their local copies is excluded.
This is how two very serious problems are solved in one word :)

yield() method

We have already looked at many methods of the class Thread, but there is one important one that will be new to you. This is the yield() method . Translated from English as “give in.” And that's exactly what the method does! Flow management.  The volatile keyword and the yield() method - 2When we call the yield method on a thread, it actually says to other threads: “Okay, guys, I’m not in a particular hurry, so if it’s important for any of you to get CPU time, take it, I’m not urgent.” Here's a simple example of how it works:
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
We sequentially create and launch three threads - Thread-0, Thread-1and Thread-2. Thread-0starts first and immediately gives way to others. After it it starts Thread-1, and also gives way. After that, it starts Thread-2, which is also inferior. We don’t have any more threads, and after Thread-2the last one has given up its place, the thread scheduler looks: “So, there are no more new threads, who do we have in the queue? Who was the last to give up their place before Thread-2? I think it was Thread-1? Okay, so let it be done.” Thread-1does its job until the end, after which the thread scheduler continues to coordinate: “Okay, Thread-1 has completed. Do we have anyone else in line?" There is Thread-0 in the queue: it gave up its place immediately before Thread-1. Now the matter has come to him, and he is being carried out to the end. After which the scheduler finishes coordinating the threads: “Okay, Thread-2, you gave way to other threads, they have all already worked. You were the last to give way, so now it’s your turn.” After this, Thread-2 runs to completion. The console output will look like this: Thread-0 gives way to others Thread-1 gives way to others Thread-2 gives way to others Thread-1 has finished executing. Thread-0 has finished executing. Thread-2 has finished executing. The thread scheduler can, of course, run threads in a different order (for example, 2-1-0 instead of 0-1-2), but the principle is the same.

Happens-before rules

The last thing we'll touch on today is the " happens before " principles. As you already know, in Java, most of the work of allocating time and resources to threads to complete their tasks is done by the thread scheduler. Also, you have seen more than once how threads are executed in an arbitrary order, and most often it is impossible to predict it. And in general, after the “sequential” programming that we did before, multithreading looks like a random thing. As you have already seen, the progress of a multithreaded program can be controlled using a whole set of methods. But in addition to this, in Java multithreading there is another “island of stability” - 4 rules called “ happens-before ”. Literally from English this is translated as “happens before”, or “happens before”. The meaning of these rules is quite simple to understand. Imagine that we have two threads - Aand B. Each of these threads can perform operations 1and 2. And when in each of the rules we say “ A happens-before B ”, this means that all the changes made by the thread Abefore the operation 1and the changes that this operation entailed are visible to the thread Bat the time the operation is performed 2and after the operation is performed. Each of these rules ensures that when writing a multi-threaded program, some events will happen before others 100% of the time, and that the thread Bat the time of the operation 2will always be aware of the changes that the thread Аmade during the operation 1. Let's look at them.

Rule 1.

Releasing a mutex happens before occurs before another thread acquires the same monitor. Well, everything seems clear here. If the mutex of an object or class is acquired by one thread, for example, a thread А, another thread (thread B) cannot acquire it at the same time. You need to wait until the mutex is released.

Rule 2.

Thread.start() The happens before method Thread.run(). Nothing complicated either. You already know: in order for the code inside the method to start executing run(), you need to call the method on the thread start(). It is his, and not the method itself run()! This rule ensures that Thread.start()the values ​​of all variables set before execution will be visible inside the method that started executing run().

Rule 3.

Method completion run() happens before method exit join(). Let's return to our two streams - Аand B. We call the method join()in such a way that the thread Bmust wait until completion Abefore doing its work. This means that the method run()of object A will definitely run until the very end. And all changes in the data that occur in the run()thread method Awill be completely visible in the thread Bwhen it waits for completion Aand starts working itself.

Rule 4.

Writing to a volatile variable happens before reading from the same variable. By using the volatile keyword, we will, in fact, always get the current value. Even in the case of longand double, the problems with which were discussed earlier. As you already understand, changes made in some threads are not always visible to other threads. But, of course, very often there are situations when such program behavior does not suit us. Let's say we assigned a value to a variable in a thread A:
int z;.

z= 555;
If our thread Bwere to print the value of a variable zto the console, it could easily print 0 because it doesn't know about the value assigned to it. So, Rule 4 guarantees us: if you declare a variable zas volatile, changes to its values ​​in one thread will always be visible in another thread. If we add the word volatile to the previous code...
volatile int z;.

z= 555;
...the situation in which the stream Bwill output 0 to the console is excluded. Writing to volatile variables occurs before reading from them.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION