Java ReentrantLock原理

JUC锁: ReentrantLock原理

一、示例分析

公平锁

/***
*说明: 该示例使用的是公平策略。 
*/
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyThread extends Thread {
    private Lock lock;
    public MyThread(String name, Lock lock) {
        super(name);
        this.lock = lock;
    }
    
    public void run () {
        lock.lock();
        try {
            System.out.println(Thread.currentThread() + " running");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }
}

public class AbstractQueuedSynchonizerDemo {
    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock(true);
        
        MyThread t1 = new MyThread("t1", lock);        
        MyThread t2 = new MyThread("t2", lock);
        MyThread t3 = new MyThread("t3", lock);
        t1.start();
        t2.start();    
        t3.start();
    }
}


运行结果:

Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running
 

二、加锁lock过程

  1. 获取当前线程
  2. 获取锁的状态getState()
  3. 判断锁的状态
  4. 如果锁是自由状态则第五步,如果不是自由状态则第七步
  5. 判断自己是否需要排队
    什么情况下当前线程不需要排队(排队=入队+阻塞)
    1、队列没有初始 对头和队尾等于null的时候是不需要排队
    2、队列当中只有一个线程的时候是不需要排队的
  6. 如果不需要排队则cas加锁
  7. 判断是否重入(一般情况下不重入)
  8. 下不重入直接返回false(加锁失败)
  9. 加锁失败之后会调用addWaiter,主要是入队(入队不等于排队)
    入队完成之后第一个节点是一个虚拟出来的节点(thread等于null),即前置节点,而不是我们入队的节点
  10. 判断是否需要自旋
  11. 如果需要自旋则再次获取锁,如果失败则park
  12. 如果不需要自旋则直接park

线程执行lock.lock,下图给出了方法调用中的主要方法。
Java ReentrantLock原理_第1张图片
由上图可知,最后的结果是t3线程会被禁止,因为调用了LockSupport.park。

三、加锁总结

  1. AQS框架 第一个线程t1获取锁的时候 代价基本为0(cas锁的状态,记录当前持有锁的线程),而且连队列都为null(队列都没有初始化)
  2. 当第一个线程释放锁之后第二个线程t2来加锁(t1 和t2是没有竞争执行),代价基本为0,(cas锁的状态,记录当前持有锁的线程),而且连队列都为null(队列都没有初始化),因为是重复上面步骤6
  3. t1没有释放锁这个t2来加锁,锁的情况如下
    1. t2会加锁失败,则返回false,然后调用addWaiter方法区初始化队列,然后自己入队
    2. 会把t2封装成为一个node,调用ENQ方法让当前node入队
    3. 入队成功之后调用acquireQueued final Node p = node.predecessor();获取上一个节点
    4. 判断上一个节点是否头节点
    5. 如果是头结点则再次获取锁(一次自旋)
    6. 如果获取到了锁,表示t1释放了锁(AQS框架当中第一个node永远可以理解为是当前持有锁的线程)
    7. 如果t1没有释放锁,t2自旋一次之后还是没有获取到锁则park 排队
  4. t3来加锁,t1没有释放锁,t2这个时候已经排队(和t2的区别在于t3入队之后不会自旋,直接排 队)。
    t3 因为t1没有释放锁,所以t3肯定拿不到锁,肯定会调用addWaiter–enq方法入队。

四、解锁unlock

第一种情况:

  1. 只有一个t1上锁了,当调用unlock解锁的时候,sync.release(1);
  2. 把锁的状态改为自由状态
  3. boolean free = false; 只是标识一下目前还没有释放锁成功,因为你仅仅把锁改成了自由状态,线程没有释放(而且还有可能是重入),所以这个变量是一个过渡变量。
  4. setExclusiveOwnerThread(null) 把持有锁的线程改为null 锁彻底释放了
  5. 以上是释放锁,接下来可能需要唤醒队列当中的阻塞线程去获取锁(因为有可能不需要唤醒)
  6. Node h = head;拿到队头if (h != null && h.waitStatus != 0)判断是否有对头,是否有线程在排队
  7. 由于当前这种情况肯定没有人排队则不需要唤醒,则return true 标识解锁成功

第二种情况:

就是队列当中有线程排队,比如t2

  1. 调用tryRelease方法释放
  2. if (h != null && h.waitStatus != 0) 判断是否需要唤醒下一个,当前这种情况肯定需要唤醒一个
  3. Node h = head;把头结点传给了unparkSuccessor,unparkSuccessor(h);
  4. 得到队列当中第一个排队的线程, 也就是t2所标识的node对象LockSupport.unpark(s.thread); 唤醒t2线程
  5. 由于t2在lock方法中被阻塞那么唤醒则也是从lock方法中被唤醒往下执行
 private final boolean parkAndCheckInterrupt() {
  //简单的唤醒t2
   LockSupport.park(this); 
   //这里为什么需要调用一下Thread.interrupted() 
   return Thread.interrupted();

 }

Thread.interrupted()这个方法主要干嘛?清除打断标记(复位)

五、内部类
Java ReentrantLock原理_第2张图片
Sync类存在如下方法和作用如下。

Java ReentrantLock原理_第3张图片

NonfairSync类
NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法,源码如下:

// 非公平锁
static final class NonfairSync extends Sync {
    // 版本号
    private static final long serialVersionUID = 7316153563782823691L;

    // 获得锁
    final void lock() {
        if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用
            // 把当前线程设置独占了锁
            setExclusiveOwnerThread(Thread.currentThread());
        else // 锁已经被占用,或者set失败
            // 以独占模式获取对象,忽略中断
            acquire(1); 
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

从lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。

FairSyn类
FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法,源码如下:

// 公平锁
static final class FairSync extends Sync {
    // 版本序列化
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        // 以独占模式获取对象,忽略中断
        acquire(1);
    }

    /**
        * Fair version of tryAcquire.  Don't grant access unless
        * recursive call or no waiters or is first.
        */
    // 尝试公平获取锁
    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取状态
        int c = getState();
        if (c == 0) { // 状态为0
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功
                // 设置当前线程独占
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据
            // 下一个状态
            int nextc = c + acquires;
            if (nextc < 0) // 超过了int的表示范围
                throw new Error("Maximum lock count exceeded");
            // 设置状态
            setState(nextc);
            return true;
        }
        return false;
    }
}
  

跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。

Java ReentrantLock原理_第4张图片

可以看出只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。这也是和Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。

六、类的构造函数

  • ReentrantLock()型构造函数;默认是采用的非公平策略获取锁
public ReentrantLock() {
    // 默认非公平策略
    sync = new NonfairSync();
}
  • ReentrantLock(boolean)型构造函数

可以传递参数确定采用公平策略或者是非公平策略,参数为true表示公平策略,否则,采用非公平策略:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

核心函数分析

通过分析ReentrantLock的源码,可知对其操作都转化为对Sync对象的操作,由于Sync继承了AQS,所以基本上都可以转化为对AQS的操作。如将ReentrantLock的lock函数转化为对Sync的lock函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)的不同而调用到Sync的不同子类。 所以可知,在ReentrantLock的背后,是AQS对其服务提供了支持。

你可能感兴趣的:(开发语言,个人开发,java,ReentrantLock,AQS)