你真的明白ReentrantLock了吗?

synchronized是托管给JVM执行的,Lock的锁定是通过代码实现的。所以Lock比较灵活,可以便于开发人员根据合适的场景进行操作,Lock是一个接口,需要实现它来进行使用,ReetrantLock是Lock的主要实现类,ReetrantLock是一个可重入锁,同时可以指定公平锁和非公平锁,我们来具体看一下他的实现方式。

一、ReentrantLock使用方式

        ReentrantLock lock = new ReentrantLock();
        //如果被其它线程占用锁,会阻塞在此等待锁释放
        lock.lock();
        try {
            //操作
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();  
        }

上面只是其中一种方式,可以看到ReentrantLock的使用方式比较简单,创建出一个ReentrantLock对象,通过lock()方法进行加锁,使用unlock()方法进行释放锁操作。

他的加锁方式有三种,使用lock、trylock、trylock(long,TimeUnit)指定时间参数。使用lock来获取锁的话,如果锁被其他线程持有,那么就会处于等待状态。另外需要我们去主动的调用unlock方法去释放锁,即使发生异常,他也不会主动释放锁,需要我们显式的释放。使用trylock方法获取锁,是有返回值的,获取成功返回true,获取失败返回false,不会一直处于等待状态。使用trylock(long,TimeUnit)指定时间参数来获取锁,在等待时间内获取到锁返回true,超时返回false。还可以调用lockInterruptibly方法去中断锁,如果线程正在等待获取锁,可以中断线程的等待状态。

二、什么是可重入锁

上面我们说ReentrantLock是一个可重入锁,那么,什么是可重入锁呢?可重入锁是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。我们先来看下验证可重入锁的代码:

public static class TestReentrantLock {
    private Lock lock = new ReentrantLock();
    public void method() {
        lock.lock();
        try {
            System.out.println("方法1获得ReentrantLock锁");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public void method2() {
        lock.lock();
        try {
            System.out.println("方法2重入ReentrantLock锁");
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
            new TestReentrantLock().method();
    }
}

由上面的代码我们可以得知,ReentrantLock是具有可重入性的。那么,这种可重入的底层实现方式是什么呢?

可重入锁的底层实现方式

ReentrantLock是使用AQS中的state的值,线程可以不停地lock来增加state的值,对应地需要unlock来解锁,直到state为零。并且state的值是用volatile进行修饰,以下是具体源码实现。

 //java.util.concurrent.locks.ReentrantLock.FairSync
 protected final boolean tryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    int c = getState();
    //当前锁没被占用
    if (c == 0) {
        //1.判断同步队列中是否有节点在等待
       if (!hasQueuedPredecessors() &&
           compareAndSetState(0, acquires)) {//2.如果上面!1成立,修改state值(表明当前锁已被占用)
            //3.如果2成立,修改当前占用锁的线程为当前线程
           setExclusiveOwnerThread(current);
           return true;
       }
    }
    //占用锁线程==当前线程(重入)
    else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;//
       if (nextc < 0)
           throw new Error("Maximum lock count exceeded");
        //修改status
       setState(nextc);
       return true;
    }
    //直接获取锁失败
    return false;
}

三、什么是公平锁

我们前面说了ReentrantLock可以实现公平锁和非公平锁,那么什么是公平锁?什么是非公平锁呢?
所谓的公平锁,就是在多个线程请求获取同一个资源的时候,能够保证依照线程请求的顺序,依次执行,来保证公平竞争的效果。反之,非公平锁就是不按照线程请求顺序,每个线程一起去争抢锁,谁抢到是谁的。

上面我们说到ReentrantLock中的公平锁是通过继承AQS来实现的,我们来看下他的具体实现方式。

AQS

AQS是一个抽象类,主要是通过继承的方式来使用。AQS的功能分为两种:独占和共享。AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。

当有锁竞争的时候,当有新的线程加入进来,会将此线程封装成Node节点追加到同步队列中,并将新线程的前置指针指向上一个节点,将上一个节点的后置指针指向新线程的节点。是通过CAS来进行指针指向的这个修改的。


当头结点在释放锁时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,设置头节点不需要用CAS,原因是设置头节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证。

公平锁源码

加入同步队列(当同步队列为空时会直接获得锁),等待锁

//java.util.concurrent.locks.ReentrantLock.FairSync
final void lock() {
    acquire(1);
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire():模板方法,获取锁

//java.util.concurrent.locks.AbstractQueuedSynchronizer
//1
private Node addWaiter(Node mode) {
 //生成node
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
    //将node加到队列尾部
       node.prev = pred;
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
       }
    }
    //如果加入失败(多线程竞争或者tail指针为null)
    enq(node);
    return node;
}
//1.1  
private Node enq(final Node node) {
 //死循环加入节点(cas会失败)
    for (;;) {
       Node t = tail;
       if (t == null) { //tail为null,同步队列初始化
        //设置head指针
           if (compareAndSetHead(new Node()))//注意这里是个空节点!!
               tail = head;//将tail也指向head
       } else {
           node.prev = t;//将当前node加到队尾
           if (compareAndSetTail(t, node)) {
               t.next = node;
               return t;//注意这里才返回
           }
       }
    }
}
//2
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
    //表示是否被打断
       boolean interrupted = false;
       for (;;) {
        //获取node.pre节点
           final Node p = node.predecessor();
           if (p == head //当前节点是否是同步队列中的第二个节点
           && tryAcquire(arg)) {//获取锁,head指向当前节点
               setHead(node);//head=head.next
               p.next = null;//置空 
               failed = false;
               return interrupted;
           }

           if (shouldParkAfterFailedAcquire(p, node) && //是否空转(因为空转唤醒是个耗时操作,进入空转前判断pre节点状态.如果pre节点即将释放锁,则不进入空转)
               parkAndCheckInterrupt())//利用unsafe.park()进行空转(阻塞)
               interrupted = true;//如果Thread.interrupt()被调用,(不会真的被打断,会继续循环空转直到获取到锁)
       }
    } finally {
       if (failed)//tryAcquire()过程出现异常导致获取锁失败,则移除当前节点
           cancelAcquire(node);
    }
}

