Detailed explanation of the use of multi-threaded lock syn\lock

Table of contents

One: synchronized

1.1: Synchronized is a heavyweight lock

1.2: The underlying implementation principle of Synchronized

1.3:Synchronized lock storage location

1.4: Upgrade process of Synchronized lock

Two: Lock

2.1Lock

2.2ReentrantLock

2.2.1 Fair lock/unfair lock

2.2.2 Timeout mechanism

2.2.3 Reentrant locks

2.3 Read-write lock ReentrantReadWriteLock

Three: Additional Supplements:

3.1 In-depth analysis of lock: principle of read-write lock


Foreword:

Multiple threads share the same global variable or static variable, and multiple threads read data at the same time without data security issues, but when one thread writes data, it is possible for other threads to read and write shared data A thread safety problem occurred.

For example, if there is a primary key id interface that manually encapsulates a certain module, in the case of multi-threading, the global variable is not locked, which may cause the situation that the primary key is the same.

The normal operation mode is to lock public resources such as public code or global variable replication, for example:

1. Synchronized lock (biased lock, lightweight lock, heavyweight lock)

2. Volatile locks can only guarantee the visibility between threads, but cannot guarantee the atomicity of data

3. The Atomic atomic class provided in the jdk1.5 concurrent package

4. Lock lock

One: synchronized

例如:某一个代码块进行加锁处理:

       Object o=new Object();
        synchronized(o.getClass()){
            System.out.println(name+"开始执行");
            try {
                Thread.sleep(2000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }

            System.out.println(name+"执行完毕");
        }

例如:整理api进行加锁
    public synchronized String getValue(String name){
        System.out.println(name+"开始执行");
        try {
            Thread.sleep(2000);
        }catch (Exception e){
        }
        System.out.println(name+"执行完毕");
        retuen null;
    }
   

Note: It should be noted that when the same object is locked, it will cause access resource conflicts. When it is not the same object, this will not be the case, for example:

SynchronizedTest t=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.getValue("线程1");
            }
        }).start();
        SynchronizedTest t1=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t1.getValue("线程2");
            }
        }).start();

SynchronizedTest t=new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.getValue("线程1");
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.getValue("线程2");
            }
        }).start();

 

 This is the overall code of the lock API. The test situation is this. If the lock keyword is placed in the static method or the first lock example above, only part of the code is locked. At this time, no matter whether it is an object or not, the resource will be locked. access;

1.1: Synchronized is a heavyweight lock

Synchronized is implemented through a monitor lock (monitor) inside the object, and the essence of the monitor lock is implemented by relying on the Mutex Lock (mutual exclusion lock) of the underlying operating system. The operating system needs to switch from user mode to core mode to switch between threads. This cost is very high, and the transition between states takes a relatively long time, which is why Synchronized is inefficient. Therefore, we call this kind of lock implemented by the operating system Mutex Lock "heavyweight lock".

1.2: The underlying implementation principle of Synchronized

A synchronized method implicitly locks the method through the ACC_SYNCHRONIZED keyword. When the method to be executed by the thread is marked with ACC_SYNCHRONIZED, it needs to obtain the lock before executing the method.
Synchronous code blocks are locked through monitorenter and monitorexit execution. When the thread executes to monitorenter, the lock must be obtained before the subsequent methods can be executed. When the thread executes to monitorexit, the lock must be released. Each object maintains a counter of the number of times it is locked. When the counter is not 0, only the thread that acquires the lock can acquire the lock again
 

1.3:Synchronized lock storage location

The lock used by Synchronized exists in the object header of java. An object is mainly divided into 4 parts in memory after new comes out:

Mark Word: Stores the object's hashCode, GC information, and lock information in three parts. This part occupies 8 bytes.

Class Pointer: A pointer to class object information is stored. On the 64-bit JVM, there is a compressed pointer option -ClassPointer pointer: -XX:+UseCompressedClassPointers is 4 bytes and is not turned on to 8 bytes. It is enabled by default.

