并发编程系列——4Reentrantlock核心原理分析

学习目标

  1. 锁的类别有哪些

  2. reentrantlock与synchronized的区别

  3. 设计一把锁要考虑啥

  4. 如何使用reentrantlock

  5. reentrantlock的lock流程

  6. reentrantlock的unlock流程

  7. AQS的理解

第1章 锁分类

1、乐观锁和悲观锁

  • 乐观锁:采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法。具体过程为:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作。(CAS)

  • 悲观锁:采用悲观思想处理数据,在每次读取数据时都认为别人会修改数据,所以每次在读写数据时都会上锁,这样别人想读写这个数据时就会阻塞、等待直到拿到锁。(Synchronized)

2、公平锁和非公平锁

  • 公平锁:指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。

  • 非公平锁:指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待。

3、共享锁和独占锁

  • 独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现。

  • 共享锁:允许多个线程同时获取该锁,并发访问共享资源。ReentrantReadWriteLock中的读锁为共享锁的实现。

4、重量级锁和轻量级锁

5、可重入锁和不可重入锁

  • 可重入锁:也叫作递归锁,指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境下,ReentrantLock和synchronized都是可重入锁。

  • 不可重入锁:每次都要重新抢占锁资源。StampedLock是jdk1.8新增的重入锁

6、自旋锁:说白了就是一个循环

7、分段锁:说白了就是对每个区间进行上锁

8、如何进行锁优化

  • 减少锁持有的时间 减少锁持有的时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。

  • 减小锁粒度 减小锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是ConcurrentHashMap中的分段锁。

  • 锁分离 锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。 操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据。

  • 锁粗化 锁粗化指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分得太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。

  • 锁消除 在开发中经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是因为程序编码不规范引起的。这时,我们需要检查并消除这些不必要的锁来提高系统的性能。

第2章 reentrantlock与synchronized的区别

1、底层实现层面

synchronized 是JVM层面的锁,是Java关键字

reentrantlock是JUC下面的一个类,是java实现的

2、是否可手动释放

synchronized不需要手动释放,reentrantlock需要手动释放

3、是否可以中断

synchronized不可中断,reentrantlock可以

4、效率层面

reentrantlock效率比synchronized要高

5、锁的层面

reentrantlock的对象就是锁本身,synchronized锁的是别的对象;

reentrantlock可以

6、公平和非公平层面

reentrantlock可以是公平也可以是非公平,通过构造函数指定,synchronized是非公平

第3章 如何设计一把锁

先来看看我这代码里面实现了一把锁

