The benefits of non-reentrant lock (spinlock) brought

What is the spin lock
multi-thread access to shared resources, in order to prevent concurrency issues caused by lock mechanisms are usually introduced to deal with concurrency issues.

A thread to get the resources for this resource lock, other threads such as B To access this resource must first obtain a lock, at which point A holding lock this resource, and only waiting for thread A logic executed, the lock is released, this time B in order to obtain the lock and then obtain the resources to the resource.

This process, A resource has been holding the lock, then no other thread to obtain a lock, such as how B do? Usually there will be two ways:

A process is not to acquire a lock directly into the blockage (BLOCKING), this is the mutex

2. Another is that the process does not acquire a lock, do not enter the obstruction, but has been circulating to see if A can wait until the release of the lock resource.

The above two methods, academic, there are several different definitions manner, when the university is learning C ++, the "C ++ 11" had this description:

Spin lock (spin lock) is a non-blocking lock, that is to say, if a thread needs to acquire a lock, but the lock is already in use by another thread, the thread will not be suspended, but is constantly consume CPU time, stop trying to acquire the lock.

Mutex (mutex) is blocking locks, when a thread can not acquire a lock, the thread will be directly suspended, the thread is no longer consumes CPU time, when the other thread releases the lock, the operating system will activate the suspended thread, let it be put into operation.

The "core design and implementation linux" often mentioned two kinds of states, one is the kernel state or a user state, for the spin-lock, the lock so that spin in user mode threads, and the need to reassign mutex proceeds to kernel mode. Here we have the kernel mode and user mode preliminary awareness on the line, relatively light user mode, kernel mode heavy. User mode and kernel mode linux this is the essential knowledge base, learn from this, can be optimized on a lot of programming language API, for example javaio on the part of the operation io time, first from user mode into kernel mode , then the operation to core state abstract input and output devices, where reduced user mode kernel mode conversion part is optimized new io, talk to you later.

The wiki defined as follows:

Computer science spin lock A lock for multi-thread synchronization, thread repeatedly checks the lock variable is available. As the thread remains in the implementation of this process, it is a busy waiting.

Spinlocks avoid process context scheduling overhead, so for a very short time the thread will only clog the occasion is valid. So the operating system is implemented in many places tend to use spin locks. Windows operating system provides light to read and write lock (SRW Lock) on the inside with a spin lock. Clearly, the single-core CPU is not suitable for use spin locks, where the single-core CPU refers to a single-core single-threaded CPU, because, at the same time only one thread is running in the state, found it impossible to assume that A running thread to acquire the lock, We can only wait to unlock, but not suspended because a itself, so that the thread holding the lock B is no way to enter the running state, can only wait until the operating system and give a time slice runs out, in order to have the opportunity to be scheduled. Use high spin lock in this case the price. (Red part is I edit wiki entries, single-core CPU is not suitable for spin locks, this is only the case for single-core single-threaded, and now the technology is basically a single-core support multithreading)

Why use spin lock
mutex has a disadvantage in his execution flow is managed code - user mode code - the code kernel mode, and loss of context switching overhead, if the acquired resource lock thread A logic processed immediately released resource lock out, if it is taken mutually exclusive manner, the thread B to the lock is not acquired from the process to acquire the lock, the user must scheduling mode and kernel mode, and the overhead of context switching loss. So there will be a spin lock mode, so that it is user-mode thread B waiting in circulation, reduce consumption.

Spinlock more suitable for the case of a user holding the lock latch in a relatively short time, in this case of spin lock is much higher than the efficiency of the mutex.

Spinlock potential problems may
take up too much CPU resources if thread A lock holder has been a long time holding the lock handle their own logic, then the thread loop B would have been waiting for over-occupancy cpu resources
recursive use may deadlock, but this scenario is generally not write
CAS
Nimbus defined terms, this simply means that CAS is defined by the operating system, composed of a number of instructions, the operations are atomic, the instructions, if executed, will be fully implemented, it has not been interrupted. CAS has three operands, memory value V, the expected value of the old A, to be modified a new value B. If and only if the expected value of the A and V are the same memory value, the memory value V revised to B, or do nothing.

CAS issue of
the ABA problem classic CAS, the above mentioned operation when the CAS, to detect the value has not changed, if a value is A, and later became a B, and later became A, CAS would think that did not happen Variety.
       solution:

       The version number plus 1. 1A - 2B - 3A

        2. For java, jdk1.5 provides AtomicStampedReference to solve this problem

A shared variable can only guarantee atomic operations 
        CAS is usually a variable atomic operation so if multiple variables atomic operations will be a problem.

        solution

        1. simple and crude, lock, but added complexity, the way most low

        2. With the above reasons plus the version number of the same, which is the number of variables makes up a variable (you can spell a string)

        3. For java, jdk1.5 provides AtomicStampedReference, this reference is a reference to the object, the number of variables in this object can be