Instance data (instance data): records the variable data in the object. Reference type: -XX:+UseCompressedOops is 4 bytes and does not open to 8 bytes Oops Ordinary Object Pointers

Padding: Used for alignment. In the 64-bit service version of the object, it is stipulated that the object memory must be divisible by 8 bytes. If it is not divisible, it will not be possible to rely on alignment for a long time. For example: new creates an object, the memory only occupies 18 bytes, but it is stipulated that it can be divisible by 8, so the
storage structure of padding=6 Mark Word is as follows:

1.4: Upgrade process of Synchronized lock

In order to reduce the performance consumption caused by acquiring and releasing locks, Java SE 1.6 introduces " biased locks " and "lightweight locks": there are 4 states of locks, and the order of levels from low to high is: lock-free state, biased Lock state, lightweight lock state, and heavyweight lock state. Locks can be upgraded but not downgraded.

1. Biased lock: In most cases, not only does the lock not have multi-thread competition, but it is always obtained by the same thread multiple times. In order to make the thread acquire the lock at a lower cost, a biased lock is introduced. When a thread accesses the synchronization block and acquires the lock, it will record the thread ID storing the lock bias in the object header and the stack frame. Later, when the thread enters the synchronization block, it first judges whether the Mark Word pointing to the current thread is stored in the object header. Biased lock, if it exists, acquire the lock directly.

2. Lightweight lock: When other threads try to compete for a biased lock, the lock is upgraded to a lightweight lock. Before the thread executes the synchronization block, the JVM will first create a space for storing the lock record in the stack frame of the current thread, and replace the MarkWord in the object header with a pointer to the lock record. If successful, the current thread acquires the lock. If it fails, other threads are identified as competing for the lock, and the current thread tries to use spin to acquire the lock.

3. Heavyweight lock: When the lock is waiting in place, it will consume CPU resources. Therefore, the spin must have certain conditional control, otherwise, if a thread executes the synchronization code block for a long time, the thread waiting for the lock will continue to loop and consume CPU resources instead. By default, the number of lock spins is 10 times. You can use the -XX:PreBlockSpin parameter to set the number of times the spin lock waits. If the lock has not been acquired after 10 times, it will be upgraded to a heavyweight lock
 

Two: Lock

Foreword:

Compared with synchronized, lock is more flexible to try to lock code fragments

For example: acquire and release locks by hand, acquire lock A first, then acquire lock B, release lock A after acquiring lock B, and acquire lock C at the same time, etc. Code logic shackles


2.1Lock

Lock is an interface class, which has two implementation classes, and the specific implementation can be distinguished

ReentrantLock、ReentrantReadWriteLock

2.2ReentrantLock

The demo example is as follows: in case there is an exception and the lock cannot be released, add tra to catch the exception, and release the lock in finally


    Lock lock = new ReentrantLock();
    public void run(){
        //获取锁 
        lock.lock();
        try {
           //公关资源处理,
        }catch (Exception e){

        }finally {
            //释放锁
            lock.unlock();
        }
       
    }


    

ReentrantLock It is mainly realized by using CAS+AQS queue. It supports fair locks and unfair locks, both of which are implemented similarly.

CAS is mainly for comparison and replacement, and AQS is mainly for queue locks. , have time to add here; TODO to be improved

2.2.1 Fair lock/unfair lock

By default, non-functional locks are created. To create fair locks, add true when creating objects

  • Unfair lock: If another thread comes in and tries to acquire it at the same time, it is possible for this thread to acquire it first;

  • Fair lock: If another thread comes in to try to acquire at the same time, when it finds that it is not at the head of the queue, it will be queued to the end of the queue, and the thread at the head of the queue will acquire the lock.

The structure supports passing  fair in to specify whether it is a fair lock

  • FairSync() fair lock
  • NonfairSync() unfair lock

2.2.2 Timeout mechanism

tryLock(long timeout, TimeUnit unit) Provides the function of acquiring locks over time. Its semantics is to return if the lock is acquired within the specified time true, and return if the lock is not acquired false.

2.2.3 Reentrant locks

