各种各样的锁

在JAVA环境中,线程Thread有如下几个状态:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。

《计算机操作系统》中对同步机制准则的归纳(P50):

  1. 空闲让进。当无进程处于临界区时,表明临界资源处于空闲状态,应允许一个请求进入临界区的进程立即进入自己的临界区,以有效的利用临界资源。
  2. 忙则等待。当已有进程进入临界区时,表明临界资源正在被访问,因而其他试图进入临界区的进程必须等待,以保证对临界区资源的互斥访问。
  3. 有限等待。对要求访问临界资源的进程,应保证在有限时间内能进入自己的临界区,以免陷入“死等”状态。
  4. 让权等待。当进程不能进入自己的临界区时,应该释放处理机,以免进程陷入“忙等”状态。

概念

1.可重入:

==========================================
C:可重入函数 这种情况出现在多任务系统当中,在任务执行期间捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理程序临时中断。如果从信号处理程序返回,
继续执行进程断点处的正常指令序列,从重新恢复到断点重新执行的过程中,函数所依赖的环境没有发生改变,就说这个函数是可重入的,反之就是不可重入的。 众所周知,在进程中断期间,系统会保存和恢复进程的上下文,然而恢复的上下文仅限于返回地址,cpu寄存器等之类的少量上下文,而函数内部使用的诸如全局或静态变量,
buffer等并不在保护之列,所以如果这些值在函数被中断期间发生了改变,那么当函数回到断点继续执行时,其结果就不可预料了。
比如malloc
,假如一个进程此时正在执行malloc分配堆空间,此时程序捕捉到信号发生中断,执行信号处理程序中恰好也有一个malloc,这样就会对进程的环境造成破坏,
因为malloc通常为它所分配的存储区维护一个链接表,插入执行信号处理函数时,进程可能正在对这张表进行操作,而信号处理函数的调用刚好覆盖了进程的操作,造成错误。
malloc:memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址。当无法知道内存具体位置的时候,
想要绑定真正的内存空间,就需要用到动态的分配内存。
malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿链表寻找一个大到足以满足用户请求所需要的内存块。
然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)
返回到链表上。
调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求
的片段了。于是,malloc函数请求延时,并开始在空闲链上检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,
malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。

满足下面条件之一的多数是不可重入函数: (
1)使用了静态数据结构; (2)调用了malloc或free; (3)调用了标准I/O函数;标准io库很多实现都以不可重入的方式使用全局数据结构。 (4)进行了浮点运算。许多的处理器/编译器中,浮点一般都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现)。
==========================================
java:可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
可重入锁最大的作用是避免死锁,比如:
public void lock(){
        Thread current = Thread.currentThread();
        while(!owner.compareAndSet(null, current)){
        }
    }
同一线程第二次调用lock就会死锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我(原作者)看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

2.公平

公平锁 —— 公平锁即尽量以请求锁的顺序来获取锁。
比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。 非公平锁无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁(饥饿)。 在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,
但是可以设置为公平锁:
2 public ReentrantLock() { 3 sync = new NonfairSync(); 4 } 7 public ReentrantLock(boolean fair) { 8 sync = (fair)? new FairSync() : new NonfairSync(); 9 }

3.悲观和乐观

在关系数据库管理系统里:

悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。指在具体操作数据前悲观的认为其他线程可能要对自己操作的数据进行修改。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作(否则阻塞)。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁(悲观锁)的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

4.排他和共享

共享锁(S锁):共享 (S) 用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。

又称读锁,若事务T对数据对象A加上共享锁(S锁),则事务T可以读A但不能修改A,其他事务只能再对A加共享锁(S锁),而不能加排他锁(X锁),直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。获准共享锁的事务只能读数据,不能修改数据。

排他锁(X锁):用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。

又称写锁。若事务T对数据对象A加上排他锁(X锁),事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

实现类型:

自旋锁(Spin lock)

自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

  1. // use thread itself as  synchronization state  
  2.     private AtomicReference<Thread> owner = new AtomicReference<Thread>();   
  3.     private int count = 0; // reentrant count of a thread, no need to be volatile 
  4.     public void lock() {  
  5.         Thread t = Thread.currentThread();  
  6.         if (t == owner.get()) { // if re-enter, increment the count.   为了可重入
  7.             ++count;
  8.             return;  
  9.         }  
  10.         while (!owner.compareAndSet(null, t)) {} //spin
  11.       //锁未被占据(owner=null),返回true,owner=t
  12.       //锁未被占据(owner=other)返回false,循环
  13.     }  
  14.     public void unlock() {  
  15.         Thread t = Thread.currentThread();  
  16.         if (t == owner.get()) { //only the owner could do unlock;  
  17.             if (count > 0) --count; // reentrant count not zero, just decrease the counter.  
  18.             else {  
  19.                 owner.set(null);// compareAndSet is not need here, already checked  
  20.             }  
  21.         }  
  22.     }

