Art is long, life is long

First, why there are thread safety problems under multi-threading

Security issues with shared variables

A variable I, if thread A or thread B can access and modify the value of variable I independently without any problem, then if you modify variable I in parallel, there will be A security problem.

public class MyTest {

    public static int i = 0;

    public static void addA(a){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
       		new Thread(new Runnable() {
                @Override
                public void run(a) {
                    addA();
                }
            }).start();
        }
        Thread.sleep(2000); System.out.println(MyTest.i); }}Copy the code

The output may be 98 the first time and 97 the second time, which is different from the expected result (100), so whether an object is thread-safe depends on whether it is accessed by multiple threads 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 results of this object to keep consistent with the result we expected), that means the object is thread-safe.

For thread-safety, access to data state is essentially managed, and this state is usually shared and mutable.

Shared: this data variable can be accessed by multiple threads.

Mutable: The value of a variable can change over its lifetime.

2, Synchroinzed keyword (solve shared resource competition problem)

Synchroinzed has been around for a long time, but it used to be a heavyweight lock, so it was easy to use. Synchroinzed was optimized in javaSE 1.6 to introduce biased and lightweight locking. Using Synchroinzed is recommended for low concurrency situations. This is recommended for low concurrency cases because Synchroinzed upgrades to heavyweight locks.

Locking mode:

  1. Modifier instance method, the lock is the current instance object, to enter the synchronization code to obtain the lock of the current instance
  2. Modifying static methods, the lock is the class object of the current class, which is acquired before entering the synchronized code
  3. Modifies a block of code. A lock is an object inside parentheses. A lock is acquired for a given object before entering a synchronized code base.
public class SynchroinzedDemo {

    /** * lock static methods */
    public static synchronized void test(a){}
    /** * lock the instance method */
    public synchronized void test1(a){}
    /** * Lock code block */
    public void test2(a){
        synchronized(this) {}}}Copy the code

Lock the above code:

public class MyTest {

    public static int i = 0;

    public synchronized static void addA(a){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
          new Thread(new Runnable() {
                @Override
                public void run(a) {
                    addA();
                }
            }).start();
        }
        Thread.sleep(2000); System.out.println(MyTest.i); }}Copy the code

After several runs, the result is 100, indicating thread-safe.

Composition of Java objects

In the JVM, objects are divided into three areas of memory:

  • Object head

    • Mark Word: Default store object HashCode, generational age, and lock flag bit information. It reuses its own storage space based on the state of the object, which means that the data stored in Mark Word changes as the lock flag bit changes during runtime.
    • Klass Point: A pointer to an object’s class metadata that the virtual machine uses to determine which class the object is an instance of.
  • The instance data

    • This part is mainly to store the data information of the class, the information of the parent class.
  • The filling

    • Since the virtual machine requires that the object’s starting address be a multiple of 8 bytes, the padding data does not have to exist, just for byte alignment.

      Have you ever been asked how many bytes an empty object takes? It is 8 bytes, because of the alignment of the padding, less than 8 bytes to fill it will help us automatically complete.

Lock escalation

Lock upgrade direction: no lock – > bias lock – > lightweight lock – > heavyweight lock, and the expansion direction is irreversible.

Biased locking:

Biased locking is a lock optimization introduced in JDK6. In most cases, locks are not contested by multiple threads, and are always acquired multiple times by the same thread. Biased locking is introduced to make it cheaper for the thread to acquire the lock. A bias lock is biased in favor of the first thread to acquire it, and if the lock is not acquired by another thread during subsequent execution, the thread holding the bias lock will never need to synchronize.

Lightweight lock

Biased locks are quickly upgraded to lightweight locks if it becomes apparent that another thread is applying for the lock.

spinlocks

Spinlocks principle is very simple, if the thread holding the lock can lock is released in a very short time resources, and the thread lock wait for competition there is no need to do between kernel mode and user mode switch into the block pending state, they just need to wait for a while (spin), such as thread holding the lock immediately after releasing the lock locks, thus avoiding the consumption of user and kernel thread switching.

Heavyweight lock

Referring to the original Synchronized implementation, heavyweight locks are blocked when other threads attempt to acquire the lock and wake up only when the thread holding the lock releases it.

Lock upgrade scenario

  • Scenario 1: The program has no lock contention. So in this case we don’t need to lock, so in this case the object lock state is unlocked.

  • Scenario 2: Often only one thread locks.

    Locking process: The same thread may often acquire the lock. In this case, in order to avoid the performance cost caused by locking, the lock is not added in the actual sense. The execution process of partial locking is as follows:

  1. The thread first checks whether the thread ID of the object header is the current thread;

  2. A: If the thread ID of the object header is the same as the current thread ID, the code is executed directly.

    B: If the thread ID is not the current thread ID, use CAS to replace the thread ID in the object header. If CAS fails to replace the thread ID, it indicates that a thread is executing and there is lock contention. In this case, cancel biased lock and upgrade to lightweight lock.

  3. If the CAS replacement succeeds, change the thread ID of the object header to its own thread ID, and then execute the code.

  4. After executing the code, release the lock and change the thread ID of the object header to null.

  • Scenario 3: There are threads competing for locks, but the conflict time for acquiring locks is short.

