公平锁和非公平锁以及他们的实现原理是什么

文章目录

完整的Java锁机制可以看这篇文章: https://blog.csdn.net/weixin_44797327/article/details/134761807?spm=1001.2014.3001.5502

什么是非公平锁和公平锁呢?

非公平锁 就是不按照线程先来后到的时间顺序进行竞争锁,后到的线程也能够获取到锁,公平锁就是按照线程先来后到的顺序进行获取锁,后到的线程只能等前面的线程都获取锁完毕才执行获取锁的操作,执行有序。

我们来看看lock()这个方法,这个有区分公平锁和非公平锁,这个两者的实现不同,先来看看公平锁,源码如下:

java 复制代码
// 直接调用 acquire(1)
final void lock() {
     acquire(1);
 }

我们来看看acquire(1)的源码如下:

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

这里的判断条件主要做两件事:

  1. 通关过该方法tryAcquire(arg)尝试的获取锁
  2. 若是没有获取到锁,通过该方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg)就将当前的线程加入到存储等待线程的队列中。

其中tryAcquire(arg)是尝试获取锁,这个方法是公平锁的核心之一,它的源码如下:

java 复制代码
protected final boolean tryAcquire(int acquires) {
             // 获取当前线程 
            final Thread current = Thread.currentThread();
            // 获取当前线程拥有着的状态
            int c = getState();
            // 若为0,说明当前线程拥有着已经释放锁
            if (c == 0) {
                 // 判断线程队列中是否有,排在前面的线程等待着锁,若是没有设置线程的状态为1。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 设置线程的拥有着为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
                // 若是当前的线程的锁的拥有者就是当前线程,可重入锁
            } else if (current == getExclusiveOwnerThread()) {
                // 执行状态值+1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 设置status的值为nextc
                setState(nextc);
                return true;
            }
            return false;
        }

tryAcquire()方法中,主要是做了以下几件事:

  1. 判断当前线程的锁的拥有者的状态值是否为0,若为0,通过该方法hasQueuedPredecessors()再判断等待线程队列中,是否存在排在前面的线程。
  2. 若是没有通过该方法 compareAndSetState(0, acquires)设置当前的线程状态为1。
  3. 将线程拥有者设为当前线程setExclusiveOwnerThread(current)
  4. 若是当前线程的锁的拥有者的状态值不为0,说明当前的锁已经被占用,通过current == getExclusiveOwnerThread()判断锁的拥有者的线程,是否为当前线程,实现锁的可重入。
  5. 若是当前线程将线程的状态值+1,并更新状态值。

公平锁的tryAcquire(),实现的原理图如下:

我们来看看acquireQueued()方法,

该方法是将线程加入等待的线程队列中,源码如下:

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循环处理
            for (;;) {
                // 获取前置线程节点
                final Node p = node.predecessor();
                // 这里又尝试的去获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 直接return  interrupted
                    return interrupted;
                }
                // 在获取锁失败后,应该将线程Park(暂停)
                if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued()方法主要执行以下几件事:

  1. 死循环处理等待线程中的前置节点,并尝试获取锁,若是p == head && tryAcquire(arg),则跳出循环,即获取锁成功。
  2. 若是获取锁不成功shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()就会将线程暂停。

acquire(int arg)方法中,最后若是条件成立,执行下面的源码:

java 复制代码
selfInterrupt();
// 实际执行的代码为
Thread.currentThread().interrupt();

即尝试获取锁失败,就会将锁加入等待的线程队列中,并让线程处于中断等待。公平锁lock()方法执行的原理图如下:

之所以画这些原理的的原因,是为后面写一个自己的锁做铺垫,因为你要实现和前人差不多的东西,你必须了解该东西执行的步骤,最后得出的结果,执行的过程是怎么样的。

有了流程图,在后面的实现自己的东西才能一步一步的进行。这也是阅读源码的必要之一。

lock()方法,其实在lock()方法中,已经包含了两方面:

  1. 锁方法lock()
  2. 尝试获取锁方法tryAquire()

接下来,我们来看一下unlock()方法的源码。

java 复制代码
  public void unlock() {
        sync.release(1);
    }

直接调用release(1)方法,来看release方法源码如下:

