The rational use of threads can improve the processing performance of the program, in a real sense, achieve concurrent execution of tasks, and improve the program throughput, but it will also cause a series of thread safety issues, such as shared variable wait. With thread-safety, it is essentially managing access to data state, and that state is usually shared and mutable. Sharing means that the variable can be accessed by multiple threads at the same time; Mutable means that the value of a variable can change over its lifetime.
Whether an object line is thread-safe depends on whether it can be accessed by multiple threads at the same time and how the object is used in the program. If multiple threads access the same Shared object, without additional synchronization and invoke server-side code under the condition of without doing other coordination, the Shared object’s state is still correct (correctness means that the object is consistent with results and expected results), that means the object is thread-safe.
How to solve data security problems caused by thread parallelism in Java?
The essence of the data security problem is to solve the problem of concurrent access to shared data. This problem can be solved if thread parallelism can be turned into serial. In Java, the most heard and contacted is the concept of lock, such as pessimistic lock, optimistic lock and so on, it is a kind of synchronization means to deal with concurrency, if you want to solve the problem of data security, then the lock must realize the feature of mutual exclusion, Java provides the lock method is synchronized keyword.
Basic understanding of synchronized
Synchronized has long been an elder statesman in multithreaded concurrent programming, and many would call it a heavyweight lock. However, with various optimizations made to Synchronized in Java SE 1.6, biased and lightweight locks introduced in Java SE 1.6 to reduce the performance cost of acquiring and releasing locks are less heavy in some cases.
Basic use of synchronized
The use of synchronized can be divided into two forms: one is to modify code blocks (more flexible), the other is to modify methods;
There are two kinds of scope: object lock and class lock, the difference is whether to protect across objects and threads
-
Accessorize instance methods (scoped to the current instance, lock the current instance before entering the code)
Synchronized is equivalent to synchronized(this), which modifies the current instance object in a code block
-
Modify a block of code (specify a lock object, for a lock object, acquire the lock for a given object before entering the synchronized code)
-
Modify static methods (scoped to class objects, shackled to the current class object, to obtain the current class object lock before entering the synchronized code)
Synchronized modifies static methods in the same way that synchronized(Class) modifies the current Class object in a code block, and is global in scope
Code sample
Synchronized synchronized synchronized synchronized synchronized synchronized synchronized synchronized synchronized synchronized synchronized Methods test1 and test2 are equivalent (object locks), methods test3 and test4 are equivalent (class locks) */
public class SynchronizedTest {
public synchronized void test1(a) {}
public void test2(a) { synchronized (this) {}}public synchronized static void test3(a) {}
public void test4(a) { synchronized (SynchronizedTest.class) {} }
public static void main(String[] args) throws IOException {
SynchronizedTest st1 = new SynchronizedTest();
SynchronizedTest st2 = new SynchronizedTest();
// Different instances call the same method
// new Thread(()->st1.test1()).start();
// new Thread(()->st2.test1()).start();
// The same instance calls different methods
// new Thread(()->st1.test1()).start();
// new Thread(()->st1.test2()).start();
// Example of a class lock
new Thread(()->st1.test4()).start();
newThread(()->st2.test4()).start(); System.in.read(); }}Copy the code
How are synchronized locks stored in memory?
Syntactically synchronized(lock) controls the granularity of the lock based on the life cycle of the object, i.e. scope is determined by the life cycle of the object (instance, class object). Next, take a look at the JVM source code to see how objects are laid out in memory.
Layout of objects in memory
In the Hotspot virtual machine, the layout of the object in memory can be divided into three parts: Header, Instance Data, and Padding, as shown below:
JVM source code implementation
We create an instance of an object using new in Our Java code, and the JVM actually creates an instanceOopDesc object.
The Hotspot VIRTUAL machine uses the Oop-Klass model to describe Java Object instances (OOP (Ordinary Object Point) is used to describe specific types of Object instances). The Hotspot VIRTUAL machine uses instanceOopDesc and arrayOopDesc to describe object headers (arrayOopDesc describes array types)
InstanceOopDesc corresponding hotstpot virtual machine file location/SRC/share/vm/oops/instanceOop HPP, Corresponding hotstpot arrayOopDesc virtual machine file location/SRC/share/vm/oops/arryOop HPP, source code is as follows:
class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size(a) { return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes(a) {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes(a);return(offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); }};Copy the code
As can be seen from the instanceOopDesc code, instanceOopDesc inherited from oopDesc, oopDesc definition in the/SRC/share/vm/oops/oop. HPP
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
/** omit the following part of the source */
}
Copy the code
As can be seen from the source code, the oopDesc definition contains two members, respectively _mark and _metadata
_mark
Represents an object tag of type markOop, also known as MarkWord, that records information about the object and the lock_metadata
Represents the class metadata information stored in the first address of the object pointing to his class metadata (Klass), where_klass
Represents a normal pointer,_compressed_klass
Represents a compressed class pointer
MarkOop defined in/SRC/share/vm/oops/markOop HPP file, the source code is as follows:
// The markOop describes the header of an object.
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
// - hash contains the identity hash value: largest value is
// 31 bits, see os::random(). Also, 64-bit vm's require
// a hash value no bigger than 32 bits because they will not
// properly generate a mask larger than that: see library_call.cpp
// and c1_CodePatterns_sparc.cpp.
//
// - the biased lock pattern is used to bias a lock toward a given
// thread. When this pattern is set in the low three bits, the lock
// is either biased toward a given thread or "anonymously" biased,
// indicating that it is possible for it to be biased. When the
// lock is biased toward a given thread, locking and unlocking can
// be performed by that thread without using atomic operations.
// When a lock's bias is revoked, it reverts back to the normal
// locking scheme described below.
//
// Note that we are overloading the meaning of the "unlocked" state
// of the header. Because we steal a bit from the age we can
// guarantee that the bias pattern will never be seen for a truly
// unlocked object.
//
// Note also that the biased state contains the age bits normally
// contained in the object header. Large increases in scavenge
// times were seen when these bits were absent and an arbitrary age
// assigned to all biased objects, because they tended to consume a
// significant fraction of the eden semispaces and were not
// promoted promptly, causing an increase in the amount of copying
// performed. The runtime system aligns all JavaThread* pointers to
// a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
// to make room for the age bits & the epoch bits (used in support of
// biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
//
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
//
// We assume that stack/thread pointers have the lowest two bits cleared.
class markOopDesc: public oopDesc {
private:
// Conversion
uintptr_t value(a) const { return (uintptr_t) this; }
public:
// Constants
enum { age_bits = 4.// Generation age
lock_bits = 2./ / lock
biased_lock_bits = 1.// Whether the lock is biased
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, // The hash value of the object
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2 // The timestamp of bias lock
};
/** other code omitted */
}
Copy the code
MarkWord records information about objects and locks, and when an object is modified as a lock by the synchronized keyword, a series of subsequent operations around the lock are related to MarkWord. MarkWord is 32-bit in 32-bit and 64-bit in 64-bit. The data stored in MarkWord will change with the change of the lock flag bit, which can be divided into the following five types:
Any object in JAVA can be locked
-
Every Object in Java is derived from the Object class, and Java Object has a native C++ Object oop/oopDesc corresponding to it within the JVM
-
When a thread obtains a lock, it is actually acquiring a monitor object. Monitor can be thought of as a synchronization object. All Java objects carry monitor naturally, as shown in the markOop.
ObjectMonitor* monitor(a) const {
assert(has_monitor(), "check");
// Use xor instead of &~ to provide one extra tag-bit check.
return (ObjectMonitor*) (value() ^ monitor_value);
}
Copy the code
When multiple threads access the code quickly, it is equivalent to contending with the ObjectMonitor and modifying the lock identifier in the object. In the above code, the ObjectMonitor object has a close relationship with the logic of thread contending for the lock.
Upgrades to synchronized locks
Synchronized locks have been optimized after java1.6. What is the background of the optimization or the requirement scenario for biased and lightweight locks?
The previous analysis of MarkWord source, mentioned biased lock, lightweight lock, heavyweight lock, since the use of lock can achieve thread security, but also bring performance degradation. If locks are not used, concurrency performance can be improved, but thread safety cannot be guaranteed, and both cannot meet requirements at the same time.
The authors of the Hotspot VIRTUAL machine have investigated and found that, in most cases, the code for locks not only does not have multithreaded contention, but the same thread always acquires the lock multiple times. Based on this probability, locking was optimized after jdk1.6 to reduce the performance overhead of acquiring and releasing locks by introducing the concept of biased locking and lightweight locking. Therefore, synchronized locks have four states at runtime: no lock, biased lock, lightweight lock and heavyweight lock. The state of the lock will be upgraded from low to high according to the intensity of the lock contention. (Locks can only be upgraded (or revoked), not degraded)
The basic principle of bias locking
After investigation, in most cases, there is no multi-thread competition when the lock is not tight, but the same thread always obtains the lock for many times. In order to make the cost of the lock lower, the concept of biased lock is introduced.
Point a thread access to join synchronized block of code lock will be stored in the object lock head of the current thread ID, following this thread entry and exit the synchronized code block, do not need again chains and release the lock, a direct comparison object is stored in the first point to the current thread to lock, if the thread ID of biased locking storage and the current thread ID is equal, Indicates that the bias lock is biased toward the current thread, so there is no need to try again to acquire the lock.
Biased lock acquisition and undo logic
-
Get the MarkWord of the lock object and determine if it is in biased state (biASED_lock =1 and ThreadId is null)
-
If it is biased, the current thread ID is written to MarkWord via CAS
- If the CAS succeeds, indicating that the biased lock of the lock object has been acquired, the block of synchronized code is then executed
- If the CAS fails, it means that another thread has acquired a biased lock, and the current lock is contested. The thread that has acquired the lock needs to be revoked and the lock upgraded to a lightweight lock (this operation can only be performed until the global safe point, i.e. no thread has executed the synchronized code block).
-
If the state is biased, you need to check that the ThreaId stored in MarkWord is equal to the ID of the current thread
-
If it is equal, the lock does not need to be acquired again and the synchronized code block can be executed directly
-
If not, the biased lock is biased to other threads. You need to cancel the biased lock and upgrade it to a lightweight lock
-
Bias lock revocation
The revocation of biased lock is not to restore the lock object to the lock-free biased state (biased lock does not have the concept of lock release), but to upgrade biased lock directly to lightweight lock when cas fails and thread contention exists in the process of acquiring the lock.
When the thread with bias lock is revoked, the thread with bias lock has two cases:
-
If the thread that obtained the biased lock has exited the critical section, that is, the synchronization code block is completed, the object head will be set to lock free, and the thread that contended for the lock can re-bias itself based on CAS.
-
If the thread that obtained the bias lock has not finished executing the synchronized code block and is within the critical zone, the bias lock will be upgraded to a lightweight lock, and the thread that obtained the bias lock will continue executing the code block
In practical application development, there must be more than two threads competing for locks in the vast majority of cases, so enabling biased locking will increase the resource consumption of obtaining locks. The JVM parameter UserBiasedLocking can be used to set the opening and closing of biased locking
The fundamentals of lightweight locks
Locking and unlocking logic for lightweight locks
After the lock is upgraded to a lightweight lock, the MarkWord of the lock object will be changed accordingly. The process of upgrading to a lightweight lock is as follows:
- A thread creates a LockRecord in its own stack pin
- Copies the MarkWord of the object header of the lock object into the lock record created by the thread
- Points the owner pointer in the lock record to the lock object
- Replace the MarkWord in the object header of the lock object with a pointer to the thread lock record
spinlocks
Lightweight locks use spin locks in the locking process, which means that while another thread is competing for the lock, the thread will loop in place instead of blocking it. Once the thread that acquired the lock releases it, the thread can acquire it immediately (the process of acquiring the lock consumes CPU resources as it loops in place, equivalent to executing a for loop with nothing at all). Therefore, lightweight locks are only used in scenarios where the synchronous code block execution time is very short, so that the thread waiting in place can acquire the lock in a very short time.
The use of a spin lock is also the background to a probability that the performance of the lock can be improved through a loop when most synchronized blocks of code are executed in a short time. However, the spin must be conditionally controlled, otherwise it will consume too much CPU resources as one thread synchronizes code blocks for too long. The default number of spins is 10, which can be modified with the JVM parameter PreBlockSpin.
After JDK1.6, adaptive spin locking was introduced, which means that the number of spins is not fixed, but is determined by the time of the previous spin on the same lock and the state of the lock owner. If, on the same lock object, a thread spins to wait successfully to acquire the lock, and the thread holding the lock is running, then the virtual machine assumes that the spin is likely to succeed again, and it will allow the spin wait to last a relatively long time. If a lock is rarely successfully acquired with spin, subsequent attempts to acquire the lock may omit the spin process and block directly, avoiding wasting processor resources.
Unlock lightweight locks
The release process of lightweight lock is to obtain the reverse logic of the lock. The LockRecord in the stack pin can be replaced back to the MarkWord through CAS operation. If successful, there is no competition. If it fails, the current lock is competing, and the lightweight lock is upgraded to the heavyweight lock.
The fundamentals of heavyweight locking
When a lightweight lock expands beyond a heavyweight lock, it means that the thread can only be suspended and blocked, waiting to be woken up.
Monitor for heavyweight locks
public class App {
public static void main(String[] args) {
synchronized (App.class) {}
test();
}
public static synchronized void test (a) {}}Copy the code
Java P tool to view the generated class file information to analyze the real details of synchronized keyword javap -v app. class,
After the synchronization block is added, you will see a Monitorenter and Monitorexit in the bytecode file
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/seiya/concurrent/App
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method test:()V
18: return
Copy the code
Each Java object is associated with a monitor, which we can think of as a lock. When a thread wants to execute a block of code modified by the synchronized keyword, the line first acquires the monitor of the synchronized modified object. Monitorenter means to acquire an object monitor, and MonitoreXit means to release ownership of the monitor so that other blocked threads can try to acquire the monitor.
The monitor relies on the MutexLock of the operating system. When a thread is blocked, it enters the kernel scheduling state. This causes the system to switch back and forth between the user state and the kernel state, which seriously affects the lock performance.
Heavyweight lock lock basic process
Any thread accessing Object (Object with synchronized protection) first obtains the monitor of Object. If it fails, the thread enters the synchronized queue and becomes BLOCKED. When a precursor to an Object (the thread that acquired the lock) releases the lock, the release wakes up the thread blocking on the synchronization queue to retry the acquisition of the monitor.
Synchronized combines wait, notify, and NotifyAll of Java Objects
When a blocking thread is woken up depends on when the thread that acquired the lock finishes executing the synchronized block and releases the lock. If you want to display control, you need to use a signaling mechanism: Object provides wait, notify, and NotifyAll to control the state of threads.
Wai /notify/ NotifyAll basic concepts
-
Wait: Thread A, which holds the object lock, intends to release the lock permission, release CPU resources, and enter the waiting state
-
Notify: Indicates that thread A, which holds the object lock, intends to release the lock permission, notifying the JVM to wake up A thread X, which is competing for the object lock. After thread A completes the synchronization code and releases the lock, thread X obtains the object lock permission directly, and the other competing threads continue to wait (even after thread X completes the synchronization and releases the object lock, the other competing threads continue to wait until A new Notify or NotifyAll awakens).
-
Notifyall: Different from notify, notifyAll wakes up all the threads competing for the same object lock. After thread A releases the lock, notifyAll the awakened threads may obtain the object lock permission
Three methods must be in a synchronized synchronous keyword limit within the scope of the call, or complains. Java lang. IllegalMonitorStateException, which means no synchronization, so the thread on the state of the object lock is uncertain, can’t call these methods. In addition, synchronization is used to ensure that the notify thread is aware of changes made to the variables when it returns from the WAIT method.