ReentrantLock解析,lock与unlock方法分析

介绍ReentrantLock之前,先介绍下背景知识,也就是要用到的知识点。这些知识点包括:比较并交换CAS(Compare And Swap )、ReentrantLock的类结构(其父类,内部类等)

声明:

我主要是通过一种通俗的语言进行内容的总结,帮助大家更好的理解,记忆,更容易去理解书上的讲解。对于一些专业的陈述,大家还是需要去看书。

1、CAS

举例说明。对于如下类

public class Blog {
	private int count;

	public int getCount(){
		return count;
	}
	public synchronized int increCount(){
		return ++count;
	}
}

这里有10个线程都要对变量count进行加1的操作。这种情况下我们会对其进行并发控制。也就是加synchronized关键字或者使用ReentrantLock对象的lock方法进行加锁,如increCount()方法。而CAS并不需要使用这些,就可以保证变量a最后结果的正确性。

其实CAS指的是sun.misc.Unsafe这个类中的一些方法的统称。例如,Unsafe这个类中有compareAndSwapInt、compareAndSwapLong等方法。

CAS的过程是:它包含了3个参数CAS(O,V,E,N)。O表示要更新的对象。V表示指明更新的对象中的哪个变量,E是进行比较的值,如果V==E,则将N赋值给V。

这里以JDK中的AtomicInteger类来进行分析。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    private static final Unsafe unsafe = Unsafe.getUnsafe(); //这里是初始化一个Unsafe对象。因为CAS是这个类中的方法。
    private static final long valueOffset;

    static {
      try {

    	  /*一个java对象可以看成是一段内存,各个字段都得按照一定的顺序放在这段内存里,
    	  同时考虑到对齐要求,可能这些字段不是连续放置的,用这个方法能准确地告诉你某个
    	  字段(也就是下面的value字段)相对于对象的起始内存地址的字节偏移量,因为是相对
    	  偏移量,所以它其实跟某个具体对象又没什么太大关系,跟class的定义和虚拟机的内
    	  存模型的实现细节更相关。通俗一点就是在CAS(O,V,E,N)中,O是你要更新那个对象,
    	  V就是我要通过这个偏移量找到这个对象中的value对象,来对他进行操作。*/

        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
      } catch (Exception ex) { throw new Error(ex); }
    }
    //volatile,保证变量的可见性。
    private volatile int value;

    //有参构造
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    public AtomicInteger() {
    }

    public final int get() {
        return value;
    }

    public final int getAndIncrement() {
    	//为什么会无限循环,后面的方法中会说明
        for (;;) {
            int current = get(); //得到当前的值
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

    public final boolean compareAndSet(int expect, int update) {
    /*this指的是该对象,valueOffset就是前面讲的偏移量,找到偏移量对应的值,也就是value变量
          对应的值,如果except等于这个value,则将value更改为update。可能这大家会有个问题是,如果我两个
          线程同时读,expect的值一样,都等于value,这样两个线程进行判断的时候value都等于except,都将update赋值给value
          ,结果会造成最后的结果不正确。其实这个方法是native方法,同时也是同步的,一个线程进行操作,另一个线程是需要
          等待的,虽然两个线程的expect的值一致,但是当第二个线程再调用这个方法时,value的值已经发生了变化。
        此时会返回false,所以在上述的getAndIncrement()方法中会使用for进行无线循环。 */
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

}

2、ReentrantLock的类结构

ReentrantLock实现了Lock接口,而Lock接口主要提供了以下方法:

   void lock();
   void lockInterruptibly() throws InterruptedException;
   boolean tryLock();
   boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
   void unlock();
   Condition newCondition();

ReentrantLock类有一个静态内部抽象类Sync,这个Sync继承AbstractQueuedSynchronizer类,AbstractQueuedSynchronizer类继承自AbstractOwnableSynchronizer。

而ReentrantLock类中还有两个静态内部类,分别为NonfairSync(非公平锁)和FairSync(公平锁)。在AbstractQueuedSynchronizer抽象类中有一个静态内部类Node,这个Node就是存放等待线程的节点。

3、分析lock()方法

ReentrantLock类有一个成员变量为private final Sync sync; ReentrantLock的lock()、unLock()等方法,调用的都是sync的一些方法。如下:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = (fair)? new FairSync() : new NonfairSync(); //构造时指定为公平锁还是非公平锁
    }

    public void lock() {
        sync.lock();
    }

    public void unlock() {
        sync.release(1);
    }
}

