There are many kinds of lock classification in Java multithreading, one of the main classification methods is optimistic and pessimistic division. This article mainly introduces how to write an optimistic lock code by hand. However, in order to ensure the integrity of the article, it will be introduced from the basics.
First, the concept of optimistic locking
It is said to write the concept of optimistic lock, but usually the concept of optimistic lock and pessimistic lock should be written together. It makes more sense in comparison.
1. Pessimistic lock concept
Pessimistic locking: Always assume the worst, every time you go to get data you think someone else will change it, so every time you get data you lock it, so that someone else tries to get the data and it blocks until it gets the lock.
Synchronized, for example, is a pessimistic lock; when a method uses the synchronized modifier, other threads need to wait until another thread releases the method.
This pessimistic locking mechanism is also used in databases. Such as row locks, table locks, read locks, write locks, etc., are locked before the operation. Other threads cannot synchronize operations until it is released.
2. Optimistic locking concept
Optimistic locking: Always assume the best, every time you go to get data, you assume that no one will change it, so you don’t lock it, and only make a judgment call when you update itIn the meantime
Did someone else update the data?
Note that “during this period” means the period between when the data is available and when the data is updated. Because there is no lock, other threads may change. Another point is that the optimism lock is actually unlocked.
Now that you understand the concept, consider an example: classes under the Atomic package in Java use optimistic locking. Let’s pick one and see how it’s implemented officially, and then we can implement it ourselves.
3, optimistic lock implementation cases
There are three main features to consider in Java concurrency: atomicity, visibility, and orderliness. The purpose of AtomicInteger is to ensure atomicity. Here’s the demo:
public class Test {
// a variable a
private static volatile int a = 0;
public static void main(String[] args) {
Test test = new Test();
Thread[] threads = new Thread[5];
// Define 5 threads, add 10 to each thread
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
System.out.println(a++);
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
Copy the code
This example is very simple: we define a variable a, which starts at 0, and then use 5 threads to increment it by 10 each. It makes sense that the 5 threads increment by 50.
For a++ operations, there are actually three steps.
** (1) Read the value of a from main memory **
** (2) Add 1 to a **
** (3) refresh a to main memory **
Thread 1 increments a by 1, but thread 2 reads it before it can reload to main memory. At this point, thread 2 must read the old value that has not been brushed into memory. That was the mistake. The solution is to use AtomicInteger:
public class Test3 {
// Define a with AtomicInteger
static AtomicInteger a = new AtomicInteger();
public static void main(String[] args) {
Test3 test = new Test3();
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
// Use getAndIncrement to increment
System.out.println(a.incrementAndGet());
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
Copy the code
Now we define a with AtomicInteger and increment with incrementAndGet, and the result is always 50. Let’s break it down:
4. Optimistic lock case analysis
To find out, we need to start with AtomicInteger’s incrementAndGet method. Because this method implements a lock-like function. The jdk1.8 version is used here, there are some differences between different versions.
/ * *
* Atomically increments by one the current value.
* @return the updated value
* /
public final int incrementAndGet(a) {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
Copy the code
The broadening operation primarily uses the unsafe getAndAddInt method. Because I am not covering AtomicInteger specifically, I will not analyze the source code in detail.
-
Unsafe: Unsafe is a class under the Sun. misc package that gives the Java language the ability to manipulate memory Spaces like Pointers in THE C language. So we’re directly manipulating memory and adding one.
-
Unsafe. GetAndAddInt: The Unsafe.compareAndSwapInt method is called internally. This mechanism is called the CAS mechanism,
**CAS is a comparison-and-replace technique commonly used to implement concurrent algorithms. The CAS operation contains three operands -- memory location, expected original value, and new value. When the CAS operation is performed, the value of the memory location is compared to the expected value. If it matches, the processor automatically updates the value of the memory location to the new value; otherwise, the processor does nothing. **
Let’s use an example to explain it and I’m sure you’ll get a little clearer.
Meaning, your father wants you to marry Zhang SAN, wait until the real wedding day, if your father expected bride (Zhang SAN) and you really get the bride, you do a wedding, otherwise do not do a wedding.
But there is a common problem with this CAS mechanism. That’s the ABA problem, you put $100 on the table, you come back with $100, but while you were gone, someone else took $100 and gave it back. That’s the ABA problem.
Would you let someone take your $1 million and give it back without you knowing it?
The idea behind the ABA problem is to add a version number to the data.
5. Optimistic thinking
Optimistic locking can be implemented by CAS + versioning.
-
CAS mechanism: When multiple threads attempt to update the same variable using CAS at the same time, only one thread can update the value of the variable, and all the other threads fail. CAS effectively says “I think position V should contain the value A; If this value is included, place B in this position; Otherwise, do not change the location, just tell me the current value of the location “.
-
Versioning mechanism: The CAS mechanism ensures that data is not changed to another synchronization mechanism when updated, and versioning mechanism ensures that the synchronization mechanism is not changed (meaning the ABA problem above).
Based on this idea we can implement an optimistic lock. So let’s write some code. This code has been tested on my own computer.
Implement an optimistic lock
Step 1: Define the data we want to manipulate
public class Data {
// Data version number
static int version = 1;
// Real data
static String data = "Architect Technology Stack for Java";
public static int getVersion(a){
return version;
}
public static void updateVersion(a){
version = version + 1;
}
}
Copy the code
Step 2: Define an optimistic lock
public class OptimThread extends Thread {
public int version;
public String data;
Constructor and getter and setter methods
public void run(a) {
1. Read data
String text = Data.data;
println("Thread"+ getName() + ", the obtained data version number is:" + Data.getVersion());
println("Thread"+ getName() + ", the expected data version number is:" + getVersion());
System.out.println("Thread"+ getName()+"Read data completed =========data =" + text);
// 2. Write data: The expected version number is consistent with the data version number, so update
if(Data.getVersion() == getVersion()){
println("Thread" + getName() + ", version number:" + version + ", manipulating data");
synchronized(OptimThread.class){
if(Data.getVersion() == this.version){
Data.data = this.data;
Data.updateVersion();
System.out.println("Thread" + getName() + "Write data completed =========data =" + this.data);
return ;
}
}
}else{
// 3. If the version number of the thread is incorrect, it needs to be read and executed again
println("Thread"+ getName() + ", the obtained data version number is:" + Data.getVersion());
println("Thread"+ getName() + ", the expected version number is: + getVersion());
System.err.println("Thread"+ getName() + ", needs to be re-executed. = = = = = = = = = = = = = =");
}
}
}
Copy the code
Step 3: Test
public class Test {
public static void main(String[] args) {
for (int i = 1; i <= 2; i++) {
new OptimThread(String.valueOf(i), 1."fdd").start();
}
}
}
Copy the code
Two threads are defined and then read and write
Step 4: Output the results
This result can be seen as long as there is no change when reading the data, but when updating the data, it is necessary to judge whether the current version number is consistent with the expected version number. If so, it is updated; if not, it indicates that the update failed.
OK, that’s where I’ll start today’s article. If the problem also please criticize correct.