I put together my previous articles to become Github, welcome everyone big guy star github.com/crisxuan/be…
Before we know the AtomicInteger, AtomicLong, AtomicBoolean atomic tools such as class, let’s continue to look at in Java. Util. Concurrent. The atomic bag of tools.
See AtomicInteger, AtomicLong, AtomicBoolean for more information
An Atomic XXX magical journey
The article about the understanding of AtomicReference JDK tool class is boring, which does not represent the decline of the quality of the article, because I want to work out a full set of bestJavaer analysis, it is bound to be dependent on the understanding of JDK tool class.
Remember: Technology is about the long haul.
Basic use of AtomicReference
Let’s talk about the platitude account problem again here, through the personal bank account problem, to gradually introduce the use of AtomicReference, let’s first look at the basic personal account class
public class BankCard {
private final String accountName;
private final int money;
The constructor initializes accountName and Money
public BankCard(String accountName,int money){
this.accountName = accountName;
this.money = money;
}
// Do not provide any set methods to modify personal accounts, only provide get methods
public String getAccountName(a) {
return accountName;
}
public int getMoney(a) {
return money;
}
// Override toString() to print BankCard
@Override
public String toString(a) {
return "BankCard{" +
"accountName='" + accountName + '\' ' +
", money='" + money + '\' ' +
'} '; }}Copy the code
The personal account class contains only two fields: accountName and money, which represent the accountName and the account amount. Once set, the accountName and the account amount cannot be changed.
Now let’s assume that multiple people are making separate contributions to this account, each making a certain amount of money. Ideally, each person is making a certain amount of money each time the account is made. Let’s verify this process.
public class BankCardTest {
private static volatile BankCard bankCard = new BankCard("cxuan".100);
public static void main(String[] args) {
for(int i = 0; i <10; i++){new Thread(() -> {
// Read the global reference first
final BankCard card = bankCard;
// Create a new account and deposit a certain amount of money
BankCard newCard = new BankCard(card.getAccountName(),card.getMoney() + 100);
System.out.println(newCard);
// Finally, assign a reference to the new account to the old account
bankCard = newCard;
try {
TimeUnit.MICROSECONDS.sleep(1000);
}catch(Exception e){ e.printStackTrace(); } }).start(); }}}Copy the code
In the above code, we first declare a global variable BankCard, which is volatile and is intended to be visible to other threads after its reference changes, and to print the change in the amount of the account after each payer has deposited a certain amount of money. We can look at the output.
As you can see, we expected to end up with 1100 yuan, but we ended up with 900 yuan. Where did that 200 yuan go? We can conclude that the above code is not a thread-safe operation.
What’s the problem?
Although volatile kept each account up to date, because of the combination operations that took and changed account references in the previous steps, each individual operation was atomic, but the combination was not atomic. So the final result will be biased.
We can use the following thread switch diagram to illustrate how this process changes.
As you can see, the final result is probably because after thread T1 gets the latest account change, thread t2 gets the latest account status, and then switch to T1, where T1 modifies the reference, thread t2 changes the reference, so the value of the account reference is changed twice.
So how do you ensure thread-safety between retrieving and modifying references?
The simplest and most crude way to do this is to use the synchronized keyword.
Use synchronized to ensure thread safety
Synchronized can ensure the security of shared data. The code is as follows
public class BankCardSyncTest {
private static volatile BankCard bankCard = new BankCard("cxuan".100);
public static void main(String[] args) {
for(int i = 0; i <10; i++){new Thread(() -> {
synchronized (BankCardSyncTest.class) {
// Read the global reference first
final BankCard card = bankCard;
// Create a new account and deposit a certain amount of money
BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
System.out.println(newCard);
// Finally, assign a reference to the new account to the old account
bankCard = newCard;
try {
TimeUnit.MICROSECONDS.sleep(1000);
} catch(Exception e) { e.printStackTrace(); } } }).start(); }}}Copy the code
Compared with BankCardSyncTest, BankCardSyncTest adds synchronized lock. After running BankCardSyncTest, we found that the correct result could be obtained.
Changing bankcardSynctest. class to the bankCard object also ensures thread-safety, because only the bankCard changes in this program, and no other data is shared.
If there is other shared data, we need to use BankcardSynctest.clas to ensure thread-safety.
. In addition, the Java. Util. Concurrent atomic package under the AtomicReference can also ensure thread safety.
Let’s take a look at AtomicReference and then use AtomicReference to rewrite the code above.
Understand the AtomicReference
Use AtomicReference for thread safety
Let’s rewrite the example above
public class BankCardARTest {
private static AtomicReference<BankCard> bankCardRef = new AtomicReference<>(new BankCard("cxuan".100));
public static void main(String[] args) {
for(int i = 0; i <10; i++){new Thread(() -> {
while (true) {// Use atomicReference.get
final BankCard card = bankCardRef.get();
BankCard newCard = new BankCard(card.getAccountName(), card.getMoney() + 100);
// Use CAS optimistic locks for non-blocking updates
if(bankCardRef.compareAndSet(card,newCard)){
System.out.println(newCard);
}
try {
TimeUnit.SECONDS.sleep(1);
} catch(Exception e) { e.printStackTrace(); } } }).start(); }}}Copy the code
In the sample code above, we use an AtomicReference to encapsulate the BankCard reference, then use the get() method to get the AtomicReference, then use the CAS optimistic lock for a non-blocking update, The updated standard is that if the value obtained with bankcardref.get () is equal to the memory value, it will add the amount of money in the bankcard account + 100. Let’s watch the output.
As you can see, some of the output is out of order, the reason for this is simple, it is possible that before the output results, the thread switch, and then print the value of the following thread, and then switch back to the output, but you can see that there is no same amount of bank cards.
AtomicReference source code parsing
With the above example in mind, let’s take a look at how to use AtomicReference
AtomicReference and AtomicInteger are very similar in that they both use the following three attributes internally
Unsafe is a class under the Sun.misc package, and AtomicReference relies on some native methods provided by Sun.misc.Unsafe to ensure atomicity of operations.
The Unsafe objectFieldOffset method obtains the offset of the memory address of a member attribute relative to the object’s memory address. This offset, valueOffset, simply finds the address of the variable in memory so that subsequent operations can be performed directly from the address.
The value is the actual value in the AtomicReference, and because it is volatile, it is actually the memory value.
The difference is that AtomicInteger encapsulates an integer, whereas AtomicReference corresponds to a plain object reference. That is, it ensures thread-safety when you modify object references.
get and set
Let’s start with the simplest get and set methods:
Get () : Gets the value of the current AtomicReference
Set () : Sets the current AtomicReference value
Get () atomically reads the data in the AtomicReference, and set() atomically sets the current value, because both get() and set() ultimately operate on the value variable, which is volatile. Therefore, get and set are equivalent to reading and setting memory. As shown in the figure below
LazySet method
Did you know that Volatile has memory barriers?
What is a memory barrier?
Memory barrier, also known as memory barrier, memory gate barrier, barrier instruction, etc., is a kind of synchronization barrier instruction. It is a synchronization point in the operation of random memory access by CPU or compiler, so that all read and write operations before this point can be executed before the operation after this point. It is also a technique for making the state of memory in a CPU processing unit visible to other processing units.
The CPU uses many optimizations, such as caching, instruction reordering, etc. The ultimate goal is performance. That is, when a program executes, it doesn’t matter if the instructions are reordered as long as the end result is the same. So instructions are not executed sequentially, but out of order, which causes a lot of problems, which also leads to memory barriers.
Semantically, all writes prior to the memory barrier are written to memory; Any read operation after the memory barrier can obtain the result of any write operation before the synchronous barrier. Therefore, for sensitive blocks, memory barriers can be inserted after write operations but before read operations.
The overhead of a memory barrier is very lightweight, but even a small barrier has a cost, and LazySet does just that by reading and writing variables as normal variables.
It could also be: Too lazy to put up barriers
GetAndSet method
Set atomically to the given value and return the old value. Its source code is as follows
It calls the getAndSetObject method in unsafe, the source shown below
As you can see, the getAndSet method involves two CPP implementation methods, getObjectVolatile and compareAndSwapObject, which are used to do… In the while loop, the value of the latest object reference is retrieved each time. If two objects are successfully swapped using CAS, the value of var5 is returned directly, which should be the memory value before the update.
CompareAndSet method
This is the key CAS method for AtomicReference. Unlike AtomicInteger, AtomicReference is the compareAndSwapObject that is called, AtomicInteger calls the compareAndSwapInt method. The implementation of these two methods is as follows
Path in the hotspot/SRC/share/vm/prims/unsafe. The CPP.
We have parsed the source code of AtomicInteger before, so let’s parse the source code of AtomicReference next.
Because the object exists in the heap, the method index_oop_from_field_offset_long should get the memory address of the object and then use the atomic_compare_exchange_OOP method to exchange the CAS of the object.
This code will first determine whether UseCompressedOops, also known as pointer compression, is used.
Here’s a brief explanation of pointer compression: The JVM was originally 32-bit, but with the rise of 64-bit JVMS, there is a problem. The memory footprint is larger, but the JVM should not exceed 32 GB of memory. To save space, after JDK 1.6, In 64-bit JVMS, pointer compression can be enabled to compress the size of our object Pointers to help us save memory. In JDK 8, this is enabled by default.
If pointer compression is not enabled, 64-bit JVMS use 8 bytes (64-bit) to store real memory addresses, which presents problems compared to the previous use of 4 bytes (32-bit) to store addresses:
- Increased GC overhead: 64-bit object references take up more heap space, leaving less space for other data, speeding up GC and making GC more frequent.
- Lower CPU cache hit ratio: With larger 64-bit object references, the CPU will have fewer OOP to cache, reducing CPU cache efficiency.
Because there were so many problems with storing 64-bit memory addresses, programmers invented pointer compression, which allowed us to use the previous four bytes to store pointer addresses while expanding memory storage.
As you can see, the underlying atomic_compare_exchange_OOP method also uses the Atomic: CMPXCHG method for CAS exchange, and then decode the old values back. If you understand this code must tell me, let me ask a wave)
WeakCompareAndSet method
WeakCompareAndSet: very seriously look at several times, found JDK1.8 this method and compareAndSet method exactly the same ah, pit me…
But is that really the case? Not really, the JDK source code is too extensive and profound to design a repetitive method, and you think the JDK team is not going to make this low-level team, but why?
The book “Java High Concurrency In Detail” provides an answer
conclusion
This article mainly introduces the emergence of AtomicReference background, the use of AtomicReference scene, and introduces the source code of AtomicReference, the key method of source code analysis. This AtomicReference article basically covers all the content of AtomicReference on the network. Unfortunately, CPP source code may not be analyzed in place, which requires sufficient KNOWLEDGE of C/C++ programming. If there are readers who have the latest research results, Please let me know in time.
In addition, add my becomecxuan on wechat to join the one question of the day group and one interview question of the day to share. For more content, please refer to my Github to become the bestJavaer. This article has been included, see the original link for details.
I have uploaded six PDFS by myself. After cXuan, the programmer, followed the official account on wechat, he replied to CXuan in the background and got all THE PDFS. These PDFS are as follows
Six PDF links