使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程,非公平锁。

支持可重入,判断是否是重入,重入不需要改变同步状态,而只需要计数。

class SpinLock {  
    // use thread itself as  synchronization state  
    private AtomicReference<Thread> owner = new AtomicReference<Thread>();   
    private int count = 0; // reentrant count of a thread, no need to be volatile  //reentrant这个修饰符用于把函数定义为可重入函数
    public void lock() {  
        Thread t = Thread.currentThread();  
        if (t == owner.get()) { // if re-enter, increment the count.  
            ++count;  //锁住的次数
            return;  
        }  
        while (owner.compareAndSet(null, t)) {} //spin  
      
//锁未被占据(owner=null),返回true,owner设为t
  
     //锁被占据(owner=other),返回false,循环 } public void unlock() { Thread t = Thread.currentThread(); if (t == owner.get()) { //only the owner could do unlock; if (count > 0) --count; // reentrant count not zero, just decrease the counter. else { owner.set(null);// compareAndSet is not need here, already checked } } } }

当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。

这在操作系统中被定义为整形信号量,然而整形信号量如果没拿到锁会一直处于“忙等”状态(没有遵循有限等待和让权等待的准则),因而这种锁也叫Spin Lock。由于自旋锁只是将当前线程不停地执行循环体(忙等),不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,因为每个线程都需要执行,占用CPU时间,性能下降明显。如果线程竞争不激烈,并且锁保护的临界区很小,适合使用自旋锁,CAS操作需要硬件的配合。

在自旋锁中 另有三种常见的锁形式:TicketLock ,CLHlock 和MCSlock

Ticket Lock

Ticket Lock 是为了解决上面的公平性问题,类似于现实中银行柜台的排队叫号:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮询。

import java.util.concurrent.atomic.AtomicInteger;
public class TicketLock {
   private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
   private AtomicInteger ticketNum = new AtomicInteger(); // 排队号
   public int lock() {
         // 首先原子性地获得一个排队号
         int myTicketNum = ticketNum.getAndIncrement();
         // 只要当前服务号不是自己的就不断轮询
        while (serviceNum.get() != myTicketNum) {
        }
        return myTicketNum;
    }
    public void unlock(int myTicket) {
        // 只有当前线程拥有者才能释放锁
        int next = myTicket + 1;
        serviceNum.compareAndSet(myTicket, next);
    }
}

多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

CLH自旋锁 :Craig, Landin, and Hagersten (CLH) locks

CLH lock is Craig, Landin, and Hagersten (CLH) locks, CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service.
The CLH lock is a scalable, high performance, fairness and spin lock based on the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.

CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

public class ClhSpinLock implements Lock {  
    AtomicReference<QNode> tail = new AtomicReference<QNode>(new QNode());  
   ThreadLocal
<QNode> myPred; ThreadLocal<QNode> myNode; public ClhSpinLock () {
     //初始化,tail=new,mynode=new,pre=null tail
= new AtomicReference<QNode>(new QNode()); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; myPred = new ThreadLocal<QNode>() { protected QNode initialValue() { return null; } }; } @Override public void lock() { final QNode qnode = myNode.get();//获取当前线程的本地变量myNode qnode.locked = true; QNode pred = tail.getAndSet(qnode);//设置当前qnode,返回pre的引用 myPred.set(pred); while (pred.locked) {
     //忙等 } }
 //ThreadLocal为每一个线程维护变量的副本
 //每个Thread中都维护一个ThreadLocalMap类型的threadLocals(储存Entry[],每个Entry键为线程对象,而值对应线程的变量副本。

public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }

    @Override  
    public void unlock() {  
        final QNode qnode = myNode.get();//获取当前线程的本地变量myNode
        qnode.locked = false;  
        myNode.set(myPred.get()); //当前线程myNode换成当前线程myPred
  
}

  
private static class QNode { 
    
private volatile boolean locked; 
  
}

  public static void main(String[] args) throws InterruptedException {  
          final ClhSpinLock lock = new ClhSpinLock ();  
          lock.lock();  
          for (int i = 0; i < 10; i++) {  
            new Thread(new Runnable() {  
              @Override  
              public void run() {  
                lock.lock();  
                System.out.println(Thread.currentThread().getId() + " acquired the lock!");  
                lock.unlock();  
              }  
            }).start();  
            Thread.sleep(100);  
          }  
          System.out.println("main thread unlock!");  
          lock.unlock();  
    }  }

另一种实现:

 

public class CLHLock {
    public static class CLHNode {
        private boolean isLocked = true; // 默认是在等待锁
    }

    @SuppressWarnings("unused" )
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(CLHLock.class, CLHNode .class , "tail" );

    public void lock(CLHNode currentThreadCLHNode) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThreadCLHNode); // 转载人注释: 把this里的"tail" 值设置成currentThreadCLHNode
        if(preNode != null) {//已有线程占用了锁,进入自旋
            while(preNode.isLocked ) {
            }
        }
    }