1. Reentrant lock. A reentrant lock means that the same thread can acquire the same lock multiple times. Both ReentrantLock and synchronized are reentrant locks.

2. Interruptible locks. An interruptible lock refers to whether a thread can respond to interruption during the process of trying to acquire the lock. Synchronized is an uninterruptible lock, and ReentrantLock provides an interrupt function.

3. Fair lock and unfair lock. A fair lock means that when multiple threads try to acquire the same lock at the same time, the order in which the locks are acquired is in the order in which the threads arrive, while an unfair lock allows threads to "jump in the queue". Synchronized is an unfair lock, and the default implementation of ReentrantLock is an unfair lock, but it can also be set as a fair lock
 

2.3 Read-write lock ReentrantReadWriteLock

Demo example:

百度copy一个demo代码,理解一下使用即可     
//声明读写锁
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //获取读锁
    private Lock readLock = readWriteLock.readLock();

    //获取写锁
    private Lock writeLock = readWriteLock.writeLock();

    private boolean isUpdate = true;

 public String read(String key) {
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + " 读操作正在执行。。。");
        try {
            Thread.sleep(2000);
            return map.get(key);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
        return null;
    }

    /**
     * 写操作
     * @param key
     * @param value
     */
    public void write(String key, String value) {
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + " 写操作正在执行。。。");
        try {
            Thread.sleep(2000);
            map.put(key, value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * 读写操作  锁的降级
     */
    public void readWrite(){
        readLock.lock();    //保证isUpdate是最新的数据
        if (isUpdate) {
            readLock.unlock();
            writeLock.lock();
            map.put("name", "admin");
            readLock.lock(); //锁的降级,如果此处不加读锁,直接执行下一步释放写锁,多线程情况下有可能又被写锁抢占资源,通过此方法实现将写锁降级为读锁
            writeLock.unlock();
        }

        String value = map.get("name");
        System.out.println(value);
        readLock.unlock();
    }

    public static void main(String[] args) throws Exception{
        ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();

        //写操作测试  可以知道写操作是互斥的
        Runnable runnable1 = () -> {
            for (int i = 0; i < 5; i++) {
                readWriteLockTest.write("key" + i, "value"+i);
            }

        };
        new Thread(runnable1).start();
        new Thread(runnable1).start();
        new Thread(runnable1).start();

        //读操作测试  可以知道读操作是可以并发执行的
        Runnable runnable2 = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(readWriteLockTest.read("key" + i));
            }

        };
        new Thread(runnable2).start();
        new Thread(runnable2).start();
        new Thread(runnable2).start();

        //读写操作测试 锁的降级
        Runnable runnable3 = () -> {
            for (int i = 0; i < 5; i++) {
                readWriteLockTest.readWrite();
            }

        };
        new Thread(runnable3).start();
        new Thread(runnable3).start();
        new Thread(runnable3).start();
    }
}

When there is no write operation, there is no problem with multiple threads reading a resource at the same time, so multiple threads should be allowed to read the shared resource at the same time; but if a thread wants to write these shared resources, other threads should not be allowed to read the shared resource. Read and write operations are performed.

For this scenario, JAVA's concurrent package provides a read-write lock ReentrantReadWriteLock, which represents two locks, one is a lock related to read operations, called a shared lock; the other is a lock related to writing, called an exclusive lock, described as follows

Prerequisites for a thread to enter a read lock:

  • no write locks from other threads,
  • There is no write request or there is a write request, but the calling thread and the thread holding the lock are the same.

Preconditions for a thread to enter a write lock:

  • No read locks from other threads
  • no write locks from other threads

That is to say:

  • When a thread holds a read lock, the thread cannot acquire the write lock (because when acquiring the write lock, if the current read lock is found to be occupied, the acquisition will fail immediately, regardless of whether the read lock is held by the current thread).

  • When a thread holds a write lock, the thread can continue to acquire the read lock (if the write lock is found to be occupied when acquiring the read lock, the acquisition will fail only if the write lock is not occupied by the current thread).

