我的知识海洋

What are you following

  • 首页
  • 标签
  • 分类目录
  • 文章归档
  • 行路万里
  • 读书万卷
  • About Me

  • 搜索
面经 解决方案 操作系统 Java源码 开源 GSoC 哲学 中间件 回溯 链表 书 top 数据库 分布式 滑动窗口 配置 动态规划 前缀树 并查集 Redis 总结 年终总结 面试 算法基础

9.22 | 手写一个排他锁,当多线程运行时只有线程名字为指定名字的线程能获取到锁

发表于 2022-09-22 | 分类于 学习 | 阅读次数 1117
# Java源码
9.21 | HashMap 的key能不能为null ,map 的呢?
9.24 | 讲讲操作系统的多级页表,为什么多级页表会省空间?

源代码链接

手写一个排他锁

要求当多线程运行时只有线程名字为指定名字的线程能获取到锁

步骤

  1. 实现 lock 接口
  2. 写一个静态类,继承 AbstractQueuedSynchronizer
  3. 重写 aqs 里面的方法,完成需求
  4. aqs 里只有五个方法可以被重写
    1. tryAcquire tryRelease 排他锁 加锁、解锁
    2. tryAcquireShared tryReleaseShared 共享锁 加锁 解锁
    3. isHeldExclusively 当前线程是否被线程独占的方法
    private static class Sync extends AbstractQueuedSynchronizer {

        // 加锁
        protected boolean tryAcquire(int arg) {
            // 线程名字必须是 AQS-Lock 才能加锁
            if (!Thread.currentThread().getName().equals("AQS-Lock")) {
                return false;
            }
            if (compareAndSetState(0, arg)) {
                // 设置为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 解锁
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalStateException();
            }
            // 此处不用cas的原因是,既然要调用解锁,那自然是自身持有锁,有锁,代表自己有一个单独的私人线程
            // 所以不涉及多线程问题,无需cas
            setState(0);
            return true;
        }

        Condition getCondition(){
            return  new ConditionObject();
        }
    }

测试用例

 private static CreateLock createLock = new CreateLock();

    public static void main(String[] args) {
        Thread A = new Thread(() -> {
            testLock();
            while (true) {

            }
        });
        Thread B = new Thread(() -> {
            testLock();
        });

        //  只有 A 能拿到锁,因为加锁限制了线程名字
        A.setName("AQS-Lock");
        B.setName("Lock");

        A.start();
        B.start();
    }

    private static void testLock() {
        createLock.lock();
        try {
            System.out.println("当前获取到锁的线程名称是:"
                    + Thread.currentThread().getName());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            createLock.unlock();
        }
    }

输出结果

当前获取到锁的线程名称是:AQS-Lock

然后一直等待。。。(因为线程A的 while true )

q2 说一下 多线程情况下 ReentrantLock 公平锁加锁过程?

先定义一下公平锁和非公平锁

公平锁

现在有ABC三条线程, A线程获取到了锁,BC线程获取失败,BC线程会加入到同步队列的尾端。

A线程释放锁后, B线程收到唤醒通知,进行锁的争抢。

就这在这时,一个新来的线程D也来争抢了,D线程看到B正在排队(D线程其实是调用AQS源码里边的 hasQueuedPredecessors 方法) ,就放弃争抢,直接加入同步队列尾端。继续等待

非公平锁

现在有ABC三条线程,A线程获取到了锁,BC线程获取失败,BC线程会加入到同步队列的微端。A线程释放锁后,B线程收到唤醒通知,进行锁的争抢。

就这在这时候,一个新来的线程D也来争抢了

D线程很不讲武德,直接和B线程进行争抢。如果D线程抢夺失败,加入同步队列。

如果D线程成功,对B线程不公平。对C也不公平,因为C也在排队。
此外 C 线程不会争抢锁,因为只有前置节点通知后续节点才会唤醒,也就是说 C 线程只能被 B 唤醒,唤醒的前提是B线程争抢锁成功,并且B线程释放锁之后才会通知 C 。

加锁成功的线程

image

  1. 先调用 FairSync 里的 lock()方法

  2. 使用 AQS 的 模版方法 acquire, acquire (AQS) 会调用tryAcquire (我们重写的方法)

  3. 抢占成功后会 setExclusiveOwnerThread(current) ,设置当前线程持有锁

加锁失败的线程

image

整体流程如上,简单叙述一下:

如果获取失败,当前线程就会被添加到同步队列尾端。

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

添加到尾端用的是 addWaiter 方法,addWaiter 方法会进行一次cas添加,如果添加失败, 则调用enq(死循环添加,一定要成功,如果添加成功,就会再次调用 )

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果队列不为空, 使用 CAS 方式将当前节点设为尾节点
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 队列为空、CAS失败,将节点插入队列
    enq(node);
    return node;
}

在 enq 方法里判断当前节点的前置节点是不是 head 节点,如果是,并且加锁成功,就算加锁成功

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

获取锁失败后, 判断是否把当前线程挂起

调用 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 当前节点的前驱就是head节点时, 再次尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 获取锁失败后, 判断是否把当前线程挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在 shouldParkAfterFailedAcquire 中,如果前置节点的状态是 signal,就等待,如果是 cancel 取消状态,将其移出,如果是其他状态,就
替换为 signal 状态,然后自己等待

    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
    	// SIGNAL 设置了前一个节点完结唤醒
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

如果还是失败,在acquireQueued 方法里,调用 selfInterrupt() 中断方法:

 // 当前线程进入waite状态,且将前驱节点的转态设置为signal,用于唤醒自己,返回中断状态状态
  //注意,这里会清除中断状态!!
  if (shouldParkAfterFailedAcquire(p, node) &&
      parkAndCheckInterrupt())
      interrupted = true; //如果等待过程中被中断过就将interrupted标记置为true

在acquireQueued(final Node node, int arg) 中,获取锁的方式是一个死循环,成功:会将的标志位failed(标记当前Node是否成功获取资源)置为false。

如果失败:抛出异常,则执行执行finally中cancelAcquire(node)方法,取消当前线程请求资源的操作。该方法负责将需要取消的node踢出等待队列

参考

https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantLock.html#%e5%85%ac%e5%b9%b3%e9%94%81

https://segmentfault.com/a/1190000023497508

# Java源码
9.21 | HashMap 的key能不能为null ,map 的呢?
9.24 | 讲讲操作系统的多级页表,为什么多级页表会省空间?

  • 文章目录
  • 站点概览
erdengk

erdengk

91 日志
5 分类
24 标签
RSS
Github E-mail
Creative Commons
友链
  • 星球球友
  • Joey
  • 北松山(itwaix)-TP在职
  • JooKS' Blog-GSoC 2022 Mentor
  • Chever-John-Shein在职
  • 一堆网页小游戏
  • 飞鸟记
0%
© 2019 — 2026 erdengk
由 Halo 强力驱动
陕ICP备2021015348号-1
川公网安备 51011202000481号
轻点广告,请我喝水,非常感谢 (。・ω・。)ノ(*/ω\*)