J.U.C ReentrantLock可重入锁使用以及源码分析

本质: 锁是用来解决线程安全问题的
Java中Lock的其他实现,WiteLock写锁、ReadLock读锁,本文主要以ReentrantLock重入锁展开

ReentrantLock 重入锁

重入锁、互斥锁,用来解决死锁问题的

1.ReentrantLock 的使用

	static Lock  lock = new ReentrantLock();
    static int sum = 0;
    public static void incr(){
        lock.lock();	//抢占锁 没有抢占的会阻塞
        try {
            Thread.sleep(1);
            sum ++ ;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();	//释放锁
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                LockExample.incr();
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(sum); //输出1000
    }

J.U.C Lock和Synchronized使用的区别就在于Lock的加锁和释放锁需要手动操作

2.ReentrantLock原理实现

满足线程的互斥特性,意味着同一个时刻只允许一个线程进入到加锁的代码中

一把锁应该具备的基础条件:

  • 有锁无锁的标识
  • 没有抢占锁的线程处理
    • 等待(直接先阻塞,释放CPU资源)
      • wait/notify 存在无法唤醒指定线程
      • LockSupport.park/unpark (阻塞指定线程,唤醒指定线程)
      • Condition
    • 排队(运行N个线程阻塞,此时线程处于等待状态)
      • 通过一个数据结构,把N个排队的线程存储起来
  • 锁的释放过程
    • LockSupport.unpark(thread) ->唤醒处于队列中的指定线程
  • 锁的公平性(是否允许插队)

以上条件还不是很清晰的话,继续看下面锁流程分析图
锁流程图

3.源码分析

1. Lock.lock()加锁

public void lock() {
    //这个地方可以看到 有两个方法 公平(FairSync)和非公平(NonfairSync)
    sync.lock();
}
//非公平锁代码
final void lock() {
    //非公平锁和公平锁就是这里不同
    //非公平锁在这里先尝试更新state状态 如果成功 直接获取锁
    //公平锁就是直接去取抢占锁
    if (compareAndSetState(0, 1))
        //如果这里修改状态成功 直接设置锁的拥有者为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //否则就去抢占锁 接下来看这里###
        acquire(1);
}

//AbstractQueuedSynchronizer
public final void acquire(int arg) {
    //尝试获取锁失败的话 就加到AQS队列 先看 tryAcquire
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//公平锁抢占的代码 非公共锁的抢占nonfairTryAcquire
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //与非公平锁的区别在hasQueuedPredecessors 下面说 
        if (!hasQueuedPredecessors() &&
            //cas操作state
            compareAndSetState(0, acquires)) {
            //拿到锁后设置线程变量 抢占锁流程就完毕了
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //重入锁
    else if (current == getExclusiveOwnerThread()) {
        //记录重入锁次数
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        //状态 为0 或者>0
        setState(nextc);
        return true;
    }
    return false;
}
  • 接下来分析公平锁hasQueuedPredecessors 的情况 ,返回true说明当前线程要去排队,非公平锁是没有这个逻辑的
public final boolean hasQueuedPredecessors() {
    //尾节点
    Node t = tail;
    //头节点
    Node h = head;
    //h后面的节点
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
//h!=t 返回true代表队列中至少有两个不同Node节点存在。
//(s = h.next) == null 返回false代表是有后继节点的。
//s.thread != Thread.currentThread() 返回true代表后继节点的线程不是当前线程,那当前线程自然得老老实实的去排队。
  • 上面可以看到,满足上述情况并且CAS操作state成功的话,线程就拿到锁了,接下来继续分析没有拿到锁的线程怎么处理?
//回到AbstractQueuedSynchronizer#acquire 
//acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE独占状态
  
//先看addWaiter
private Node addWaiter(Node mode) {
	//把当前线程封装成Node
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    //当前AQS队列不为空 则使用cas操作将node添加到tail节点 
    if (pred != null) {
    	//尾插法
        node.prev = pred;
        //设置新节点为尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //cas失败通过enq加到AQS队列
    enq(node);
    return node;
}
//通过不断的自旋以及CAS操作加到AQS队列
private Node enq(final Node node) {
    for (;;) {
        //从尾节点往前操作到头节点 线程不安全的情况下 头节点的next节点一直在变
        Node t = tail;
        //当前尾节点是空的 创建一个头尾节点
        if (t == null) { // Must initialize
            //因为此时是没拿到锁操作 多线程下不是安全的 所以必须用CAS操作
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //当前线程节点加到尾节点后面
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  • 接下来看下加到AQS队列的线程怎么处理
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
                //取出当前node的pre节点(前一个节点)
                final Node p = node.predecessor();
                //如果前置节点是head 说明当前节点排在等待队列的第一位 直接再去获取锁
                if (p == head && tryAcquire(arg)) {
                    //成功获取锁
                    //说明头节点已执行完毕并且是释放了锁 然后唤醒的当前节点
                    //设置为头节点 并返回 然后执行加锁的代码
                    setHead(node);
                    //将前节点移除队列,这样就没有指向了,帮助GC快速回收
                    p.next = null; // help GC
                    failed = false;
                    return interrupted; //将中断状态返回,false表示没有中断
                }
                //shouldParkAfterFailedAcquire有三种情况
                //1 如果已经是SIGNAL(等待被叫醒)状态 返回true
                //2 ws>0 关闭状态(线程被中断) 移除关闭线程 返回false
                //3 更新状态为SIGNAL状态 返回false
                //返回false就下次继续自旋去实现操作 最终会返回true并执行阻塞代码
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // LockSupport.park(this); 阻塞当前线程 唤醒后从这里开始执行
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

加锁的代码到这里就分析完了,接下来继续分析释放锁的代码。

2.Lock.unlock()释放锁

public void unlock() {
	//还是已安全锁为例
    sync.release(1);
}
//arg 代表锁的次数 重入锁需要释放到0才可被其他线程获取
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //1.修改state 为初始状态0
            //2.Node s = node.next;  LockSupport.unpark(s.thread); 唤醒下一个线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    //重入锁需要释放到0才可被其他线程获取
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

//再看下释放锁并唤醒下一个等待线程
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        //修改state为初始状态0
        compareAndSetWaitStatus(node, ws, 0);
    //获取下个等待唤醒的线程    
    Node s = node.next;
    //下一个节点无效或者节点状态为关闭状态的
    if (s == null || s.waitStatus > 0) {
        s = null;
        //通过从尾部节点开始扫描,找到距离head最近的一个
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //唤醒下一个有效的线程 下一个线程在acquireQueued自旋
        LockSupport.unpark(s.thread);
}

只有我认为自旋用得很妙吗?以上就是本章的全部内容了。
一定要自己打开源码跟着看一遍,切莫纸上谈兵

上一篇:线程安全性之有序性和内存屏障
下一篇:线程通信synchronized中的wait/notify、J.U.C Condition的使用和源码分析

书山有路勤为径,学海无涯苦作舟

你可能感兴趣的:(java开发,源码,java,lock,锁)