公平锁与非公平锁的区别在于:如果有10个线程,第1个线程获取了锁,紧接着有8个线程已经处在了等待释放锁。如果是非公平锁的话,第10个线程可以尝试直接获取锁,有可能是第1个线程释放锁了以后第10个直接就获取了。而公平锁就是,第10个线程必须等待前面的线程都获取锁,并释放,才可以获取锁,先进先出的意思。

这里以非公平锁来进行分析。

当调用lock()方法是,也就是执行了sync.lock()方法,如下是代码:

       final void lock() {
            if (compareAndSetState(0, 1)) //首先尝试获取锁。该方法在下面有说明
                setExclusiveOwnerThread(Thread.currentThread());//如果获取锁,则将当前线程赋值给exclusiveOwnerThread变量。
             else
                acquire(1);//如果失败,则调用此方法。继续往下看这个方法。
        }
    //   stateOffset = unsafe.objectFieldOffset
    //(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
    protected final boolean compareAndSetState(int expect, int update) {
        //这个stateOffset对应的是Sync的父类中的state字段,这个字段用来标识是否有线程已经获取锁
    	//如果有的话,state>0,否则state==0;所以如果expect==state==0,那么就获取锁,将
    	//state置为1
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    public final void acquire(int arg) {
    	//先尝试获取锁,如果获取锁失败了,也就是!tryAcquire(arg)为true则执行
    	//后面的方法。注意&&,前面为true后面才执行。获取锁失败后,会将该线程加入等待队列
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//我们是以非公平锁来讲解的。
        }
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) { //状态为0,说明没有线程获取锁。尝试获取锁
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //如果当前线程为exclusiveOwnerThread变量中存储的线程,说明当前线程已经
        //获取了锁,这里是当前线程再次要获得锁,所以state要继续+1。
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        //获取锁失败,返回false。
        return false;
    }

讲完了tryAcquire(arg)方法,紧接着讲 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这两个方法。

    private Node addWaiter(Node mode) {
    	//当前线程节点,可以看看Node类中的一些变量和方法,比较简单。
        Node node = new Node(Thread.currentThread(), mode);

        Node pred = tail;
        //判断有没有尾节点(也就是前面是否有等待线程)。如果有尾节点,则将当前线程的节点插入到队列的尾部
        //也就是将当前线程变成尾节点。
        if (pred != null) {
            node.prev = pred;
            //CAS的操作。
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果没有尾节点,说明前面还未有等待线程。调用下面的方法,创建等待队列
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
    	//enq方法主要是创建头,尾节点,也就是等待队列,下面都会用的CAS同步操作,加上
    	//for循环,防止两个线程同时创建头尾节点。代码比较容易读懂
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                Node h = new Node(); // Dummy header
                h.next = node;
                node.prev = h;
                if (compareAndSetHead(h)) {
                    tail = node;
                    return h;
                }
            }
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

addWaiter方法分析完了,紧接着就是acquireQueued(addWaiter(Node.EXCLUSIVE), arg))中的acquireQueued方法。

    //这个方法是不断地获取锁,直到成功的获取锁
    final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
            	//得到当前节点的上一个节点
                final Node p = node.predecessor();
                //p==head意思是如果上一个节点为头节点,才尝试获取锁,为什么呢?
                //因为公平锁也会调用这个方法,这里要保证公平,只有当前线程节点在队列的第一个
                //才可以获取锁。
                if (p == head && tryAcquire(arg)) {
                	//如果成果获得锁,则将当前节点变成头结点。因为获得锁,所以当前节点要移出等待
                	//队列,变成了头结点。
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                //第一个方法是如果获取失败,是否要对当前线程进行阻塞(后面有分析)。防止线程一直进行for循环。
                //&&后面的方法是对当前线程进行阻塞并且判断是否中断。这里注意的是,如果一个线程在
                //等待锁期间这个线程被中断了,这里会将interrupted赋为true,但是并不return。这个
                //还一直进行for循环,知道这个线程获得了锁,所以lock()方法不能立即响应中断,必须等线程
                //获得了锁才可以响应中断。对应的可以立即响应中断的方法为lockInterruptibly()方法
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
    }
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //这个pre是当前节点(node)的上一个节点。waitStatus有4个状态,这个方法后有说明
    	int s = pred.waitStatus;
    	//小于0,也就是SIGNAL状态,或者是CONDITION状态
        if (s < 0)
            return true;
      //大于0,也就是CANCLE状态。
        if (s > 0) {
	    do {
		node.prev = pred = pred.prev;/
	    } while (pred.waitStatus > 0);
	    pred.next = node;
    }
        else
        	//这里为什么要将当前节点的父节点的waitStatus设置成-1呢,这是因为
        	//在unLock()方法中会根据当前节点的waitStatus来判断后续是否还有节点。
        	//如果当前的waitStatus==-1,说明有后续节点对其进行了设置。后续节点
        	//处在阻塞状态。后面会对unLock()方法进行分析。
            compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
        return false;
    }
        static final int CANCELLED =  1;//这个状态说明该节点已经被取消。

        static final int SIGNAL    = -1;//这个状态说明该节点后续有阻塞的节点

        static final int CONDITION = -2;//这个状态跟Condition有关,也就是线程之间通过Condition方法进行通信。我会在接下的博客中写有关Condition的文章
   private final boolean parkAndCheckInterrupt() {
    	//阻塞当前线程
        LockSupport.park(this);
        return Thread.interrupted();
    }

