JAVA Concurrent Programming Introduction, thinking about the realization philosophy behind synchronous lock

Multithreading is similar to preemptive multitasking processing in concept. The reasonable use of threads can improve the processing ability of the program, but it also brings disadvantages. For shared variable access, there will be security problems. Here is an example of multi-threaded access to shared variables:

public class ThreadSafty {

    private static int count = 0;

    public static void incr() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count ++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0 ; i < 1000; i++) {
            new Thread(()->{
                ThreadSafty.incr();
            },"threadSafty" + i).start();
        }
        TimeUnit.SECONDS.sleep(3);
        System.out.println("The operation result is:" + count);
    }

}

The run result of the variable count is always a random number less than or equal to 1000 because of the visibility and atomicity of the thread.

1, Data security of multithreaded access

How to ensure the data security of threads running in parallel, the first thing we can think of here is to lock it. There are optimistic locks and pessimistic locks in relational databases, so what is a lock? It is a way to deal with concurrency and realize the feature of mutual exclusion.

The key to implementing locks in the Java language is Synchronized.

2, Basic application of Synchronized

2.1 three locking methods of synchronized

  • Static method: lock the current class object, and obtain the lock of the current class object before entering the synchronization code
synchronized  static void method(){}
  • Decorated code block: Specifies the lock object. Before entering the synchronization code, obtain the lock of the specified object
void method(){
    synchronized (SynchronizedDemo.class){}
}
  • Modify instance method: lock the current instance. Obtain the lock of the current instance before entering the synchronization code
    Object lock = new Object();
    //Only valid for the current object instance
    public SynchronizedDemo(Object lock){
            this.lock = lock;
    }
     void method(){
         synchronized(lock){}
    }

2.2 how do synchronized locks store data?

Take how the object is stored in the jvm memory as a starting point to see what features in the object can realize the lock

2.2.1 layout of objects in Heap memory

In the Hotspot virtual machine, the layout of objects in heap memory can be divided into three parts:

  • Object header: including object tag and class meta information
  • Instance data
  • Align fill

Hotspot uses instanceOopDesc and arrayopdesc to describe the object header, and arrayopdesc object to describe the array type. The definition of instanceOopDesc is defined in the instanceOop.hpp In addition, the definition of arrayopdesc corresponds to arrayOop.hpp

class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }

  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // 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();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

#endif // SHARE_VM_OOPS_INSTANCEOOP_HPP

See the source code instanceOopDesc inherits from oopDesc, which is defined in oop.hpp In the document:

class oopDesc {
  friend class VMStructs;
  private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;//Normal pointer
    narrowKlass _compressed_klass;//Compression class pointer
  } _metadata;

  // Fast access to barrier set.  Must be initialized.
  static BarrierSet* _bs;
......

There are two important member variables in the oopDesc class_ mark: records information related to the object and lock. It belongs to markOop type_ metadata: record class meta information

class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }

 public:
  // Constants
  enum { 
     age_bits                 = 4,//Generational age
     lock_bits                = 2,//Lock identification
     biased_lock_bits         = 1,//Biased lock or not
     max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
     hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,//Object's hashCode
     cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
     epoch_bits               = 2//Timestamp of biased lock
  };
......

markOopDesc records information about an object and a lock, which is often called Mark Word. When an object is added with the Synchronized keyword, a series of operations related to a lock are related to it. The length of Mark Word in 32-bit system is 32bit, and that in 64 bit system is 64bit.

The data in Mark Word will change as the flag bit of the lock changes.

2.2.2 layout of printing objects in Java

pom dependency

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
System.out.println(ClassLayout.parseInstance(synchronizedDemo).toPrintable());
com.sy.sa.thread.SynchronizedDemo object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           31 00 00 00 (00110001 00000000 00000000 00000000) (49)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Large end storage and small end storage

     0     4        (object header)                           31 00 00 00 (00110001 00000000 00000000 00000000) (49)
     4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Hex: 0x 00 00 00 00 00 01
 (64 bit) binary: 00000000 00000000 00000000 00000000 00000000 00000000

0 01 (unlocked)

  • The last three bits look at the status and flags of the lock.
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a8 f7 76 02 (10101000 11110111 01110110 00000010) (41351080)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

000 as a lightweight lock

2.2.3 why can all objects implement locks?

