Original technical article, copyright belongs to the author, if reproduced, please indicate the source of public number, to be determined, the original public number has been frozen for a long time
preface
The last article introduced the principle of multi-threading and its significance. At the end of the article, we raised the issue of thread safety, which is also a word we often hear in high concurrency. This article mainly introduces how to quickly use the lock mechanism provided by JDK in the single instance service, and understand its principle. The rest of AQS and Condition are left to explain later. The full text is pure hand typing, the code is pure hand writing, the picture is pure hand drawing, I hope to bring you some harvest, if there is any wrong place, welcome to correct.
Think, what scenarios are thread unsafe
We often emphasize thread safety when developing high-concurrency applications. What kind of scenarios are unsafe?
Shangdao (An) Gu (Li), the example of moving goods was mentioned in the last chapter. I wonder if you have tried the last case.
In the case mentioned in the last article,
At present, there is only one person to move (A) a pile of goods in the warehouse, tentatively 1,000,000 pieces. If A can only move one piece at a time, a total of 1,000,000 times will be needed. If one person can be added to move with A, assuming that the two people take the same time to move, then they can move 500,000 times each.
As a result of the previous section,
Transfer completed, Employee: B, transfer: [272708] times Transfer completed, Employee: A, transfer: [727292] times
Reflections on the previous chapter
Synchronized and AtomicInteger are used in the example to ensure that the total amount of goods remaining is handled by multiple people. The value is also correct. AtomicInteger can be changed into Integer or synchronized can be removed. The total amount of the two people’s final handling will be inconsistent with the total amount of the original goods.
Let’s remove synchronized and replace AtomicInteger with int
# move goodsStatic class Carry extends Thread{/** Carry */ private String peopleName; /** private int carryNum; public Carry(String peopleName) { this.peopleName = peopleName; } @Override public voidrun() {
while(! isInterrupted()) {if (cargoNum > 0) {
cargoNum--;
carryNum++;
} else {
System.out.println("Handling completed, employee:" + peopleName + ", carry: [" + carryNum + "]"); interrupt(); }}}}Copy the code
Open move
Static int cargoNum = 1000000; static int cargoNum = 1000000; public static void main(String[] args) { Carry carry1 = new Carry("甲");
Carry carry2 = new Carry("乙");
carry1.start();
carry2.start();
}
Copy the code
The results of
Transfer completed, Employee: B, transfer: [983347] transfer completed, Employee: A, transfer: [995228]Copy the code
We found that there were only 1,000,000 goods in total. To be reasonable, they should have moved 1,000,000 times in total in the end, but they both moved nearly 1,000,000 times. Isn’t it repetitive labor? Yes, this is caused by thread insecurity in multithreading.
Let’s think about why,
A, b two people carrying, at the same time when a move to get the total remaining quantity of goods, and then 1, own workload + 1, and then move out the goods, the same is true of b, seems as if no problem, but imagine if a, b move at the same time, to get the remaining amount at the same time, such as 1000, and at the same time – 1, so two people think the rest of the 999, This creates a surplus of labor.
If we can guarantee that when a updates the remaining total, B will not update, and vice versa, when B updates, A will not update, then ultimately this problem will not occur.
So you’re going to use a lock, add a lock, lock it while I’m using it, you wait, I’m done, open the lock, you use it, so that you can keep your data safe.
Java Concurrency In Practice defines thread safety as the following: When multiple threads are accessing an object, there is no need for additional synchronization or any other coordination on the caller’s side without taking into account the scheduling and alternate execution of those threads In the runtime environment. The object is thread-safe if the correct result is obtained by calling it.
Built-in lock
synchronized
Synchronized is a built-in mutex provided by JDK, which is roughly divided into class lock and object lock. As the name implies, class lock is the object (class) of the locking class file, and object lock is the instance object of the locking class.
The following is a brief understanding of how this works, which will be further dissected in subsequent bytecode and JMM chapters.
Synchronized implementations rely on Monitor, and an object instance is associated with a Monitor, referring to the JVM specification
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref
The relationship information is mainly contained in the object header, which is divided into two parts: Mark Word (marking information) and Klass Pointer (pointing to class metadata). (More on object analysis in a future bytecode chapter)
Where LockWord in Mark Word points to a Monitor, which contains information as shown in the figure above.
Owner
Is null when initialized and identifies the thread when held by a threadEntryQ
, associated with a mutex (Semaphore) that blocks other threads that want to hold MonitorRCThis
, marks the number of threads that are locked or waitingNest
, and records the reentrant timesHashCode
HashCode copied from the object headerCondidate
, the successor thread that needs to be woken up, if none, 0
To summarize, if a thread wants to lock an object, it needs to hold the Monitor associated with the object, and other threads (Thread2, Thread3) block. The JVM is implemented through CAS, which will be explained later in this article.
Kind of lock
Class locks lock class objects, class files that are objects in the JVM
For example, the following
First we create a print class
public class Print {
public void print() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " print.....");
# 1 s sleep
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " print end....."); }}Copy the code
Thread class
static class PrintThread extends Thread {
@Override
public void run() {
Print print = new Print();
# locks in classsynchronized (Print.class) { try { print.print(); } catch (InterruptedException e) { e.printStackTrace(); }}}}Copy the code
perform
public static void main(String[] args) {
PrintThread printThread1 = new PrintThread();
PrintThread printThread2 = new PrintThread();
printThread1.start();
printThread2.start();
}
Copy the code
The results of
Thread-0 print.....
Thread-0 print end.....
Thread-1 print.....
Thread-1 print end.....
Copy the code
We can see that although both threads are calling each Print instance, the second thread is obviously blocked, and if we remove synchronized (print.class), the result is as follows
Thread-0 print.....
Thread-1 print.....
Thread-0 print end.....
Thread-1 print end.....
Copy the code
The two threads are executing in parallel, and adding synchronized to a method that calls the static modifier also locks the class, which we won’t demonstrate here.
Object lock
Object locks instance objects
Let’s modify the thread class, and print instance is passed in via construct
static class PrintThread extends Thread {
private Print print;
PrintThread(Print print) {
this.print = print;
}
@Override
public void run() {
synchronized (print) { try { print.print(); } catch (InterruptedException e) { e.printStackTrace(); }}}}Copy the code
Execute class, again two threads passing in different class instances
public static void main(String[] args) {
PrintThread printThread1 = new PrintThread(new Print());
PrintThread printThread2 = new PrintThread(new Print());
printThread1.start();
printThread2.start();
}
Copy the code
The results of
Thread-0 print.....
Thread-1 print.....
Thread-1 print end.....
Thread-0 print end.....
Copy the code
As you can see, the different threads do not affect each other
So, what if we pass in the same instance?
Perform class
public static void main(String[] args) {
Print print = new Print();
PrintThread printThread1 = new PrintThread(print);
PrintThread printThread2 = new PrintThread(print);
printThread1.start();
printThread2.start();
}
Copy the code
The results of
Thread-0 print.....
Thread-0 print end.....
Thread-1 print.....
Thread-1 print end.....
Copy the code
As you can see, for the same print instance, the two threads execute sequentially. The first thread holds the lock, and the second thread blocks until the first thread completes.
volatile
Volatile is one of the lighter locks provided by the JDK than synchronized, so to help understand that, let’s start with two appetizers.
Data sharing between threads
Now we have a variable a that is 1, and thread 1 and thread 2 both need to update the value of A, so what is the value of A after the simultaneous operation?
Obviously, a could end up being 2, or it could end up being 3, so if thread 1 gets a, it gets a 1, it gets 2, but it hasn’t been synchronized back into main memory, so thread 2 gets a a and it’s still 1, it’s set to 3, isn’t that unsafe? In addition to thread safety with synchronized as described earlier, we can also use volatile.
If a field is declared volatile, the Java thread memory model ensures that all threads see the variable’s value as consistent. However, only atomic operations are supported to be visible, such as
# two threads do ++ on I at the same time
public class VolatileTest {
static volatile int i = 0;
static class AddThread extends Thread {
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
public static void main(String[] args) {
AddThread add1 = new AddThread();
AddThread add2 = new AddThread();
add1.start();
add2.start();
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("I ends up being"+ i); }}Copy the code
The results of
I ends up being 177,762Copy the code
Ultimately, I should be 200,000, so volatile does not guarantee thread-safety for nonatomic operations.
Add a few more features of volatile:
- Atomicity, which is thread-safe for atomic operations such as assignment, true&false, etc. A++ is not considered atomic operations
- Visibility. As mentioned above, variables that are volatile can be seen by other threads immediately after the atomic operation value changes
- To prevent reordering, as follows,
The JVM runs the compiler and processor to reorder instructions for performance optimization.
public class VolatileDemo {
private int i = 0;
private boolean flag = false;
public void write() {
# 1 operation
i = 1;
# 2 operation
flag = true;
System.out.println("Update completed...");
}
public void read() {
# 3 operation
if (flag) {
# 4 operation
System.out.println("I is updated to I ->"+ i); }}}Copy the code
The normal write operation is 1->2 and the read operation is 3->4. However, if an instruction rearrangement occurs, the write operation may become 2->1. Therefore, the read operation may not be complete and the value is read, so we can use volatile to modify flag
private volatile boolean flag = false;
Copy the code
This prevents the compiler or processor from reordering instructions based on memory barriers
The memory barrier | instructions |
---|---|
StoreStore | Disallow normal writes above and volatile write reordering below |
StoreLoad | Prevents volatile writes above from potentially volatile read/write reordering below |
LoadLoad | Disallow all normal reads below and volatile read reordering above |
LoadStore | Disallow all normal writes below and volatile read reordering above |
For variables that are volatile
operation | instructions |
---|---|
Volatile write | Insert a StoreStore barrier before. |
Volatile write | Then insert a StoreLoad barrier. |
Volatile read | Insert a LoadLoad barrier in front. |
Volatile read | After inserting a LoadStore barrier. |
To sum up, we can draw the following anti-rearrangement rules: NO stands for prohibition of rearrangement
ThreadLocal
ThreadLocal can provide thread-local variables that can be shared within the same thread, and separate threads from each other.
A simple example
Create thread copy, default to 1
static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
# get the value
int n = threadLocal.get();
# + +
n++;
System.out.println(Thread.currentThread().getId() + ":"+ n); }).start(); }}Copy the code
The results of
13: 2
15: 2
14: 2
12: 2
16: 2
Copy the code
We can see that each thread gets a value from a threadLocal that is independent of each other
Let’s tear the source code by hand
The set () method
/**
* Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if(map ! = null) map.set(this, value);else
createMap(t, value);
}
Copy the code
Look at the comment and set the current thread copy of this thread-local variable to the specified value. It’s a bit of a mouthful, so we can see that we’re getting the current thread and we’re calling getMap(t), so let’s look at this method
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Copy the code
Get the current Thread t.t hreadLocals, returns the ThreadLocal. ThreadLocalMap, here we first make clear the first layer, the Thread will contain a ThreadLocal. ThreadLocalMap
Then I see the ThreadLocal. ThreadLocalMap
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" inthe code that follows. */ static class Entry extends WeakReference<ThreadLocal<? >> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<? > k, Object v) { super(k); value = v; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; / /... I'll do a little bit later, but I'll focus on the topCopy the code
A ThreadLocalMap defines an Entry, a key is a WeakReference to a ThreadLocal, and a value is the value we save.
We usually use a strong reference, such as Object o = new Object(), o and Object are strong references, if the reference, GC, o will not be recycled, and weak reference, in the next GC, the Object will be recycled.
Yes, back to the set (), we obtained the relationship between general Thread – > ThreadLocal. ThreadLocalMap – > ThreadLocal, well, seems to be a bit around, it doesn’t matter, later have eggs!
Let’s look at the get() method
/**
* Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local
*/
public T get() {
Get the current thread
Thread t = Thread.currentThread();
Get the ThreadLocalMap of the current thread
ThreadLocalMap map = getMap(t);
if(map ! = null) {Get the value of the current thread for the current ThreadLocal
ThreadLocalMap.Entry e = map.getEntry(this);
if(e ! = null) { @SuppressWarnings("unchecked")
T result = (T)e.value;
returnresult; }}return setInitialValue();
}
Copy the code
If you understand the relationship between Thread, ThreadLocal, and ThreadLocalMap, get() should be easy to understand
And then getEntry(this)
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null ifno such */ private Entry getEntry(ThreadLocal<? > key) {Get the corresponding value via ThreadLocal, hash algorithm
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
# is the key
if(e ! = null && e.get() == key)return e;
else
return getEntryAfterMiss(key, i, e);
}
Copy the code
If it doesn’t, it’ll loop through getEntryAfterMiss(key, I, e)
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for keyHash code * @param e the entry at table[I] * @return the entry associated with key, or null if no such */ # Until found, where markdown displays the bug, o_O private Entry getEntryAfterMiss(ThreadLocal
key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e ! = null) { ThreadLocal
k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }Copy the code
ThreadLocal ThreadLocal ThreadLocal ThreadLocal ThreadLocal ThreadLocal ThreadLocal ThreadLocal If you still don’t understand, it doesn’t matter, look at the Easter eggs:
The solid line represents strong references and the dotted line represents weak references. If you understand this diagram, you should be comfortable with ThreadLocal.
However, we often find that using ThreadLocal causes memory leaks, especially when using thread pools. As can be seen from the figure, Entry is a strong reference. If the thread is not finished, this piece of memory will always exist. If the thread pool is used, there will be a large amount of unnecessary data on the same thread.
The explicit lock
lock
Now that we have learned about built-in locks, we will look at a lighter type of lock, explicit locks. This section will mainly explain how to use them (4000 words are written here, so as to avoid excessive use of o_O). The core principle of AQS and Condition will be explained in detail later.
ReentrantLock
Use cases
As usual, let’s start with a non-thread-safe example
# counter
public class Counter {
private Integer n = 0;
public void Add() {
n++;
}
public Integer getN() {
returnn; }}Copy the code
Start two threads, each calling Add () 100000 times
static class AddThead extends Thread {
private Counter counter;
AddThead(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
counter.Add();
}
}
}
public static void main(String[] args) {
Counter counter = new Counter();
for (int i = 0; i < 2; i++) {
new AddThead(counter).start();
}
while(Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("Result:"+ counter.getN()); } results: 194758Copy the code
The use of the Lock
public class Counter {
private Lock lock = new ReentrantLock();
private Integer n = 0;
public void Add() {
try {
lock.lock();
n++;
} finally {
lock.unlock();
}
}
public Integer getN() {
returnn; }} Result: 200000Copy the code
Writing paradigm
try {
lock.lock();
# other
} finally {
lock.unlock();
}
Copy the code
Release the lock when you’re done
reentrant
public static void main(String[] args) {
Lock lock = new ReentrantLock();
# 5 times the lock
for (int i = 0; i < 5; i++) {
lock.lock();
}
# Unlock 5 times
for (int i = 0; i < 5; i++) {
lock.unlock();
}
System.out.println("end ....");
}
Copy the code
Acquiring a lock
We start two threads, one holding the lock and the other tryLock()
Attempt to acquire a lock
static Lock lock = new ReentrantLock();
# hold locks
static class Holder extends Thread {
@Override
public void run() {
try {
lock.lock();
# sleepThread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); }}}# try to lock
static class TryLock implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
# try to acquire lock, return true
if (lock.tryLock()) {
try{
return true; }finally { lock.unlock(); }}return false;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Holder holder = new Holder();
holder.start();
FutureTask<Boolean> futureTask = new FutureTask<>(new TryLock());
Thread tryThread = new Thread(futureTask);
tryThread.start();
Boolean result = futureTask.get();
System.out.println("result :" + result);
}
result :false
Copy the code
Get lock timeout
Add get wait time, lock.tryLock(6000, timeunit.milliseconds), and then we’ll do it again
The results of
result :true
Copy the code
Fair locks and unfair locks
The so-called fair lock refers to the first come first lock, while the non-fair lock allows preemption in the case of thread switching. Whoever grabs the lock owns the lock.
ReentrantReadWriteLock
Whereas the previously mentioned ReetrantLock runs only one thread at a time, ReentrantReadWriteLock allows multiple threads to execute simultaneously to improve throughput in read and write scenarios.
demo
public class ReentrantReadWriteLockDemo {
ReadWriteLock lock = new ReentrantReadWriteLock();
public void write() {
try {
lock.writeLock().lock();
System.out.println(Thread.currentThread().getId() + "Hold write lock");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId() + "Ready to release write lock");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public void read() {
try {
lock.readLock().lock();
System.out.println(Thread.currentThread().getId() + "Hold read lock");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId() + "Ready to release read lock"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); }}}Copy the code
read
public static void main(String[] args) { ReentrantReadWriteLockDemo lockDemo = new ReentrantReadWriteLockDemo(); new Thread(()->{ lockDemo.read(); }).start(); new Thread(()->{ lockDemo.read(); }).start(); } = = = = = = = = = = = = = = = = = = = = = = = the = = = = = = = = = = = = = = = = = = = = = = = = 11 holding read lock 12 read lock to release read lock 12 ready to release lockCopy the code
You can see that two threads can execute simultaneously
Read and write
public static void main(String[] args) { ReentrantReadWriteLockDemo lockDemo = new ReentrantReadWriteLockDemo(); new Thread(()->{ lockDemo.read(); }).start(); new Thread(()->{ lockDemo.write(); }).start(); } = = = = = = = = = = = = = = = = = = = = = = = the = = = = = = = = = = = = = = = = = = = = = = = = 11 read lock 11 ready to release lock 12 holds write locks 12 ready to release the write lockCopy the code
As you can see, the two are mutually exclusive and write and read alike
Write about
public static void main(String[] args) { ReentrantReadWriteLockDemo lockDemo = new ReentrantReadWriteLockDemo(); new Thread(()->{ lockDemo.write(); }).start(); new Thread(()->{ lockDemo.write(); }).start(); } = = = = = = = = = = = = = = = = = = = = = = = the = = = = = = = = = = = = = = = = = = = = = = = = 11 write lock 11 ready to release the write lock 12 holds write locks 12 ready to release the lockCopy the code
Also mutually exclusive, so we can conclude that writes are mutually exclusive, as long as they are written, they are mutually exclusive:
operation | If the mutex |
---|---|
read | no |
Read and write | is |
Write about | is |
Write to read | is |
Jdk1.8 provides StampedLock (an improved read/write lock), which will be explored in more detail in the following sections.
LockSupport
In addition to using wait(), notify()/notifyall() to block and wake up threads, we can use the LockSupport tool.
LockSupport blocks and wakes up threads, while wait and notify are methods that lock objects.
Take a look at LockSupport source code
park()
public static void park() {
UNSAFE.park(false, 0L);
}
Block duration can be set
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
Copy the code
upPark()
Wake up a thread
public static void unpark(Thread thread) {
if(thread ! = null) UNSAFE.unpark(thread); }Copy the code
Park and unpark call native methods. In addition to locking the thread, it is important to note that Interrupt exceptions cannot be caught when blocking. You need to determine the state manually.
CAS
CAS (compare and swap), look at the name should be easy to understand, compare and replace. This is the technology that CPU processors can provide today, and the locks we used earlier, whether on locking objects or blocking threads, have a relatively high performance cost. CAS, on the other hand, does not require A lock. It is implemented by an address V, the original value A, the new value B, for example, A =0, we want to change to 1, we first need to know the memory address of A, and then compare whether it is 0, if it is changed to 1. If not, return the current value and then spin.
CAS is also used in object locking and AQS. The principle is the same as above. AQS will be explained later. In JAVA, there are some classes for atomic operations implemented through CAS, such as:
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
- .
In retrospect, we used synchronized to lock Integer for cargo calculation; now we use AtomicInteger for cargo calculation
static AtomicInteger n = new AtomicInteger(0); d# 10,000 computations per thread
static class AddThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
n.incrementAndGet();
}
}
}
public static void main(String[] args) {
Start five threads
for (int i = 0; i < 5; i++) {
new AddThread().start();
}
while(Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("n :"+ n.get()); } Result n :50000Copy the code
As you can see, we didn’t use synchronized, volatile, or lock, but the end result was correct. Other AtomicXXX use can be practiced by yourself.
conclusion
This chapter mainly introduces the Locking mechanism in JAVA, including synchronized, volatile, lock, CAS, etc., some of which are in-depth, and some of the main use (this time write a little more, try to break down the details, in-depth point), I hope to bring you some help. If there are written questions or need to discuss the comments can be corrected.