public class RepeatLock {
    boolean isLocked = false;
    Thread lockedBy = null;
    int lockedCount = 0;
    public synchronized void lock()
            throws InterruptedException{
        Thread thread = Thread.currentThread();
        while(isLocked && lockedBy != thread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = thread;
    }
    public synchronized void unlock(){
        if(Thread.currentThread() == this.lockedBy){
            lockedCount--;
            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
}

public class Count {
    // 不可重入锁
    UnRepeatLock lock = new UnRepeatLock();
    public void print() {
        System.out.println("获取lock");
        lock.lock();
        System.out.println("调用doAdd之前");
        doAdd();
        System.out.println("调用doAdd之后");
        lock.unlock();
        System.out.println("释放lock");
    }
    public void doAdd() {
        System.out.println("再次获取lock");
        lock.lock();
        //do something
        lock.unlock();
        System.out.println("再次释放lock");
    }
    public static void main(String[] args) {
        Count cc = new Count();
        new Thread(()->{cc.print();}).start();
    }
}

实现锁的互斥特性

  • 锁的互斥特性->要有共享资源->标记(0无锁,1有锁)

  • 没有抢占到锁的线程?(等待->唤醒)

  • 等待的线程怎么存储?->数据结构去存储一系列等待中的线程,FIFO

  • 公平和非公平(能否插队)

  • 重入的特性(识别是否是同一个线程)

第4章  源码解读

本文只介绍非公平锁的实现

public class AtomicDemo {
    private static int count = 0;
    static Lock lock = new ReentrantLock();
    public static void inc() {
        lock.lock();
        try {
            Thread.sleep(1);
            count++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void dec() {
        lock.lock();
        try {
            Thread.sleep(1);
            count--;
        } 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(()->{AtomicDemo.inc();}).start();
        }
        Thread.sleep(4000);
        System.out.println(count);
    }
}

 并发编程系列——4Reentrantlock核心原理分析_第1张图片

4.1 lock

final void lock() {
    //抢占互斥资源
    //if(state==0){
        //然后再去修改状态
    //}
    //上面这种方式肯定不行,因为可能同时有多个线程进入到if语句,这就意味着多个线程抢占到同一把锁
    //不管当前队列是否有人排队,所有锁都可以去抢
    if (compareAndSetState(0, 1))//通过CAS去抢(乐观锁)
        //保存当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

1、线程A尝试去拿到锁,这个时候state=0,能够拿到锁,把state改为1,调用setExclusiveOwnerThread方法,赋值exclusiveOwnerThread=线程A

2、线程B进来,尝试去拿锁,拿不到锁,进入acquire(1)方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

主要有3个方法,

  • tryAcquire

  • addWaiter

  • acquireQueued

我们先看第一个tryAcquire(arg),最终进入非公平锁实现类

  protected final boolean tryAcquire(int acquires) {
      return nonfairTryAcquire(acquires);
  }
final boolean nonfairTryAcquire(int acquires) {
    //得到当前线程
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果是0,说明线程A已经释放锁,现在我们假如的是A还没有释放,所以不为0,如果已经执行完,,按照刚才的逻辑把state改为1,调用setExclusiveOwnerThread		方法赋值exclusiveOwnerThread=线程B
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果当前线程是已经保存的exclusiveOwnerThread,表明同一个线程重入,只需要加重入次数
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//这里不需要CAS是因为当前线程已经获得锁了
        //为什么要统计累加次数,是为了匹配lock和unlock成对出现
        return true;
    }
    return false;
}

按照我们的假设,线程B都不满足,nonfairTryAcquire返回false,继续执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,我们先看addWaiter

//mode传入参数为null
private Node addWaiter(Node mode) {
    //node的构造函数,赋值node的nextWaiter与thread,nextWaiter=null thread=ThreadB  this.nextWaiter = mode;this.thread = thread;
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //tail默认为null,链表的尾结点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

pred=tail为null ,进入enq方法

private Node enq(final Node node) {
    //跟while(1)的区别,while从反汇编来看,代码更长,时间更久,没啥区别
    for (;;) {
        //tail为null
        Node t = tail;  
        if (t == null) { // Must initialize
            //尝试设置head=new node
            if (compareAndSetHead(new Node()))
                //将tail设置为head, tail  head都为new    Node()
                //再进入循环,t=new Node().进入else
                tail = head;
        } else {
            //t=new node  ,node为刚才ThreadB的node
            node.prev = t;
            //尝试把ThreadB的node赋值给tail
            if (compareAndSetTail(t, node)) {
                //赋值成功,headnode的后节点指向B的node,tail=B线程节点
                t.next = node;
                //返回head节点,跳出方法
                return t;
            }
        }
    }
}

到这,形成了一个链表node,并且返回tail节点

并发编程系列——4Reentrantlock核心原理分析_第2张图片

 继续调用

//node为ThreadB的node,也就是tail节点,arg=1
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //拿到ThreadB Node的前指针节点
            final Node p = node.predecessor();
            //如果P==head 满足,并且尝试去拿锁,A在占着,拿不到锁,走下一个if
            if (p == head && tryAcquire(arg)) {
                //将Node设置为head节点,并且前后指针空指向,变成下图
                setHead(node);
                //把之前的head的对象的后节点执行为空,让它进行GC回收
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //A没有释放,进入该逻辑,shouldParkAfterFailedAcquire为false,不执行,进入下次循环
            //B还是拿不到锁,进入shouldParkAfterFailedAcquire
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

进入shouldParkAfterFailedAcquire方法,第一次进入把head节点的waitStatus改成SIGNAL,第二次进入返回true

//pred为ThreadB的Node的前节点,即head节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // pred.waitStatus默认为0,进入else流程
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        //把head的waitStatus从0改成SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

第二次进入shouldParkAfterFailedAcquire后返回true,调用parkAndCheckInterrupt方法

private final boolean parkAndCheckInterrupt() {    
    LockSupport.park(this); //挂机当前线程    
    return Thread.interrupted();
}

同理,假如A没执行完,B挂起,再来一个线程C,最后会得到B、C都挂起

并发编程系列——4Reentrantlock核心原理分析_第3张图片

4.2 unlock

//参数为1
public final boolean release(int arg) {    	
    //释放锁成功        
    if (tryRelease(arg)) {            
        Node h = head;            
        if (h != null && h.waitStatus != 0)                
            unparkSuccessor(h);            
        return true;        
    }        
    return false;    
}

//尝试去释放锁

//releases传入为1
protected final boolean tryRelease(int releases) {    
    //这一步state为1,因为之前A占着,得到c=0    
    int c = getState() - releases;    
    //判断是不是当前锁是A占着    
    if (Thread.currentThread() != getExclusiveOwnerThread())        
        throw new IllegalMonitorStateException();    
    boolean free = false;    
    if (c == 0) {        
        free = true;		
        //释放锁,将exclusiveOwnerThread设置为null        
        setExclusiveOwnerThread(null);    
    }    
    //将state设置为0    
    setState(c);    
    return free;
}

释放锁成功,之下如下逻辑

Node h = head;   
//拿到head的node节点//首节点不为空,并且waitStatus为SIGNAL -1
if (h != null && h.waitStatus != 0)    
    //进入该逻辑    
    unparkSuccessor(h);
return true;
//为一个new的node首节点
private void unparkSuccessor(Node node) {    
    //ws为-1    
    int ws = node.waitStatus;    
    if (ws < 0)        
        //进入该逻辑,将head节点的waitStatus更改为0        
        compareAndSetWaitStatus(node, ws, 0);        
    
    //找到B线程的node节点    
    Node s = node.next;    
    //B线程的node节点不为空,waitStatus为-1不进入    
    if (s == null || s.waitStatus > 0) {        
        s = null;        
        //如果B节点失效,或者已经取消或者并发还没有赋值,从后往前找,找到第一个需要执行的node,t==null或者t==当前head节点,才跳出        
        //为什么?enq方法里面,if没有锁 线程A通过CAS进入if语句块之后,发生上下文切换,此时线程B同样执行了该方法,并且执行完毕。然后线程C调用了unparkSuccessor方法。        
        //假如是从头到尾的遍历形式,线程A的next指针此时还是null!也就是说,会出现后续节点被漏掉的情况。        
        for (Node t = tail; t != null && t != node; t = t.prev)            
            if (t.waitStatus <= 0)                
                s = t;    
    }    
    //得到我需要要unpark的线程,这里为ThreadB    
    if (s != null)        
        //解锁threadB        
        LockSupport.unpark(s.thread);
}

当A线程unlock释放锁后,线程B的线程被唤醒,从park的时候继续开始执行,阻塞在acquireQueued方法

final boolean acquireQueued(final Node node, int arg) {    
    boolean failed = true;    
    try {        
        boolean interrupted = false;        
        for (;;) {            
            //threadA解锁后,继续执行,拿到head节点            
            final Node p = node.predecessor();            
            //p==head节点,并且threadB去尝试加锁,这个时候A已经释放锁,进入该逻辑            
            if (p == head && tryAcquire(arg)) {                
                setHead(node); 
                //将B节点设置为head节点,里面逻辑同时把node的前指针以及thread设为null                
                p.next = null; // help GC  //将p的next执行去除                
                failed = false;                
                return interrupted;            
            }            
            if (shouldParkAfterFailedAcquire(p, node) &&               
                parkAndCheckInterrupt())   
                //当线程A解锁后,ThreaB从这里开始运行,继续for循环                
                interrupted = true;        
        }    
    } finally {        
        if (failed)            
            cancelAcquire(node);    
    }
}

继续执行for循环,进入第一个if后,链表的节点变成如下

并发编程系列——4Reentrantlock核心原理分析_第4张图片

第5章 相关问题

5.1 为什么是非公平锁

因为我任意一个新的线程都会在之前加锁的线程释放锁的时候抢占到锁,即在unparkSuccessor方法中,A释放锁,但是还没有调用LockSupport.unpark(s.thread)方法的时候,有新的线程去调用了lock,并且加锁成功,所以,非公平就体现出来了,我后来的新线程可以在我之前的线程抢先获得锁

那么,公平锁又是怎么实现的,我们知道默认是非公平锁,那么我们看下怎么做到公平

5.2 公平锁怎么实现

公平锁,拿到锁后,直接调用

public final void acquire(int arg) {    
    if (!tryAcquire(arg) &&        
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        
        selfInterrupt();
}

然后调用tryAcquire方法

protected final boolean tryAcquire(int acquires) {    
    //得到当前线程    
    final Thread current = Thread.currentThread();    
    int c = getState();    
    //能抢占到锁,假如A释放了,我们新进来一个线程去抢占锁    
    if (c == 0) {        
        if (!hasQueuedPredecessors() &&            
            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");        
        setState(nextc);        
        return true;    
    }    
    return false;
}

我们发现公平锁与非公平锁多了一个hasQueuedPredecessors方法判断,只有当hasQueuedPredecessors返回false的时候,我们才会去尝试加锁

public final boolean hasQueuedPredecessors() {    
    // The correctness of this depends on head being initialized    
    // before tail and on head.next being accurate if the current    
    // thread is first in queue.    
    Node t = tail; //首先读取tail,为了避免tail不为空,head为空的情况,初始化逻辑    
    //if (compareAndSetHead(new Node()))    //tail = head;    
    Node h = head;    
    Node s;    
    return h != t &&        
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

我们来分析下,

h != t 就是我的队列里面不止一个node节点,就是有node节点在等待,那么返回true,接下来去判断&&后面的逻辑,如何后面的逻辑也为true,就不让抢占,但是如果后面的为false,就能抢占

  1. 如果h==t,有2种情况,第一种2个都为null,说明链表还没有初始化,没有待执行线程,第二种都不为空,但是相等 说明链表已经只有一个线程在执行并且执行完了没有情况,新来的线程可以去拿到

((s = h.next) == null || s.thread != Thread.currentThread()); 这里因为第一个条件为true了,所以这个条件必须为false才能够去抢占锁,那么s = h.next) == null和s.thread != Thread.currentThread()必须都为false

  1. (s = h.next) == null表示有一个线程已经在抢占了,但是还没有完全入链表,不可抢占

  2. s.thread != Thread.currentThread() 下一个要执行的node 的thread不是当前的线程,不可抢占CLH(Craig, Landin, and Hagersten locks):

以上就能够保证,我要抢占锁,必须按照顺序来,FIFO,也就保证了线程的公平性

5.3 状态

SIGNAL -1 必须后续需要去unpark

CANCELLED 1 取消状态

CONDITION -2 等待条件状态,在等待队列中

PROPAGATE -3 状态需要向后传播

5.4 interrupted的作用

除了unpark外,还有interrupt可以中断,该作用就是当interrupt中断时,我能捕捉到,传递我的线程是否有被interrupt中断过,如果有中断过,并且拿到锁,执行selfInterrupt,告诉外面我这个线程被interrupt中断过

5.5 park和unpark

1)不会抛中断异常

2)使用更加灵活(相对于wait和notify,它们必须在加锁的代码块内),可以在任何地方用

3)可以指定锁和解锁哪个线程

下文预告

  1. HashMap的底层数据结构

  2. HashMap、CHM、HashTable三者区别

  3. HashMap的工作流程

你可能感兴趣的:(并发编程,java,jvm,开发语言)