java 复制代码
    public final boolean release(int arg) {
       // 尝试释放当前节点
        if (tryRelease(arg)) {
            // 取出头节点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 释放锁后要即使唤醒等待的线程来获取锁
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

通过调用tryRelease(arg),尝试释放当前节点,若是释放锁成功,就会获取的等待队列中的头节点,就会即使唤醒等待队列中的等待线程来获取锁。接下来看看tryRelease(arg)的源码如下:

java 复制代码
// 尝试释放锁
 protected final boolean tryRelease(int releases) {
            // 将当前状态值-1
            int c = getState() - releases;
            // 判断当前线程是否是锁的拥有者,若不是直接抛出异常,非法操作,直接一点的解释就是,你都没有拥有锁,还来释放锁,这不是骗人的嘛
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //执行释放锁操作 1.若状态值=0   2.将当前的锁的拥有者设为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 重新更新status的状态值
            setState(c);
            return free;
        }

总结上面的几个方法,unlock释放锁方法的执行原理图如下:

对于非公平锁与公平锁的区别,在非公平锁尝试获取锁中不会执行hasQueuedPredecessors()去判断是否队列中还有等待的前置节点线程。

如下面的非公平锁,尝试获取锁nonfairTryAcquire()源码如下:

java 复制代码
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 直接就将status-1,并不会判断是否还有前置线程在等待
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

以上就是公平锁和非公平锁的主要的核心方法的源码,接下来我们实现自己的一个锁,首先依据前面的分析中,要实现自己的锁,拥有的锁的核心属性如下:

  1. 状态值status,0为未占用锁,1未占用锁,并且是线程安全的。
  2. 等待线程队列,用于存放获取锁的等待线程。
  3. 当前线程的拥有者。

lock锁的核心的Api如下:

  1. lock方法
  2. trylock方法
  3. unlock方法

依据以上的核心思想来实现自己的锁,首先定义状态值status,使用的是AtomicInteger原子变量来存放状态值,实现该状态值的并发安全和可见性。定义如下:

java 复制代码
// 线程的状态 0表示当前没有线程占用   1表示有线程占用
    AtomicInteger status =new AtomicInteger();

接下来定义等待线程队列,使用LinkedBlockingQueue队列来装线程,定义如下:

java 复制代码
// 等待的线程
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<Thread>();

最后的属性为当前锁的拥有者,直接就用Thread来封装,定义如下:

java 复制代码
// 当前线程拥有者
Thread ownerThread =null;

接下来定义lock()方法,依据上面的源码分析,在lock方法中主要执行的几件事如下:

  1. 死循环的处理等待线程队列中的线程,知道获取锁成功,将该线程从队列中删除,跳出循环。
  2. 获取锁不成功,线程处于暂停等待。
java 复制代码
    @Override
    public void lock() {
        // TODO Auto-generated method stub
        // 尝试获取锁
        if (!tryLock()) {
            // 获取锁失败,将锁加入等待的队列中
            waitersQueue.add(Thread.currentThread());
            // 死循环处理队列中的锁,不断的获取锁
            for (;;) {
                if (tryLock()) {
                    // 直到获取锁成功,将该线程从等待队列中删除
                    waitersQueue.poll();
                    // 直接返回
                    return;
                } else {
                    // 获取锁不成功,就直接暂停等待。
                    LockSupport.park();
                }
            }
        }
    }

然后是trylock方法,依据上面的源码分析,在trylock中主要执行的以下几件事:

  1. 判断当前拥有锁的线程的状态是否为0,为0,执行状态值+1,并将当前线程设置为锁拥有者。
  2. 实现锁可重入
java 复制代码
    @Override
    public boolean tryLock() {
        // 判断是否有现成占用
        if (status.get()==0) {
            // 执行状态值加1
            if (status.compareAndSet(0, 1)) {
                // 将当前线程设置为锁拥有者
                ownerThread = Thread.currentThread();
                return true;
            } else if(ownerThread==Thread.currentThread())  {
                // 实现锁可重入
                status.set(status.get()+1);
            }
        }
        return false;
    }

最后就是unlock方法,依据上面的源码分析,在unlock中主要执行的事情如下:

  1. 判断当前线程是否是锁拥有者,若不是直接抛出异常。
  2. 判断状态值是否为0,并将锁拥有者清空,唤醒等待的线程。
java 复制代码
    @Override
    public void unlock() {
        // TODO Auto-generated method stub
        // 判断当前线程是否是锁拥有者
        if (ownerThread!=Thread.currentThread()) {
            throw new RuntimeException("非法操作");
        }
        // 判断状态值是否为0
        if (status.decrementAndGet()==0) {
            // 清空锁拥有着
            ownerThread = null;
            // 从等待队列中获取前置线程
            Thread t = waitersQueue.peek();
            if (t!=null) {
               // 并立即唤醒该线程
                LockSupport.unpark(t);
            }
        }
    }
相关推荐
坊钰20 分钟前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表
chenziang125 分钟前
leetcode hot100 LRU缓存
java·开发语言
会说法语的猪31 分钟前
springboot实现图片上传、下载功能
java·spring boot·后端
码农老起31 分钟前
IntelliJ IDEA 基本使用教程及Spring Boot项目搭建实战
java·ide·intellij-idea
m0_7482398336 分钟前
基于web的音乐网站(Java+SpringBoot+Mysql)
java·前端·spring boot
时雨h40 分钟前
RuoYi-ue前端分离版部署流程
java·开发语言·前端
麒麟而非淇淋1 小时前
Day13 苍穹外卖项目 工作台功能实现、Apache POI、导出数据到Excel表格
java
小爬虫程序猿1 小时前
利用Java爬虫获取速卖通(AliExpress)商品详情的详细指南
java·开发语言·爬虫
Java编程乐园1 小时前
Java中以某字符串开头且忽略大小写字母如何实现【正则表达式(Regex)】
java·正则表达式