The JAVA package CAS
 sun.misc.Unsafe inside JDK is an internal class that the operation of which has three of the CAS

JAVA applications spinlock
after JDK1.5, provided java.util.concurrent.atomic package, the package which provides a set of atomic categories. Basically the current thread to acquire the lock, a method of performing an update, other threads spin-wait approach is Unsafe methods such as internal getAndAdd atomicInteger class is actually used.

    / **
     * The atomically ADDS GIVEN The Current value to value.
     *
     * @Param The Delta value to the Add
     * @return The Previous value
     * /
    public int getAndAdd Final (int Delta) {
        return unsafe.getAndAddInt (the this, valueOffset, Delta );
    }
of course syncronized keyword in java, with a lot of optimization in 1.5, adding a partial gap was also called biased locking lock, the main way to achieve that is marked with a thread in the object header information in markword, on such resources the acquisition will tend to lock this thread, the back, the problem involves a series of lock escalation, lock gap - lightweight lock - heavyweight locks, lock escalation drawn back alone to write, this is actually a lightweight lock spin lock implementations are also used.

Original link: https: //blog.csdn.net/u010372981/article/details/814632

 

Simple spinlock (reentrant)

Spin lock means that when a thread tries to acquire a lock if the lock is held by other threads, has been circulating detect whether the lock is released, rather than into the thread suspend or sleep state.

Suitable for small spin lock lock to protect critical section, the critical section is small, then the lock takes very short time.

 

public class SpinLock implements Lock {
    /**
     *  use thread itself as  synchronization state
     *  使用Owner Thread作为同步状态,比使用一个简单的boolean flag可以携带更多信息
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();
    /**
     * reentrant count of a thread, no need to be volatile
     */
    private int count = 0;

    @Override
    public void lock() {
        Thread t = Thread.currentThread();
        // if re-enter, increment the count.
        if (t == owner.get()) {
            ++count;
            return;
        }
        //spin
        while (owner.compareAndSet(null, t)) {
        }
    }

    @Override
    public void unlock() {
        Thread t = Thread.currentThread();
        //only the owner could do unlock;
        if (t == owner.get()) {
            if (count > 0) {
                // reentrant count not zero, just decrease the counter.
                --count;
            } else {
                // compareAndSet is not need here, already checked
                owner.set(null);
            }
        }
    }
}

SimpleSpinLock there is a reference to the thread holding the lock owner property of the current owner, if the reference is null, it indicates that the lock is not occupied, not null were occupied.

Here with AtomicReference to use its atomic compareAndSet method (CAS operation), to solve the multi-threaded operation lead to inconsistent data problems, ensure that other threads can see the real condition of the lock.

Disadvantages:

  1. CAS operation requires complex hardware;
  2. Ensure that each CPU cache (L1, L2, L3, across the Socket CPU, main memory) of the data consistency, large communication overhead, and more severe in the multiprocessor system;
  3. Can not guarantee fairness, it does not guarantee the waiting process / thread to acquire a lock in FIFO order.

TicketLock

Ticket Lock to solve the fairness problem above, similar to line up bank counter in reality called numbers: The lock has a service number, meaning the thread being served, there is a queue number; each thread tries to acquire a lock before acquiring line number, and then continuously polls lock the current service number if this is their line number, and if so, said he has a lock, not continue polling.

When a thread releases the lock, the service number plus 1, so that the next thread to see this change, quit spin.

 

public class TicketLock implements Lock {
    private AtomicInteger serviceNum = new AtomicInteger(0);
    private AtomicInteger ticketNum = new AtomicInteger(0);
    private final ThreadLocal<Integer> myNum = new ThreadLocal<>();

    @Override
    public void lock() {
        myNum.set(ticketNum.getAndIncrement());
        while (serviceNum.get() != myNum.get()) {
        }
    }

    @Override
    public void unlock() {
        serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
        myNum.remove();
    }
}

Drawback:
Ticket Lock solves the problem of fairness, but on a multi-processor system, each process / thread consumes processor to read and write in the same variable serviceNum, each read and write operations have to be more processor cache be synchronized between the cache, which can lead to the system bus and memory heavy traffic, greatly reducing overall system performance.

CLH MCS locks and lock are described below in order to solve this problem.

CLHLock

CLH's inventor: Craig, Landin and Hagersten. The list is based on scalable, high-performance, fair spin locks, apply spin thread only on a local variable, it continuously polls the state of the precursor, if we find the precursor to release the lock on the end of a spin.

The CLH queue node locked QNode contains a field that indicates when true that the thread needs to acquire the lock, and does not release the lock, the thread false to release the lock. Between nodes connected by invisible chain, it was called invisible because there is no obvious chain next pointer between the nodes, but rather by influence the behavior myNode preNode node pointed to by the changes. There CLHLock on a tail pointer always points to the last node in the queue. CLHLock class diagram is shown below:

