4
头图

This is why's 99th original article

Hello, I am why brother.

No, this photo is not me. The old man mentioned in the title is this buddy. This has to be talked about a few days ago.

A few days ago, I found that in a technology group gathered by big guys, big guys had a heated discussion on the Happens-Before relationship and as-if-serial semantics.

And when I looked at the time, it was almost 23 o'clock, and the big guys were all like this, so I had to follow them, so I checked their chat records.

And I, as a rookie, although I don't have any sense of participation, I feel that what the big guys say is quite reasonable, and I strive for reasons.

So basically, my whole journey is like this:

However, when they talked and talked about "Java Concurrent Programming Practical Combat", I immediately jumped up.

I have read this book, and this book is at my hand, and I can finally get in on it.

Take a closer look, they are talking about section 16.1.4 in the book:

There are no images left, and even the word "with synchronization" doesn't even understand what it means.

So I turned to this section and read it.

Since this section is not long and has no other background except for the basic knowledge of Happens-Before relationship, I will take a screenshot of this section for everyone to see:

How do you feel after reading it?

Didn't you even have the patience to finish reading it, a feeling in the mist?

To be honest, this is what I feel when I read it. I can understand every word, but I don't know what it means when I connect it together.

So, the feeling after reading is:

Find source code

But don't panic, the example cited in the article is FutureTask, which is one of the basics of concurrent programming. I am familiar with it.

So I decided to look at the source code, but I didn't find the innerSet or innerGet methods mentioned in the book:

Since I have the source code of JDK 8 here, and the release time of this book is February 2012:

Since it is a translation, the original book may be written earlier.

Compared with the release timeline of this JDK version, if it is the source code, it is also the source code before JDK 8:

Sure enough, a big guy told me that the source code in JDK 6 is written like this:

But I think the benefits of studying JDK 6 are not great. (Mainly I am too lazy to download)

So, I still found a little clue in the source code of JDK 8.

Finally figured out what is "with the help of synchronization".

And I have to admire the code of the old man Doug Lea, it really is: wonderful.

What exactly is "with synchronization"? Let me talk carefully.

Foundation paving

In order for the article to proceed smoothly, a basic knowledge must be laid, which is the Happens-Before relationship.

The formal proposal of the Happens-Before relationship is the jsr 133 specification:

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

If you don't know what jsr133 is, you can go to this link to have a look.

http://ifeve.com/jsr133/

There is a formal description of the Happens-Before relationship that everyone is familiar with, and all the original Chinese translations that you have seen are here:

Since this passage, especially the words behind the six small black dots, is too important, it is too far away, so I dare not easily translate it in a relaxed style before.

So I decided to stand on the shoulders of the big guys and put the definitions of this part in the three books "In-depth Understanding of the Java Virtual Machine (Third Edition)", "Java Concurrent Programming Practice", and "The Art of Java Concurrent Programming" Move it with the description, and compare it with everyone.

If you are familiar with the rules, you can skip this section.

.png)

let's go.

The first is "In-Depth Understanding of the Java Virtual Machine (Third Edition)":

  • Program Order Rule: In a thread, in accordance with the order of control flow, the operation written in the front occurs first in the operation written in the back. Note that here is the control flow sequence rather than the program code sequence, because branches, loops and other structures must be considered.
  • Monitor Lock Rule (Monitor Lock Rule): An unlock operation occurs first in the subsequent to the same lock

lock operation. What must be emphasized here is "the same lock", and "behind" refers to the time sequence.

  • Volatile Variable Rule (Volatile Variable Rule): The write operation of a volatile variable occurs first in the subsequent read operation of this variable. Here, "behind" also refers to the time sequence.
  • Thread Start Rule: The start() method of the Thread object precedes every action of this thread.
  • Thread Termination Rule: All operations in a thread occur first in the detection of the termination of this thread. We can check whether the Thread::join() method is over, the return value of Thread::isAlive(), etc. Whether the thread has terminated its execution.
  • Thread Interruption Rule: The call to the thread interrupt() method occurs first when the code of the interrupted thread detects the occurrence of an interrupt event. You can use the Thread:interrupted() method to detect whether there is an interruption.
  • Finalizer Rule: The completion of the initialization of an object (the end of the execution of the constructor) occurs first at the beginning of its finalize() method.
  • Transitivity: If operation A occurs before operation B, and operation B occurs before operation C, then it can be concluded that operation A occurs before operation C.

