zoukankan      html  css  js  c++  java
  • B9 Concurrent 重入锁(ReentrantLock)

    概述

      java.util.concurrent.locks.ReentrantLock 实现 java.util.concurrent.locks.Lock 接口,加锁(lock)和 解锁(unlock)方法都基于 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)实现,AQS 是基于 sun.misc.Unsafe 类的 CAS算法相关方法实现的。 

    代码实例

    import java.util.concurrent.locks.ReentrantLock;
    
    public class Main {
    
    	public static void main(String[] args) {
    		Command c = new Command();
    		int nThreads = 5;
    		for(int i = 0; i < nThreads; i++){
    			new Thread(c).start();
    		}
    	}
    
    }
    
    class Command implements Runnable {
    	
    	ReentrantLock lock = new ReentrantLock();
    	
    	@Override
    	public void run() {
    		try{
    			lock.lock();
    			System.out.println(Thread.currentThread().getName() + ": 获得锁!");
    		}catch(Exception e){
    			//处理异常
    		}finally{
    			System.out.println(Thread.currentThread().getName() + ": 释放锁!");
    			lock.unlock();
    		}
    	}
    }
    

    打印结果: 一个线程独占锁(排他锁),释放锁后其他的线程才能获得锁。

    ReentrantLock 初始化

      1). 首先来看下 ReentrantLock 的属性定义:

      

      ReentrantLock 的功能主要依靠定义的这个 Sync 类型的变量 sync 来实现。Sync 是 ReentrantLock 创建的一个静态内部类,它继承了 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer),即 ReentrantLock 的主要功能是依靠 AQS 实现的。

      2). ReentrantLock 的构造器

      

      ReentrantLock 提供了两种构造器,主要功能是对变量 sync 进行初始化,提供了 FairSync 和 NonFairSync两种实现,从命名可以看出分别对应公平锁和非公平锁的实现;其中 fair 为 true 时初始化为 FairSync(公平锁),fair 为 false 时初始化为 NonfairSync(非公平锁);无参构造器默认初始化为 NonfairSync。所谓 “公平” 在于是否按照线程申请锁的顺序获得锁,FairSync(公平锁)使用队列实现首先申请锁的先获得锁(FIFO),增加了额外的队列操作开销;相对而言,NonfairSync(非公平锁)的并发效能更高,吞吐量更高。

     【AQS 初始化

      1). AQS 的实现依靠 sun.misc.Unsafe 类中 CAS 算法相关的方法,故这里初始化了 Unsafe 对象 unsafe,以及相关变量的内存偏移量(offset),用于后面的 CAS 操作。

      

      2). AQS 属性介绍:AQS 使用双向链表的数据结构存储请求锁的线程,所以设计了一个节点类 Node,Node 类除了双向链表的前继指针(volatile Node prev)和后继指针(volatile Node next),存储数据包括 线程对象(volatile Thread thread)、等待状态(volatile int waitStatus)、下一个等待指针(Node nextWaiter)。一个节点表示一个线程。

      下面是一个关于 waitStatus 的值说明:

      AQS 除了双向链表的首节点(volatile Node head) 和 尾节点(volatile Node tail)外,还有一个状态属性(volatile int state)用于表示锁的状态。如果当前没有线程获得锁,state 的值为 0;若当前已经有一个线程获得锁,由于锁可重入,每获得一次锁加数加 1。

      下面代码展示了重入锁的实现,打印了线程重新获得同一把锁时,state 的值变化。

    import java.util.concurrent.locks.ReentrantLock;
    
    public class Main{
        public static void main(String[] args){
        	Command c = new Command();
            int nThreads = 5;
            for(int n = 0; n < nThreads; n++){
                new Thread(c).start();
            }
        }
    }
     
    class Command implements Runnable{
    	
    	ReentrantLock lock = new ReentrantLock();
    	
    	@Override
    	public void run() {
    		try{
    			lock.lock();
    			run2();
    		}catch(Exception e){
    			
    		}finally{
    			lock.unlock();
    		}
    	}
    	
    	public void run2(){
    		try{
    			lock.lock();
    			System.out.println(Thread.currentThread().getName() + ":" + lock.getHoldCount());
    		}catch(Exception e){
    			
    		}finally{
    			lock.unlock();
    		}
    	}
    	
    }
    

      打印结果:

      

    • getHoldCount 方法返回了当前线程获得锁的数量,如果当前线程没有获得锁则返回 0。可以看到这里调用的是变量 sync 的 getHoldCount() 方法。

     

    • 这里需要判断当前线程是否获得锁,即是否为独占锁的线程,是则调用 getState() 方法返回获得锁的数量,否则返回 0。

     

    • AQS 继承了 java.util.concurrent.locks.AbstractOwnableSynchronizer,该类存储了独占锁的线程的值。

      

           

    • AQS 中的 getState() 方法返回的就是 state 的值。 

       

    加锁操作 lock()

    • lock() 的用途是当前线程尝试去获得锁,如果当前锁没有被其他线程持有,则当前线程可以立即获得锁,设置持有锁数量为 1;如果当前线程已经持有锁,再次调用 lock() 方法时会在原来持有锁数量上加 1;如果锁被其他线程持有,则当前线程保持休眠状态直到锁的持有数被设置为 1 的时候,才会被激活尝试去获得锁。

     

      

      首先来看下,NonfairSync(非公平锁)的实现:

      

    • compareAndSetState(0, 1) 是去获得锁,如果返回 true, 则获得锁成功,否则获得锁失败。这个方法是 AQS 中的方法,使用 Unsafe 类的 CAS 方法 compareAndSwapInt 尝试将 state 的值从 0 更新为 1。如果当前没有任何线程持有锁,state 为 0,则 CAS 成功,返回 true。注意 state 使用 volatile 修饰,state 的值被当前线程修改后,立即从工作内存刷新回主内存,其他线程可见最新的修改。

     

             setExclusiveOwnerThread(Thread.currentThread());  则是将当前线程设置独占锁的线程。

    • 如果锁已经被持有(state != 0),持有线程可能是当前线程,也可能是其他线程。执行 acquire(1);

    • tryAcquire 方式是锁已经被持有的情况下,尝试去获得锁。

     

      获取锁的最新状态,通过是否为 0 判断锁是否被持有,如果没有被持有,跟上面一样的操作,通过 CAS 操作尝试去获得锁,如果获得锁成功则设置当前线程为独占锁的线程,返回 true;如果当前锁已被持有,判断独占锁的线程是否为当前线程,为当前线程则累加锁的数量,修改 state 变量,返回 true。例如一个线程持有锁的数量为 5,则 state 的值为 5。如果当前线程无法获得锁,则返回 false。

    • 如果 tryAcquire 返回 false, 则添加到独占锁等待队列中(addWaiter(Node.EXCLUSIVE))

       新建了一个 Node 节点 node,设置线程为当前线程,模式为独占锁(Node.EXCLUSIVE)

       这里创建了一个 CHL 队列锁,队列中的元素通过自旋操作尝试去获得锁。在队列中的节点按照 FIFO 的规则,越先进入队列的节点对应的线程越先获得锁。

      

     

     

    • 创建完 Node 节点 node 并加入 CHL 队列后,尝试通过队列获得锁。这里是一个自旋操作,判断当前节点 node 的前继节点(node.prev)是否为 head 节点,如果是,则使用 tryAcquire() 方法去尝试获得锁。

      

    • 成功获得锁后,通过 setHead(node) 将 head 节点设置为 node,并将 node 节点的 prev(前继节点) 和 thread(存储线程)设置为 null。设置 failed 变量为 false,并将 interrupted(是否中断)返回。

    • 每一次自旋操作,如果获取锁失败,则线程可以shouldParkAfterFailedAcquire 判断 pred (node 的前继节点)是否已经被阻塞。

      Node.SIGNAL : 值为 -1,waitStatus(等待状态) 设置为这个值说明节点线程已经要求其他的线程去将它唤醒(LockSupport#unpark),所以可以安全地对它进行阻塞(LockSupport#park),返回 true,其他情况都返回 false。

      Node.CANCEL:值为 1,唯一一个大于 0 的值,设置为这个值说明当前 pred (node 的前继节点)已经取消获得锁的操作,应该继续查找其前继节点(ws != Node.CANCEL)作为 node 的前继节点。

      其他情况,会调用 compareAndSetWaitStatus 方法将 pred (node 的前继节点)的 waitStatus(等待状态)更新为 NODE.SIGNAL。

    • 如果 shouldParkAfterFailedAcquire 返回 true, 即 node 的 前继节点已经被阻塞,则对当前线程进行 park 操作,并检查是否中断。这里使用 LockSupport#park() 方法进行阻塞线程。通过 Thread.interrupted() 判断线程是否被中断,并重置中断状态。如果线程被中断,则该方法返回 true, acquireQueued 方法中设置  interrupted 为 true。

     

    • 线程被重置中断后,返回是否中断为 true, 故在 acquire 方法中执行了 selfInterrupt 进行线程中断。

       

    • 最后在 finally 块中判断节点线程是否获得锁成功(failed = false),如果失败(failed = true),则执行 cancelAcquire 操作。(这里的代码不细说)
    • 接下来看 FairSync(公平锁)如何实现 lock(加锁操作)

        对比 NonFairSync(非公平锁),NonFairSync 会尝试先去获得锁,即后申请锁的线程有可能优先获得锁;而 FairSync(公平锁)则按照 FIFO 的原则让先申请锁的线程去获得锁。

      

       

    • 可以看到 FairSync(公平锁)的 tryAcquire 方法中与 NonFairSync(非公平锁)方法不同的是,它会先去进行 hasQueuedPredecessors 方法判断当前队列是否有线程正在进行获得锁操作。若返回 true,则有其他线程正在进行获得锁操作,当前线程不再进行获得锁操作,tryAcquire 方法返回 false。

    • 然后同样进入队列,执行与 NonfairSync(非公平锁)一样的操作

      

     【尝试获得锁 tryLock()

    • tryLock() 表示当前线程尝试去获得锁,若获得锁成功,则返回 true;若获得锁失败,则返回 false。

    • 这里用到的 nonfairTryAcquire 在上面 NonfairSync(非公平锁)实现方法中已经讲过了,但它属于 AQS 的部分,不属于 NonfairSync 的实现。可以看到 tryLock() 只会尝试一次去获得锁,并没有加入 CHL 锁队列中。

     【可以抛出异常的加锁方法  lockInterruptibly()

    • lockInterruptibly() 方法的实现逻辑与 lock() 大致相同,lockInterruptibly() 遇到线程中断的情况会抛出 InterruptedException 异常,lock() 不会抛出任何异常。

         

    • 同样加入 CHL 队列锁,不同的是 线程中断的时候,以下方法会抛出  InterruptedException 异常。

     【解锁操作  unlock()】 

    • unlock() 方法用于释放锁。如果当前线程是锁的持有者,则将持有锁的数量减 1;若持有锁的数量为 0,则锁已经被释放;如果当前线程不是锁的持有者,调用 unlock() 方法会抛出 IllegalMonitorStateException 异常。

    •  使用 tryRelease 释放锁,如果释放锁成功,则返回 true;释放锁失败则返回 false。

    • tryRelease  方法中首先判断当前线程是否持有锁(即判断当前线程是否为独占锁的线程),如果不是则抛出 IllegalMonitorStateException 异常。getState() 返回当前线程持有锁的数量,减去 releases (1) 得到释放锁后当前线程持有锁的数量 c。
    • 如果 c 为 0,则说明当前线程已经释放了锁,设置 free 为 true,设置独占锁的线程为 null。
    • 更新锁的状态为 c,返回是否释放锁 free。

    • tryRelease 返回 true,若 head 节点不为空且其  waitStatus(等待状态)不为 0,则调用 unparkSuccessor 唤醒队列的第一个线程节点

     

  • 相关阅读:
    【Lintcode】112.Remove Duplicates from Sorted List
    【Lintcode】087.Remove Node in Binary Search Tree
    【Lintcode】011.Search Range in Binary Search Tree
    【Lintcode】095.Validate Binary Search Tree
    【Lintcode】069.Binary Tree Level Order Traversal
    【Lintcode】088.Lowest Common Ancestor
    【Lintcode】094.Binary Tree Maximum Path Sum
    【算法总结】二叉树
    库(静态库和动态库)
    从尾到头打印链表
  • 原文地址:https://www.cnblogs.com/zlxyt/p/11105052.html
Copyright © 2011-2022 走看看