When a thread needs to acquire the lock, creates a new QNode, will be one of the locked set to true, need to acquire a lock, then thread tail domain call getAndSet way to become the tail of the queue, and get it before a point to trends spin locked on a reference field preNode, then the first thread chemotaxis node, predecessor node until the lock is released. When a thread needs to release the lock, the locked field current node is set to false, before becoming nodes while recovering. As shown in, FIG thread A requires the following to acquire the lock, which myNode field is true, the node pointed tail when some thread A, then thread B is also added to the rear of the thread A, the node pointed tail thread B. A and B are then threaded on its preNode domain rotation, once locked in its field preNode node becomes false, it can acquire the lock. preNode locked thread A significant field to false, this time A thread lock obtained.

To achieve the following:

 

public class CLHLock implements Lock {

    /**
     * 锁等待队列的尾部
     */
    private AtomicReference<QNode> tail;
    private ThreadLocal<QNode> preNode;
    private ThreadLocal<QNode> myNode;

    public CLHLock() {
        tail = new AtomicReference<>(null);
        myNode = ThreadLocal.withInitial(QNode::new);
        preNode = ThreadLocal.withInitial(() -> null);
    }

    @Override
    public void lock() {
        QNode qnode = myNode.get();
        //设置自己的状态为locked=true表示需要获取锁
        qnode.locked = true;
        //链表的尾部设置为本线程的qNode,并将之前的尾部设置为当前线程的preNode
        QNode pre = tail.getAndSet(qnode);
        preNode.set(pre);
        if(pre != null) {
            //当前线程在前驱节点的locked字段上旋转,直到前驱节点释放锁资源
            while (pre.locked) {
            }
        }
    }

    @Override
    public void unlock() {
        QNode qnode = myNode.get();
        //释放锁操作时将自己的locked设置为false,可以使得自己的后继节点可以结束自旋
        qnode.locked = false;
        //回收自己这个节点,从虚拟队列中删除
        //将当前节点引用置为自己的preNode,那么下一个节点的preNode就变为了当前节点的preNode,这样就将当前节点移出了队列
        myNode.set(preNode.get());
    }

    private class QNode {
        /**
         * true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁,且不需要锁
         */
        private volatile boolean locked = false;
    }
}

CLH lock queue advantage is the low complexity of space (if there are n threads, L locks, each thread only acquire a lock, then the required storage space is O (L + n), n has n threads myNode, L locks there are L tail), CLH a variant is used in a JAVA concurrent framework. The only disadvantage is the poor performance in the NUMA architecture, in such a system configuration, each thread has its own memory, if the predecessor nodes distant memory locations, chemotaxis Locked domain node before spin determination, performance will be greatly reduced, but in the SMP system configuration method is very effective. One solution ideas NUMA architecture is MCS queue lock.

MCSLock

MCS person's name from its invention initials: John Mellor-Crummey and Michael Scott. The list is based on scalable, high-performance, fair spin locks, apply spin thread only on a local variable, its direct precursor responsible for notifying the end of the spin, greatly reducing unnecessary processor cache synchronization frequency and reduced bus and memory overhead.

 

public class MCSLock implements Lock {
    private AtomicReference<QNode> tail;
    private ThreadLocal<QNode> myNode;

    public MCSLock() {
        tail = new AtomicReference<>(null);
        myNode = ThreadLocal.withInitial(QNode::new);
    }

    @Override
    public void lock() {
        QNode qnode = myNode.get();
        QNode preNode = tail.getAndSet(qnode);
        if (preNode != null) {
            qnode.locked = false;
            preNode.next = qnode;
            //wait until predecessor gives up the lock
            while (!qnode.locked) {
            }
        }
        qnode.locked = true;
    }

    @Override
    public void unlock() {
        QNode qnode = myNode.get();
        if (qnode.next == null) {
            //后面没有等待线程的情况
            if (tail.compareAndSet(qnode, null)) {
                //真的没有等待线程,则直接返回,不需要通知
                return;
            }
            //wait until predecessor fills in its next field
            // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
            while (qnode.next == null) {
            }
        }
        //后面有等待线程,则通知后面的线程
        qnode.next.locked = true;
        qnode.next = null;
    }

    private class QNode {
        /**
         * 是否被qNode所属线程锁定
         */
        private volatile boolean locked = false;
        /**
         * 与CLHLock相比,多了这个真正的next
         */
        private volatile QNode next = null;
    }
}

Compare CLH locks and lock MCS

CLH lock and lock queue shown MCS

Differences :

  1. 从代码实现来看,CLH比MCS要简单得多。
  2. 从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
  3. 从链表队列来看,CLHNode不直接持有前驱节点,CLH锁释放时只需要改变自己的属性;MCSNode直接持有后继节点,MCS锁释放需要改变后继节点的属性。
  4. CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性



链接:https://www.jianshu.com/p/824b2e4f1eed
 

发布了740 篇原创文章 · 获赞 65 · 访问量 10万+

Guess you like

Origin blog.csdn.net/qq_41723615/article/details/104348186