Followed by "Java Concurrent Programming Practical Combat":

  • Program sequence rule: If operation A is before operation B in the program, then operation A will be executed before operation B in the thread.
  • Monitor lock rule: The unlock operation on the monitor lock must be performed before the lock operation on the same monitor lock.
  • Rules for volatile variables: Write operations to volatile variables must be performed before reading operations on the variable.
  • Thread start rule: The call to Thread.Start on the thread must be executed before any operation is performed in the thread.
  • Thread termination rule: Any operation in a thread must be executed before other threads detect that the thread has ended, or successfully return from Thread.join, or return false when calling Thread.isAlive.
  • Interruption rules: When a thread calls interrupt on another thread, it must be executed before the interrupt call is detected by the interrupted thread (by throwing InterruptedException, or calling isInterrupted and interrupted).
  • Finalizer rule: The object's constructor must be executed before the object's finalizer is started.
  • Transitivity: If operation A is executed before operation B, and operation B is executed before operation C, then operation A must be executed before operation C.

"The Art of Concurrent Programming in Java", the author added a qualifier in this book "The happens-before rules closely related to programmers are as follows":

  • Program sequence rules: For each operation in a thread, happens-before any subsequent operations in the thread.
  • Monitor lock rule: to unlock a lock, happens-before and then lock the lock.
  • Volatile variable rules: write to a volatile domain, happens-before any subsequent reads of this volatile domain.
  • Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.

In other words: thread start rules, thread end rules, interrupt rules, and object end rules are actually insensible to development. In these rules, we have no room for trouble.

When you compare the descriptions of the same thing in these three books, you may be slightly impressed.

Essentially it is the same thing, but the description is slightly different.

In addition, I think I need to add a point that I think is very important, and that is a very important word action that appears in many places in the original paper:

So what is an action?

For this slightly vague definition, the fifth point at the beginning of the paper mentions the specific meaning:

In this section we define in more detail some of the informal concepts we have presented.

In this section, we will define some of the informal concepts we have proposed in more detail.

Among them, the seven concepts in the thesis are described in detail, namely:

  • Shared variables/Heap memory
  • Inter-thread Actions
  • Program Order
  • Intra-thread semantics
  • Synchronization Actions
  • Synchronization Order
  • Happens-Before and Synchronizes-With Edges

Among them, I personally understand that the action in happens-before mainly refers to the following three concepts:

Inter-thread actions, intra-thread actions, synchronization actions (Synchronization Actions).

Locking, unlocking, reading and writing of volatile variables, starting a thread, and detecting whether the thread ends are all synchronous actions.

The inter-thread actions are relative to the intra-thread actions. For example, a thread's reading and writing of local variables, that is, the reading and writing of variables allocated on the stack, is not perceptible to other threads. This is an intra-thread action. Inter-thread actions such as reading and writing of global variables, that is, variables allocated in the heap, are perceivable by other threads.

In addition, you can see where I drew the underline in Inter-thread Actions, the description is actually the same as the synchronous action. I understand that, in fact, most of the actions between threads are synchronous actions.

So you go to read a book called "In-Depth Understanding of Java Virtual Machine HotSpot". The description of happens-before in this book is slightly different. The restriction added at the beginning is "all synchronous actions...":

1) The code sequence of all synchronization actions (locking, unlocking, reading and writing of volatile variables, thread start, thread completion) is consistent with the execution sequence, and the code sequence of synchronization actions is also called the synchronization sequence.

1.1) For the same monitor in a synchronized action, unlocking occurs before locking.

1.2) The same volatile variable write operation occurs before the read operation.

1.3) The thread start operation is the first operation of the thread, and no operation before it can occur.