    public void unlock(CLHNode currentThreadCLHNode) {
        if (!UPDATER .compareAndSet(this, currentThreadCLHNode, null)) {
            //当前线程不是tail
            currentThreadCLHNode. isLocked = false ;// 改变状态,让后续线程结束自旋
        }
    }
}

 

CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH的一种变体被应用在了JAVA并发框架中。

缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的。

一种解决NUMA系统结构的思路是MCS队列锁

public class MCSLock {
    public static class MCSNode {
        MCSNode next;
        boolean isLocked = true; // 默认是在等待锁
    }

    volatile MCSNode tail;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(MCSLock.class, MCSNode. class, "tail" );

    public void lock(MCSNode currentThreadMcsNode) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThreadMcsNode);// step 1 当前线程变给tail赋值,返回前一个node
        if (predecessor != null) {
            predecessor.next = currentThreadMcsNode;// step 2 前一个的下一个是当前node
            while (currentThreadMcsNode.isLocked ) {// step 3 iLocked由前驱负责通知
            }
        }
    }

    public void unlock(MCSNode currentThreadMcsNode) {
        if ( UPDATER.get( this ) == currentThreadMcsNode) {//
            if (currentThread.next == null) {// 检查是否有人排在自己后面
                if (UPDATER.compareAndSet(this, currentThreadMcsNode, null)) {// step 4
                    // compareAndSet返回true表示确实没有人排在自己后面
                    return;
                } else {
                    // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                    // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                    while (currentThreadMcsNode.next == null) { // step 5
                    }
                }
            }
       //直接前驱负责通知其结束自旋
            currentThreadMcsNode.next.isLocked = false;
            currentThreadMcsNode.next = null;// for GC
        }
    }
}

从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋;CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性

从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的。

阻塞锁

阻塞锁的优势在于,阻塞的线程不会占用cpu时间,不会导致 CPU占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下 阻塞锁的性能要明显高于自旋锁。理想的情况则是; 在线程竞争不激烈的情况下使用自旋锁,竞争激烈的情况下使用阻塞锁。

在JDK6以后提供了LockSupport.park()/LockSupport.unpark()操作,可以将当前线程放入一个等待列表或将一个线程从这个等待列表中唤醒。这个park/unpark的等待列表是一个全局的等待列表,在unpartk的时候需要提供要唤醒的Thread对象。

package lock;  
  
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;  
import java.util.concurrent.locks.LockSupport;  
  