acquireQueued(addWaiter(Node.EXCLUSIVE), arg):加入同步队列

//java.util.concurrent.locks.AbstractQueuedSynchronizer
//1
private Node addWaiter(Node mode) {
 //生成node
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
    //将node加到队列尾部
       node.prev = pred;
       if (compareAndSetTail(pred, node)) {
           pred.next = node;
           return node;
       }
    }
    //如果加入失败(多线程竞争或者tail指针为null)
    enq(node);
    return node;
}
//1.1  
private Node enq(final Node node) {
 //死循环加入节点(cas会失败)
    for (;;) {
       Node t = tail;
       if (t == null) { //tail为null,同步队列初始化
        //设置head指针
           if (compareAndSetHead(new Node()))//注意这里是个空节点!!
               tail = head;//将tail也指向head
       } else {
           node.prev = t;//将当前node加到队尾
           if (compareAndSetTail(t, node)) {
               t.next = node;
               return t;//注意这里才返回
           }
       }
    }
}
//2
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
    //表示是否被打断
       boolean interrupted = false;
       for (;;) {
        //获取node.pre节点
           final Node p = node.predecessor();
           if (p == head //当前节点是否是同步队列中的第二个节点
           && tryAcquire(arg)) {//获取锁,head指向当前节点
               setHead(node);//head=head.next
               p.next = null;//置空 
               failed = false;
               return interrupted;
           }

           if (shouldParkAfterFailedAcquire(p, node) && //是否空转(因为空转唤醒是个耗时操作,进入空转前判断pre节点状态.如果pre节点即将释放锁,则不进入空转)
               parkAndCheckInterrupt())//利用unsafe.park()进行空转(阻塞)
               interrupted = true;//如果Thread.interrupt()被调用,(不会真的被打断,会继续循环空转直到获取到锁)
       }
    } finally {
       if (failed)//tryAcquire()过程出现异常导致获取锁失败,则移除当前节点
           cancelAcquire(node);
    }
}

selfInterrupt(): 唤醒当前线程

static void selfInterrupt() {//在获取锁之后 响应intterpt()请求
    Thread.currentThread().interrupt();
}

以上就是ReadWriteLock原理及源码的解读,希望大家能有收获。

部分内容参考:https://juejin.im/post/5ae1b4f0f265da0b7b359d7a

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269