The read-write lock has the following three important characteristics:

(1) Fair selectivity: It supports unfair (default) and fair lock acquisition methods, and the throughput is still better than fair.

(2) Re-entry: Both read locks and write locks support thread re-entry.

(3) Lock downgrade: Following the sequence of acquiring a write lock, acquiring a read lock, and then releasing a write lock, a write lock can be downgraded to a read lock.

Three: Additional Supplements:

3.1 In-depth analysis of lock: principle of read-write lock

To quote a blog post, the link is as follows:

Read lock principle of read-write lock: If a thread wants to apply for a read lock, it will first determine whether the write lock is held. If the write lock is held and the current thread is not the thread holding the write lock, then it will return - 1. Failed to acquire the lock, enter the waiting queue to wait. If the write lock is not held by the thread or the current thread and the thread holding the write lock are the same thread, it will start to acquire the read lock. The thread will first judge whether the number of read locks exceeds 65535. If not, CAS modifies the value of the upper 16 bits of the state variable, that is, the value of the state is +1. If this step fails, it will loop this operation until it succeeds. until. After the CAS modification is successful, it means that the read lock is acquired successfully. It will judge whether the current thread is the first read thread. If so, set the first multi-thread and first read counter (for performance and reentrancy). If it is not the first time to acquire the read lock, judge whether it is the same as the first reading thread, and if it is the same thread as the first reading thread, the first reading counter will be +1. If it is not the first time to read the thread, judge whether it is the last time to read the thread, and if so, add 1 to the last read counter. If not, create a new counter, set the last read thread as its own thread, and then refresh its read counter.

protected final int11 tryAcquireShared(int unused) {
 
    Thread current = Thread.currentThread();
    int c = getState();
    // exclusiveCount(c) != 0 ---》 用 state & 65535 得到低 16 位的值。如果不是0,说明写锁别持有了。
    // getExclusiveOwnerThread() != current----> 不是当前线程
    // 如果写锁被霸占了,且持有线程不是当前线程,返回 false,加入队列。获取写锁失败。
    // 反之,如果持有写锁的是当前线程,就可以继续获取读锁了。
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
        // 获取锁失败
        return -1;
    // 如果写锁没有被霸占,则将高16位移到低16位。
    int r = sharedCount(c);// c >>> 16
    // !readerShouldBlock() 和写锁的逻辑一样(根据公平与否策略和队列是否含有等待节点)
    // 不能大于 65535,且 CAS 修改成功
    if (!readerShouldBlock() && r < 65535 && compareAndSetState(c, c + 65536)) {
        // 如果读锁是空闲的, 获取锁成功。
        if (r == 0) {
            // 将当前线程设置为第一个读锁线程
            firstReader = current;
            // 计数器为1
            firstReaderHoldCount = 1;
 
        }// 如果读锁不是空闲的,且第一个读线程是当前线程。获取锁成功。
         else if (firstReader == current) {// 
            // 将计数器加一
            firstReaderHoldCount++;
        } else {// 如果不是第一个线程,获取锁成功。
            // cachedHoldCounter 代表的是最后一个获取读锁的线程的计数器。
            HoldCounter rh = cachedHoldCounter;
            // 如果最后一个线程计数器是 null 或者不是当前线程,那么就新建一个 HoldCounter 对象
            if (rh == null || rh.tid != getThreadId(current))
                // 给当前线程新建一个 HoldCounter ------>详见下图get方法
                cachedHoldCounter = rh = readHolds.get();
            // 如果不是 null,且 count 是 0,就将上个线程的 HoldCounter 覆盖本地的。
            else if (rh.count == 0)
                readHolds.set(rh);
            // 对 count 加一
            rh.count++;
        }
        return 1;
    }
    // 死循环获取读锁。包含锁降级策略。
    return fullTryAcquireShared(current);
}

 

Citing two articles: In-depth analysis: AQS principle

In-depth analysis: principle of read-write lock

Guess you like

Origin blog.csdn.net/qq_44691484/article/details/130186562