1.4) When the T2 thread finds that the T1 thread has completed or is connected to T1, the last operation of T1 must precede T2
All operations.

1.5) If thread T1 interrupts thread T2, then the T1 interruption point must precede any thread that determines that T2 is interrupted.
operating.

The operation of writing the default value to the variable must precede the first operation of the thread; the object initialization must be completed first
It is the first operation of the finalize() method.

2) If a occurs before b and b occurs before c, then it can be determined that a occurs before c.

3) The write operation of volatile precedes the read operation of volatile.

Originally, I also wanted to cite the description of happens-before in "Java Programming Thought".

As a result, I read the part about concurrency in the book, and it turned out:

No, yes, write!

Well, I think it is possible that this book of God was written before the release of jsr133 in 2004?

As a result, its English version was released in 2006, that is, the author deliberately did not write, he only mentioned "Java Concurreny in Practice" in chapter 21.11.1:

And "Java Concurreny in Practice" is the "Java Concurrency Programming Practice" we mentioned earlier.

As a book that enjoys such a high reputation in the Java world, it is a bit regretful that it does not mention happens-before.

But on second thoughts, although this book has a high status in the arena, but the positioning is actually entry-level, and it is normal to not mention this piece of knowledge.

In addition, an interesting place is this:

.png)

In "In-Depth Understanding of the Java Virtual Machine (Third Edition)", the Monitor is translated as "Management", and the other two are translated as "Monitor".

So what exactly is "Guan Cheng"?

Harm, it turned out to be the same thing.

Synchronized in Java is an implementation of monitor.

FutureTask in JDK 8

So much has been laid in the front. You probably haven't forgotten what I mainly want to share in this article?

That is the application of "synchronization" in FutureTask.

This is a screenshot of the FutureTask source code in JDK 8, focusing on the two parts I framed.

  • The state is modified by volatile.
  • The comment that follows the outcome variable.

Pay attention to this note:

non-volatile, protected by state reads/writes

You think, what is encapsulated in the outcome is the return of a FutureTask. This return may be a normal return or an exception in the task.

To give one of the simplest and most common application scenario: the main line submits the task to the thread pool through the submit method, and the return value is FutureTask:

What will you do next?

Do you call the get method of FutureTask in the main thread to get the return value of this task?

The current situation is: the thread in the thread pool writes to the outcome, and the main thread calls the get method to read the outcome?

In this scenario, should we add a volatile to the outcome to ensure visibility in our routine operations?

So why is volatile not added here?

You first smack yourself.

Next, everything to be described is developed around this topic.

Come, walk up.

First of all, looking at the whole situation, there are only two places for the write operation of the outcome variable:

set and setException, and the logic and principles of these two places are actually the same. So I only analyze the set method.

Next, look at the read operation of the outcome variable. There is only this place, which is the get method:

What needs to be explained is that the java.util.concurrent.FutureTask#get(long, java.util.concurrent.TimeUnit) method and the get method have the same principle, so there is no need to interpret too much.

So we focused our attention on these three methods:

Didn't the get method call the report method? Let's merge these two methods:

Isn't something wrong here?

Next, we actually only care about when the outcome will return. The others are interference items for me, so we turn the above get into pseudo code:

When s is NORMAL, return the outcome. Isn't this pseudo-code okay?

Next, let's look at the set method again:

The meaning of the second line is to use CAS operation to change the state from NEW to COMPLETING state. After CAS is successful, enter the if code segment.

Then after the third line of code, that is, outcome=v , the state is changed to NORMAL.

In fact, you see, from NEW to NORMAL, the state of COMPLETING in the middle is actually fleeting.

Even, it seems useless?

So for the smooth progress of the reasoning, I decided to use the contradiction method. Assuming that we don't need this COMPLETING state, then our set method becomes like this:

After simplification, this is the pseudocode of the final set:

So we put the pseudo code of get/set together:

At this point, finally all the preparations are completed.

Welcome everyone to the decryption session.

First, if the place marked ④, the value read is NORMAL, then the place marked ③ must have been executed.