public class CLHLock {  
    public static class CLHNode {  
    
private volatile Thread isLocked; }
  
private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<CLHNode>(); //储存当前线程变量CLHNode

/**
 * A reflection-based utility that enables atomic updates to --基于反射
 * designated {@code volatile} reference fields of designated  --volatile修饰符
 * classes.  This class is designed for use in atomic data structures
 * in which several reference fields of the same node are
 * independently subject to(受支配,从属于) atomic updates. For example, a tree node
 * might be declared as
 *
 * <pre> {@code
 * class Node {
 *   private volatile Node left, right;
 *
 *   private static final AtomicReferenceFieldUpdater<Node, Node> leftUpdater =
 *     AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "left");
 *   private static AtomicReferenceFieldUpdater<Node, Node> rightUpdater =
 *     AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "right");
 *
 *   Node getLeft() { return left; }
 *   boolean compareAndSetLeft(Node expect, Node update) {
 *     return leftUpdater.compareAndSet(this, expect, update);
 *   }
 *   // ... and so on
 * }}</pre>
 *
 * <p>Note that the guarantees of the {@code compareAndSet}
 * method in this class are weaker than in other atomic classes.
 * Because this class cannot ensure that all uses of the field
 * are appropriate for(适当的) purposes of atomic access, it can
 * guarantee atomicity only with respect to other invocations of
 * {@code compareAndSet} and {@code set} on the same updater.
 *
 * @since 1.5
 * @author Doug Lea
 * @param <T> The type of the object holding the updatable field
 * @param <V> The type of the field
 */
   private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");
@SuppressWarnings("unused")//unused这个参数是屏蔽:定义的变量在代码中并未使用且无法访问。 private volatile CLHNode tail;
//java在编译的时候会出现这样的警告,加上这个注解之后就是告诉编译器,忽略这些警告,编译的过程中将不会出现这种类型的警告

public void lock() { CLHNode node = new CLHNode(); LOCAL.set(node);
     * @param obj An object whose field to get and set
    * @param newValue the new value
    * @return the previous value CLHNode preNode
= UPDATER.getAndSet(this, node); //LOCAL-node存入tail,isLocked 为null if (preNode != null) { //已有线程占用了锁 preNode.isLocked = Thread.currentThread(); //prenode的islocked是当前线程(即prenode的下一个进程):迁移线程锁当前线程
     //
unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。但是这个“许可”是一次性的,且unpark函数可以先于park调用    
     //
比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。
/**
* Disables the current thread for thread scheduling purposes unless the
* permit is available.
*
* <p>If the permit is available then it is consumed and the call returns
* immediately; otherwise
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant(休眠的) until one of three things happens:
*
* <ul>
* <li>Some other thread invokes {@link #unpark unpark} with the
* current thread as the target; or
*
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
//当对处于阻塞状态的线程调用interrupt方法时,会抛出InterruptException异常,而这个异常会清除中断标记
* the current thread; or
*
* <li>The call spuriously(形似而实非的,不正确的) (that is, for no reason) returns.
* </ul>
*
* <p>This method does <em>not</em> report which of these caused the
* method to return. Callers should re-check the conditions which caused
* the thread to park in the first place. Callers may also determine,
* for example, the interrupt status of the thread upon return.
*
* @param blocker - the synchronization object responsible for this
* thread parking
* @since 1.6
*/
LockSupport.park(this); //阻塞,等待unpark;代码内部实现逻辑如下:
       //Sets the value of the object field at the specified offset(指定偏移量
       //in the supplied object(currentThread) to the given value(此处是CLHLock的实例).
       //park(native) :UNSAFE.park(false, 0L);
       //再把当前线程指定偏移量处field的值设为null preNode
= null; LOCAL.set(node); } } public void unlock() {
    CLHNode node = LOCAL.get();
  * @param obj An object whose field to conditionally set
  * @param expect the expected value
  * @param update the new value
  * @return {@code true} if successful
    
if (!UPDATER.compareAndSet(this, node, null)) {
       //解锁的thread不是最后一个,不是tail(那么node.isLocked!=null) System.out.println(
"unlock\t" + node.isLocked.getName());
/**
* Makes available the permit for the given thread, if it
* was not already available. If the thread was blocked on(被封锁)
* {@code park} then it will unblock. Otherwise, its next call
* to {@code park} is guaranteed not to block. This operation
* is not guaranteed to have any effect at all if the given
* thread has not been started.
* @param thread - the thread to unpark
*/
LockSupport.unpark(node.isLocked); // 唤醒下一个thread
       //unpark(native) UNSAFE.unpark(Thread thread);
} node
= null; } }

 

 

[转]自旋锁、排队自旋锁、MCS锁、CLH锁 - http://blog.csdn.net/fei33423/article/details/30316377

可重入锁 公平锁 读写锁、CLH队列、CLH队列锁、自旋锁、排队自旋锁、MCS锁、CLH锁-https://www.cnblogs.com/duanxz/p/6244045.html

可重入锁-http://www.cnblogs.com/duanxz/p/6705886.html

java锁的种类以及辨析(转载)- https://www.cnblogs.com/chenying99/p/4307668.html

几种自旋锁SpinLock,TicketLock,CLHLock,以及可重入实现要点,非阻塞锁实现要点 - http://blog.csdn.net/binling/article/details/50419103

实现自己的Lock对象 - http://www.it610.com/article/3686461.htm

posted @ 2018-03-12 16:34  wzbin  阅读(567)  评论(0编辑  收藏  举报