This article is published simultaneously on my wechat official account. You can follow it by scanning the QR code at the bottom of the article or searching Guo Lin on wechat. The article is updated every weekday.
The Java volatile keyword (volatile) is a volatile keyword that can be used as a volatile keyword on a server.
It made me realize that some of my friends were wrong about the keyword volatile.
I know what volatile does, but I can’t figure out how it would be used in Android development.
So I’m going to write an article to analyze this keyword and answer these friends’ questions.
Since THIS article was written by me on Sunday, it may not be as full as usual, but I believe it can be clearly explained.
For those of you who have questions about the role of volatile, CPU caching may not be familiar, so let’s start with this concept.
While a program is running, the data is stored in memory, but executing the program is done by the CPU. When the CPU is performing a task and suddenly needs some data, it reads the data from memory, gets the data, and continues to perform the task.
This is the ideal way to work in theory, but there is a problem. We know that CPU development follows Moore’s Law and the number of transistors on an integrated circuit doubles every 18 months or so, so CPU speeds can only get faster and faster.
But light CPU speed is useless, because CPU speed is still to read data from memory, and this process is very slow, so the CPU development is greatly limited.
To solve this problem, CPU manufacturers introduced caching capabilities. The data stored in memory can also be stored in the CPU cache, so that when you frequently need to access a certain data there is no need to repeat from the memory to obtain, CPU cache, then directly take the data in the cache, so that you can greatly improve the efficiency of the CPU.
When the program needs to modify some data, it can also modify the data in the cache first, because this is very fast, and then write the data in the cache back to memory after the operation.
This works fine in single-threaded scenarios, or, to be precise, in single-threaded scenarios. However, in multi-core and multi-threaded scenarios, problems may arise.
We all know that nowadays, no matter mobile phones or computers, they often claim to be multicore. Multicore means that there are multiple computing units in the CPU. Because a unit can only handle one task at a time, even if we have multiple threads open, a single-core CPU can only handle some tasks in that thread, then pause and move on to another thread, and so on. A multi-core CPU, on the other hand, allows multiple tasks to be processed at the same time, which is of course more efficient.
But multicore cpus present a new challenge, which is that in multithreaded scenarios, the DATA in the CPU cache can be inaccurate. The reason is very simple, and we can understand it by looking at the picture below.
As you can see, there are two threads executing programs through two CPU units, but they share the same memory. Now CPU1 reads data A from memory and writes it to the cache, and CPU2 reads data A from memory and writes it to the cache.
So far, so good, but if thread 2 changes the value of data A, CPU2 will first update the value of A in the cache and then write it back to memory. If thread 1 accesses data A, it can return the value of A from the cache. You’ll notice that thread 1 and thread 2 are accessing the same data A and getting different values.
This is the visibility problem in multi-core and multi-threaded scenarios, because when one thread changes the value of a variable, that variable is not immediately visible to another thread.
To make the above theoretical knowledge more convincing, HERE I wrote a small Demo to verify the above statement, the code is as follows:
public class Main { static boolean flag; public static void main(String... args) { new Thread1().start(); new Thread2().start(); } static class Thread1 extends Thread { @Override public void run() { while (true) { if (flag) { flag = false; System.out.println("Thread1 set flag to false"); } } } } static class Thread2 extends Thread { @Override public void run() { while (true) { if (! flag) { flag = true; System.out.println("Thread2 set flag to true"); } } } } }Copy the code
This code is really quite simple, we start two threads to modify the same variable flag. Thread1 uses a while(true) loop and changes flag to false when it finds that flag is true. Thread2 also uses a while(true) loop, changing flag to true when it finds it to be false.
In theory, these two threads are running at the same time, so you should keep printing alternately, you change my value, I’ll change yours back.
Could this really be the case? So let’s just run it.
As you can see, the printing process only lasts for a moment and then stops printing, but the program is not finished and still appears to be running.
How is that possible? In theory, flag is either true or false. If true, Thread1 should print; if false, Thread2 should print. Why not print either?
Thread1 and Thread2 each have a flag value in the CPU cache. The flag value cached in Thread1 is false. The cached flag value in Thread2 is true, so neither side will print.
We then demonstrate the visibility problem with a practical example. So how to solve it?
The obvious answer is volatile.
One of the important roles of the volatile keyword is to solve the visibility problem by ensuring that when a variable is modified by one thread, it is immediately visible to another thread.
As for how volatile works, I don’t know how it works. The idea is that once a variable is declared volatile, any thread that modiifies it will expire all other CPU caches, so that other threads must retrieve the latest values from memory. That solves the visibility problem.
We can modify the code as follows:
public class Main { volatile static boolean flag; . }Copy the code
Yes, it’s as simple as that. Add the volatile keyword to the flag variable. Then re-run the program with the result shown below.
Everything worked as we expected.
Another important use of the volatile keyword is to prohibit instruction reordering, which is another interesting issue.
Let’s start with two pieces of code:
int a = 10;
int b = 5;
a = 20;
System.out.println(a + b);
int a = 10;
a = 20;
int b = 5;
System.out.println(a + b);
Copy the code
In the first code, we declare a variable a equal to 10, a variable B equal to 5, change the value of a to 20, and print the value of a + b.
In the second code, we declare a variable equal to 10, change the value of a to 20, declare a variable equal to 5, and print the value of a + b.
Is there a difference between these two pieces of code?
No guesswork, there is no difference between these two pieces of code, the order in which variable B is declared and variable A is modified is arbitrary, and neither of them is in the way of the other.
For this reason, the CPU does not necessarily execute code in the exact order we wrote it. Instead, it may, for efficiency reasons, reorder code that does not matter. This operation is called instruction reordering.
So there’s nothing wrong with reordering. Yes, but only in a single-threaded environment.
Many problems become more complex once they are in a multi-threaded environment, so let’s look at the following code:
public class Main { static boolean init; static String value; static class Thread1 extends Thread { @Override public void run() { value = "hello world"; init = true; } } static class Thread2 extends Thread { @Override public void run() { while (! init) { } value.toUpperCase(); }}}Copy the code
Thread1 initializes value data and sets init to true. Thread2 waits through a while loop for initialization to complete, and then operates on value data.
So does this code work? Not necessarily, because according to the instruction reordering theory, there is no order between the value and init variables in Thread1. If the CPU rearranges these two instructions, it is possible that initialization is complete, but value has not been assigned. Thread2’s while loop will break, and then null-pointer exceptions will occur when operating on value.
Therefore, once the instruction rearrangement function enters the multithreaded environment, also may have the problem.
The solution, of course, is volatile.
Declaring the volatile keyword on a variable also disables instruction reordering on that variable. So all we need to do is change the code like this to keep the program safe.
public class Main { volatile static boolean init; . }Copy the code
Now that we know what the main purpose of the volatile keyword is, as our friend mentioned at the beginning, many people can’t figure out how it would be used on Android.
In fact, I don’t think any technique should be copied mechanically. You just have to master it and think of it when you need it, rather than trying to figure out where I’m going to use it.
I see this keyword from time to time when I look at the source code for some Google libraries, and volatile is still used when multithreaded programming is involved.
I’ll give you a common example. On Android, we’ve probably all written a file download feature. To perform the download task, we need to start a thread, read the stream data from the network, write it locally, and repeat the process until all the data has been read.
Then I can express this process in the following simple code:
public class DownloadTask { public void download() { new Thread(new Runnable() { @Override public void run() { while (true) { byte[] bytes = readBytesFromNetwork(); if (bytes.length == 0) { break; } writeBytesToDisk(bytes); } } }).start(); }}Copy the code
So far so good.
But now comes a new requirement to allow users to cancel the download. As we all know, Java threads can not be interrupted, so if you want to do the function of canceling the download, generally through the marker bit to achieve, the code as follows:
public class DownloadTask {
boolean isCanceled = false;
public void download() {
new Thread(new Runnable() {
@Override
public void run() {
while (!isCanceled) {
byte[] bytes = readBytesFromNetwork();
if (bytes.length == 0) {
break;
}
writeBytesToDisk(bytes);
}
}
}).start();
}
public void cancel() {
isCanceled = true;
}
}
Copy the code
Here we added an isCanceled variable and a cancel() method. Setting the isCanceled variable to true when calling cancel() indicates that the download was canceled.
Then, in the Download () method, if it finds that the isCanceled variable is true, it breaks out of the loop and stops downloading, thus canceling the download.
Does this notation work? According to my actual test, it does basically work normally.
But is it really safe? No, because you will find that the Download () and cancel() methods run in two threads, so changes made to the isCanceled variable by cancel() are not necessarily immediately visible to the Download () method.
As a result, it is possible for the DOWNLOAD () method to have the isCanceled variable set to true, but the CPU cache used by the download() method still records the isCanceled variable as false, so that the download cannot be canceled.
Therefore, the safest way to write it is to declare the volatile keyword for the isCanceled variable:
public class DownloadTask { volatile boolean isCanceled = false; . }Copy the code
This will ensure that your undownload function is always safe.
That’s enough to explain the use of the volatile keyword and its specific uses in Android development.
Originally wanted to use Sunday a day to write a short essay, write write as if the last to write a lot of content, but as long as it is helpful to everyone.
If you want to learn about Kotlin and the latest on Android, check out my new book, Line 1, Version 3. Click here for more details.