When lock conflicts start, biased locks are upgraded to lightweight locks; Thread acquiring a lock conflict, thread must make a decision is to continue to wait for, here or go home and wait for anybody else’s call, and lightweight lock subgrade is with the method of continue to wait here, when the lock conflicts are detected, the thread will first use way of the spin cycle locks here, because of the way to use the spin very consume CPU, When a lock cannot be spun for a certain amount of time, the lock is upgraded to a heavyweight lock.

  • Scenario 4: There are a large number of threads competing for locks, and the conflict is high.

We know that when the lock acquisition conflict is many and the time is longer, our thread cannot continue to wait here, so we have to rest first, and then wait for the thread that acquired the lock to release the lock before starting the next round of lock contention, and this form is our heavyweight lock.

User mode and kernel mode

All programs run in user-space and go into user-run state (user-mode), but many operations, such as I/O, may go into kernel-run state (kernel-mode).

The process is summarized as follows:

  1. The user mode puts some data into a register, or creates a corresponding stack, indicating the need for services provided by the operating system.
  2. User mode performs system calls (system calls are the smallest functional unit of the operating system).
  3. The CPU switches to the kernel state and jumps to the corresponding memory specified location to execute the instruction.
  4. The system calls the processor to read the data parameters we previously put into memory and execute the program’s request.
  5. When the call is complete, the operating system resets the CPU to user mode to return the result and execute the next instruction.

Iii. How does JM achieve synchronized?

Let’s take a look at a demo:

public class Sync {

    public void demo(Object o){
        synchronized (o){

        }
    }
}
Copy the code

Go to the directory where the class file is located and use javap -v demo.class to see the compiled bytecode (which I’ve captured here):

	public void demo(java.lang.Object); descriptor: (Ljava/lang/Object;) V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=2
         0: aload_1
         1: dup
         2: astore_2
         3: monitorenter
         4: aload_2
         5: monitorexit
         6: goto          14
         9: astore_3
        10: aload_2
        11: monitorexit
        12: aload_3
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
Copy the code

You should see that when a program declares a synchronized block, the compiled bytecode contains monitorenter and Monitorexit directives, These two instructions consume a reference type element on the operand stack (the reference inside the parentheses of the synchronized keyword) as the lock object to be unlocked. If you look closely, there is a Monitorenter directive and two Monitorexit directives, which the Java virtual machine uses to ensure that locks acquired are unlocked on both normal and abnormal execution paths.

Monitorenter and Monitorexit each lock object has a lock counter and a pointer to the thread that holds the lock:

  • When monitorenter executes, the target lock object’s counter is 0, indicating that it is not occupied by another thread. The Java Virtual Machine assigns it to a thread that requests it and increments the counter
    • If the target lock object counter is not 0, the Java VIRTUAL machine can increment its counter by 1 if the lock object holds the current thread. What if it does not? I’m sorry, I’m just gonna have to wait for the holding thread to release
  • When Monitorexit is executed, the Java virtual machine decreases the count of the lock object by 1. When the count drops to 0, the lock is released, and the request succeeds if it is requested by another thread

Why this approach? To allow the same thread to acquire the same lock repeatedly. For example, if a Java class has several synchronized methods, calls between these methods, either directly or indirectly, involve repeated locking operations on the same lock. This way to design, you can avoid this situation.

Conclusion:

The synchronized keyword is secured by monitorenter and Monitorexit directives

When a thread is about to acquire a shared resource:

  • Check if MarkWord has its own ThreadID in it. If so, the current thread is in a “biased lock”.
  • If not, the lock is upgraded and the CAS operation is used to perform the switch. The new thread suspends the previous thread based on the existing ThreadID in MarkWord, leaving the MarkWord content empty.
  • Then, both threads copy the lock object HashCode into the record space they created to store the lock. Then, they start using CAS to change the contents of the lock object MarkWord to the address of the record space they created. In this way, they compete with MarkWord. Threads that successfully execute CAS get resources, and those that fail go into spin
    • If the spinning thread succeeds in acquiring the resource during the spinning process (i.e., the thread that acquired the resource completes and frees the shared resource), the state remains the sameLightweight lockThe state of the
    • If no resources are available, enterHeavyweight lockAt this point, the spinning thread blocks, waits for the previous thread to complete execution and wakes itself up