why?

Because s is modified by volatile, according to the happens-before relationship:

Rules for volatile variables: Write operations to volatile variables must be performed before reading operations on the variable.

Therefore, we can conclude that the code labeled ③ is executed before the code labeled ④.

And according to the rules of procedure order, namely:

In a thread, in accordance with the control flow sequence, the operation written in the front occurs before the operation written in the back. Note that here is the control flow sequence rather than the program code sequence, because branches, loops and other structures must be considered.

It can be concluded that ② happens-before ③ happens-before ④ happens-before ⑤

According to transitive rules, namely:

If operation A occurs before operation B, and operation B occurs before operation C, then it can be concluded that operation A occurs before operation C.

It can be concluded that ② happens-before ⑤.

And ② is the writing to the outcome variable, and ⑤ is the reading of the outcome variable.

Although the variable is written and read without volatile, it completes the synchronization operation through the s variable modified by volatile, with the help of the happens-before relationship of the s variable.

That is: writing, before reading.

This is "with synchronization."

Have you got a little taste?

Don't worry, my disproval method, haven't talked about the COMPLETING state yet, let's continue the analysis.

Looking back at the pseudo code of the set method, I haven't mentioned the place marked ①.

Although the place labeled ① and the place labeled ③ are operations on volatile variables, they are not thread-safe. Can we reach an agreement on this point?

Therefore, we have to use CAS to ensure thread safety in this place.

So the program becomes like this:

In this way, the problem of thread safety is solved. But other problems also followed.

The first problem is that the meaning of the program has changed:

From "outcome assignment is completed, s becomes NORMAL", it becomes "s becomes NORMAL before assignment starts".

However, this issue is not within the scope of my discussion in this article, and this issue will be resolved in the end, so let's look at another issue, which is what I want to discuss.

What's the problem?

That is, Outcome's "Synchronization" strategy failed.

Because if we solve the problem of thread safety in this way, and disassemble the CAS operation, the program will look a bit like this:

According to the happens-before relationship, we can only infer:

② happens-before ④ happens-before ⑤, has nothing to do with ③.

Therefore, we can't get ③ happens-before ⑤, so we can't use synchronization.

At this time, what should we do if we run into it?

It's very simple, just add volatile to the outcome, where so many weird reasoning is needed.

But Doug Lea is Doug Lea after all. Adding volatile is too low. The old man is ready to "use synchronization".

We analyzed earlier that this can be used for synchronization, but thread safety cannot be guaranteed:

protected void set(V v) {
    if (s==NEW) {
        outcome = v;
        s=NORMAL;
    }
}

So, can we make it like this:

protected void set(V v) {
    if (s==NEW) {
        s=COMPLETING;
        outcome = v;
        s=NORMAL;
    }
}

COMPLETING is also a write to the s variable, so that the outcome can be "synchronized" again.

Use CAS to optimize it like this:

protected void set(V v) {
    if (compareAndSet(s, NEW, COMPLETING)){
        outcome = v;
        s=NORMAL;
    }
}

By introducing a fleeting COMPLETING state, the outcome variable can be made without volatile, and the happens-before relationship can be established, so that the purpose of "synchronization" can be achieved.

The COMPLETING state, which seems to be ugly and dispensable, turned out to be a thoughtful product based on code optimization.

I have to say, the old man this code:

It's really "Sao", can't learn, can't learn.

In addition, I also wrote an article about FutureTask before, describing another bug:

The bug written by Doug Lea in the JUC package was discovered by netizens again.

Mentioned in this article:

The old man said that he "written this way on purpose". Does this also contain the background of "synchronization"?

I don't know, but I seem to have a sense of "fantasy linkage".

Well, this article will be shared here.

Congratulations, you have learned another knowledge point that you will basically not use in your life.

Goodbye.

One last word

If you are too talented or knowledgeable, there will inevitably be mistakes. If you find something wrong, you can raise it in the message area and I will modify it.

Thank you for reading, I insist on originality, very welcome and thank you for your attention.


why技术
2.2k 声望6.8k 粉丝