Each Object in Java is derived from the Object class, and each Java Object has a native C + + Object oop/oopDesc corresponding within the JVM.

When a thread acquires a lock, it actually acquires a monitor object. Monitor can be considered as a synchronization object. All Java objects are born with monitor. In the hotspot source code markOop.hpp In the file, you can see the following code:

ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
 }

When multiple threads access the synchronized code block, it is equivalent to scrambling the object monitor to modify the lock identifier in the object. The ObjectMonitor object in the above code is closely related to the logic of thread scrambling for lock.

2.3 lock upgrade of synchronized

The lock states are: no lock, biased lock, lightweight lock and heavyweight lock. The state of the lock is upgraded from low to high according to the intensity of competition.

2.3.1 deflection lock

Storage (take 32-bit as an example): thread ID (23bit)
                   Epoch(2bit)
                   age(4bit)
                   Whether the lock is biased (1bit)
                   Lock flag bit (2bit)

When a thread adds a Synchronized synchronization lock, the thread ID will be stored in the Object Header. When the thread enters or exits the synchronization code block, it does not need to add and release the lock again, but directly compares whether there is a biased lock pointing to the current thread in the Object Header. If the thread ID is equal, it means that the lock is biased to the current thread, and there is no need to acquire the lock again.

com.sy.sa.thread.ClassLayoutDemo object internals:
OFFSET SIZE  TYPE DESCRIPTION                VALUE
  0   4    (object header)              05 e8 45 03
(00000101 11101000 01000101 00000011) (54913029)
  4   4    (object header)              00 00 00 00
(00000000 00000000 00000000 00000000) (0)
  8   4    (object header)              05 c1 00 f8
(00000101 11000001 00000000 11111000) (-134168315)
  12   4    (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

2.3.2 lightweight lock

Storage (take 32-bit as an example): pointer to lock record in stack (30bit)
                   Lock flag bit (2bit)

If the biased lock is closed or the current biased lock points to other threads, then a thread will preempt the lock at this time, and the lock will be upgraded to a lightweight lock.

Lightweight lock uses spin lock in the process of locking, and JDK 1.6 uses adaptive spin lock.

2.3.3 heavyweight lock

Storage (take 32-bit as an example): pointer to mutex (heavyweight lock) (30bit)
                   Lock flag bit (2bit)

When the lightweight lock expands to the heavyweight lock, the thread can only be suspended and blocked waiting to be awakened. First, let's look at the code of a heavyweight lock:

public class HeavyweightLock {

    public static void main(String[] args) {
        HeavyweightLock heavyweightLock = new HeavyweightLock();
        Thread t1 = new Thread(()->{
            synchronized (heavyweightLock) {
                System.out.println("tl lock");
                System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
            }
        },"heavyheightLock");
        t1.start();
        synchronized (heavyweightLock) {
            System.out.println("main lock");
            System.out.println(ClassLayout.parseInstance(heavyweightLock).toPrintable());
        }
    }

}

Results after operation:

com.sy.sa.thread.HeavyweightLock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

tl lock
com.sy.sa.thread.HeavyweightLock object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           2a cc e9 02 (00101010 11001100 11101001 00000010) (48876586)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Every Java object is associated with a monitor, which can be understood as a lock. When a thread wants to execute a code block or object modified with Synchronized, the first thing it gets is the monitor of the Synchronized modified object. Basic process of heavyweight lock: monitorenter means to get an object monitor. monitorexit means to release the ownership of the monitor so that other blocked threads can try to obtain the monitor.

2.3.4 lock upgrade summary

  • Biased lock only uses CAS to record the address of the current thread in the tag of the lock object when the first request is made. When the thread enters the synchronization code block again later, it does not need to preempt the lock. It can directly judge the thread ID, which is applicable to the situation that the lock will be preempted by the same thread for many times.
  • The lightweight lock only uses CAS operation, replacing the mark field of the lock object with a pointer to the LockRecord in the current thread stack frame. This artifact stores the original mark field of the lock object, which aims at the situation that multiple threads apply for a common lock in different time periods.
  • Heavyweight locks block and wake up locked threads. It is suitable for multiple threads competing for the same lock at the same time.

Tags: Programming Java jvm less JDK

Posted on Sat, 23 May 2020 02:53:17 -0700 by downfall