4、unLock()方法

    public void unlock() {
        sync.release(1);
    }
    public final boolean release(int arg) {
    	//tryRelease(arg)是将state标志位置为0,用来让其他线程获取锁。
        if (tryRelease(arg)) {
            Node h = head;
            //如果h.waitStatus不等于0,说明后面有线程再等待,前面shouldParkAfterFailedAcquire()
            //方法有说明。
            if (h != null && h.waitStatus != 0)
            	//这个方法后面有分析。主要是将头结点的waitStatus置为0,同时通知下一个节点。
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    private void unparkSuccessor(Node node) {

    	/**
    	 * 将头节点的waitStatus状态置为0
    	 */
        compareAndSetWaitStatus(node, Node.SIGNAL, 0);

        /**
         * 获取下一个节点,如果下一个节点被取消或者为空,则从尾部还是查找,找到第一个可用的线程
         * 。
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	//通知该节点。
            LockSupport.unpark(s.thread);
    }

5、Lock VS Sychronized(引用别人博文上的)

AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。

synchronized 的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优 化,而Lock则完全依靠系统阻塞挂起等待线程。

当然Lock比synchronized更适合在应用层扩展,可以继承 AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对 应的Condition也比wait/notify要方便的多、灵活的多。

6、总结

以上主要分析了从加锁到解锁的过程,公平锁跟这个类似,读懂这个,其他的方法比如lockInterruptibly(),也很容易明白,如果有不明白可以留言,或者有什么建议,我写的不对的地方,都可以告诉我,我们共同成长。

时间: 2024-08-29 03:52:07

ReentrantLock解析,lock与unlock方法分析的相关文章

SAX方式解析XML文件的方法分析

SAX(Simple API for XML)SAX的工作原理简单地说就是对文档进行顺序扫描,当扫描到文档(document)开始与结束.元素(element)开始与结束.文档(document)结束等地方时通知事件处理方法,由事件处理函数做相应动作,然后继续同样的扫描,直至文档结束.SAX解析方式适用于大型文档,因为他的解析是逐行进行不用像DOM中那样为所有节点创建对象,这样效率大大提高,所以虽然它不是W3C标准,但它却得到了广泛认可. 这是一个需要解析的XML文件: <?xml versio

【Java并发编程实战】—–“J.U.C”:ReentrantLock之二lock方法分析

前一篇博客简介了ReentrantLock的定义和与synchronized的差别,以下尾随LZ的笔记来扒扒ReentrantLock的lock方法.我们知道ReentrantLock有公平锁.非公平锁之分,所以lock()我也已公平锁.非公平锁来进行阐述.首先我们来看ReentrantLock的结构[图来自Java多线程系列–"JUC锁"03之 公平锁(一)]: 从上图我们能够看到,ReentrantLock实现Lock接口.Sync与ReentrantLock是组合关系,且Fair

ReentrantLock 的公平锁源码分析

ReentrantLock 源码分析   以公平锁源码解析为例: 1:数据结构: 维护Sync 对象的引用:   private final Sync sync; Sync对象继承 AQS,  Sync  分为两个类:处理公平锁锁和非公平锁: FairSync   NonfairSync 具体的类图如下: 2:接下来重点分析AQS这个类:AbstractQueuedSynchronizer: AQS中的成员变量: private transient volatile Node head;   /

Java中lock上锁 unlock解锁

代码很简单,本宝宝就直接上代码了,请喷吧! /** *jdk1.5之后出现了lock,创建锁不那么繁琐,调用lock上锁 unlock解锁,更加面向对象 */public class LockTest { public static void main(String[] args) { //运用内部类的方法实现runnable new Thread( new Runnable() { @Override public void run(){ //调用out方法中的print方法 out pOut

VMWARE虚拟机不能上网三种方法分析

bridged(桥接模式). NAT(网络地址转换模式) host-only(主机模式). 理论理解: 1.bridged(桥接模式) 在这处模式下,虚拟机等同于网络内的一台物理主机,可对手动设置IP,子网掩码,DNS,且IP地址要和主机的IP在同一网段内.这样,虚拟机就和主机如同连在一个HUB上的两台计算机,只要主机能上网,虚拟机也会在这种模式下上网. 2.host-only(主机模式) 在host-only模式中,所有的虚拟系统是可以相互通信的,但虚拟系统和真实的网络是被隔离开的. 提示:在

http数据流 gzip解压方法分析

gzip压缩的http数据流都包含 “Content-Encoding: gzip\r\n\r\n” : 紧跟其后的是压缩后的数据长度,占三个字节,然后是 \r\n ,占两个字节: 再往后就是压缩数据了,可以直接将 压缩数据 copy到文件里用gz解压缩软件可以直接解压缩. 在代码中只需要调用对应的解压缩接口应该就可以解压缩了,后续感兴趣的可以尝试一下. ps: 分析数据包建议可采用wireshark协议分析软件,自己设计的协议可以用lua语言编写解析脚本进行测试工作也挺方便的. http数据流

【Android 应用开发】 自定义组件 宽高适配方法, 手势监听器操作组件, 回调接口维护策略, 绘制方法分析 -- 基于 WheelView 组件分析自定义组件

博客地址 : http://blog.csdn.net/shulianghan/article/details/41520569 代码下载 : -- GitHub : https://github.com/han1202012/WheelViewDemo.git -- CSDN : http://download.csdn.net/detail/han1202012/8208997 ; 博客总结 : 博文内容 : 本文完整地分析了 WheelView 所有的源码, 包括其适配器类型, 两种回调接

jQuery方法源码解析--jQuery($)方法(一)

jQuery方法源码解析--jQuery($)方法 注: 1.本文分析的代码为jQuery.1.11.1版本,在官网上下载未压缩版即可 2.转载请注明出处 jQuery方法: 这个方法大家都不陌生,在使用过程中,它还有另外一个名字,美元符号:$,$(...)其实就是jQuery(...); 它有很多种用法,通常都返回一个jquery对象,也可以作为$(document).ready(...);的简写形式,分析之前先看一下jQuery都有什么用法. 1.jQuery( selector [, co

Android程序解析XML文件的方法及使用PULL解析XML案例

一.一般解析XML文件的方法有SAX和DOM.PULL (1)DOM(JAXP Crimson解析器) DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准.DOM是以层次结构组织的节点或信息片断的集合.这个层次结构允许开发人员在树中寻找特定信息.分析该结构通常需要加载整个文档和构造层次结构,然后才能做任何工作.由于它是基于信息层次的,因而DOM被认为是基于树或基于对象的.DOM 以及广义的基于树的处理具有几个优点.首先,由于树在内存中是持久的,因此可以修改它以便应用程序能对数据和结构