问题

(1)自己动手写一个锁需要哪些知识?

(2)自己动手写一个锁到底有多简单?

(3)自己能不能写出来一个完美的锁?

简介

本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁、解锁操作。

本篇文章的目标二是通过自己动手写一个锁,能更好地理解后面章节将要学习的AQS及各种同步器实现的原理。

分析

自己动手写一个锁需要准备些什么呢?

首先,在上一章学习synchronized的时候我们说过它的实现原理是更改对象头中的MarkWord,标记为已加锁或未加锁。

但是,我们自己是无法修改对象头信息的,那么我们可不可以用一个变量来代替呢?

比如,这个变量的值为1的时候就说明已加锁,变量值为0的时候就说明未加锁,我觉得可行。

其次,我们要保证多个线程对上面我们定义的变量的争用是可控的,所谓可控即同时只能有一个线程把它的值修改为1,且当它的值为1的时候其它线程不能再修改它的值,这种是不是就是典型的CAS操作,所以我们需要使用Unsafe这个类来做CAS操作。

然后,我们知道在多线程的环境下,多个线程对同一个锁的争用肯定只有一个能成功,那么,其它的线程就要排队,所以我们还需要一个队列。

最后,这些线程排队的时候干嘛呢?它们不能再继续执行自己的程序,那就只能阻塞了,阻塞完了当轮到这个线程的时候还要唤醒,所以我们还需要Unsfae这个类来阻塞(park)和唤醒(unpark)线程。

基于以上四点,我们需要的神器大致有:一个变量、一个队列、执行CAS/park/unpark的Unsafe类。

大概的流程图如下图所示:

关于Unsafe类的相关讲解请参考彤哥之前发的文章:

死磕 java魔法类之Unsafe解析

解决

一个变量

这个变量只支持同时只有一个线程能把它修改为1,所以它修改完了一定要让其它线程可见,因此,这个变量需要使用volatile来修饰。

private volatile int state;

CAS

这个变量的修改必须是原子操作,所以我们需要CAS更新它,我们这里使用Unsafe来直接CAS更新int类型的state。

当然,这个变量如果直接使用AtomicInteger也是可以的,不过,既然我们学习了更底层的Unsafe类那就应该用(浪)起来。

private boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

一个队列

队列的实现有很多,数组、链表都可以,我们这里采用链表,毕竟链表实现队列相对简单一些,不用考虑扩容等问题。

这个队列的操作很有特点:

放元素的时候都是放到尾部,且可能是多个线程一起放,所以对尾部的操作要CAS更新;

唤醒一个元素的时候从头部开始,但同时只有一个线程在操作,即获得了锁的那个线程,所以对头部的操作不需要CAS去更新。

private static class Node {
// 存储的元素为线程
Thread thread;
// 前一个节点(可以没有,但实现起来很困难)
Node prev;
// 后一个节点
Node next; public Node() {
} public Node(Thread thread, Node prev) {
this.thread = thread;
this.prev = prev;
}
}
// 链表头
private volatile Node head;
// 链表尾
private volatile Node tail;
// 原子更新tail字段
private boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

这个队列很简单,存储的元素是线程,需要有指向下一个待唤醒的节点,前一个节点可有可无,但是没有实现起来很困难,不信学完这篇文章你试试。

加锁

public void lock() {
// 尝试更新state字段,更新成功说明占有了锁
if (compareAndSetState(0, 1)) {
return;
}
// 未更新成功则入队
Node node = enqueue();
Node prev = node.prev;
// 再次尝试获取锁,需要检测上一个节点是不是head,按入队顺序加锁
while (node.prev != head || !compareAndSetState(0, 1)) {
// 未获取到锁,阻塞
unsafe.park(false, 0L);
}
// 下面不需要原子更新,因为同时只有一个线程访问到这里
// 获取到锁了且上一个节点是head
// head后移一位
head = node;
// 清空当前节点的内容,协助GC
node.thread = null;
// 将上一个节点从链表中剔除,协助GC
node.prev = null;
prev.next = null;
}
// 入队
private Node enqueue() {
while (true) {
// 获取尾节点
Node t = tail;
// 构造新节点
Node node = new Node(Thread.currentThread(), t);
// 不断尝试原子更新尾节点
if (compareAndSetTail(t, node)) {
// 更新尾节点成功了,让原尾节点的next指针指向当前节点
t.next = node;
return node;
}
}
}

(1)尝试获取锁,成功了就直接返回;

(2)未获取到锁,就进入队列排队;

(3)入队之后,再次尝试获取锁;

(4)如果不成功,就阻塞;

(5)如果成功了,就把头节点后移一位,并清空当前节点的内容,且与上一个节点断绝关系;

(6)加锁结束;

解锁

// 解锁
public void unlock() {
// 把state更新成0,这里不需要原子更新,因为同时只有一个线程访问到这里
state = 0;
// 下一个待唤醒的节点
Node next = head.next;
// 下一个节点不为空,就唤醒它
if (next != null) {
unsafe.unpark(next.thread);
}
}

(1)把state改成0,这里不需要CAS更新,因为现在还在加锁中,只有一个线程去更新,在这句之后就释放了锁;

(2)如果有下一个节点就唤醒它;

(3)唤醒之后就会接着走上面lock()方法的while循环再去尝试获取锁;

(4)唤醒的线程不是百分之百能获取到锁的,因为这里state更新成0的时候就解锁了,之后可能就有线程去尝试加锁了。

测试

上面完整的锁的实现就完了,是不是很简单,但是它是不是真的可靠呢,敢不敢来试试?!

直接上测试代码:

private static int count = 0;

public static void main(String[] args) throws InterruptedException {
MyLock lock = new MyLock(); CountDownLatch countDownLatch = new CountDownLatch(1000); IntStream.range(0, 1000).forEach(i -> new Thread(() -> {
lock.lock(); try {
IntStream.range(0, 10000).forEach(j -> {
count++;
});
} finally {
lock.unlock();
}
// System.out.println(Thread.currentThread().getName());
countDownLatch.countDown();
}, "tt-" + i).start()); countDownLatch.await(); System.out.println(count);
}

运行这段代码的结果是总是打印出10000000(一千万),说明我们的锁是正确的、可靠的、完美的。

总结

(1)自己动手写一个锁需要做准备:一个变量、一个队列、Unsafe类。

(2)原子更新变量为1说明获得锁成功;

(3)原子更新变量为1失败说明获得锁失败,进入队列排队;

(4)更新队列尾节点的时候是多线程竞争的,所以要使用原子更新;

(5)更新队列头节点的时候只有一个线程,不存在竞争,所以不需要使用原子更新;

(6)队列节点中的前一个节点prev的使用很巧妙,没有它将很难实现一个锁,只有写过的人才明白,不信你试试^^

彩蛋

(1)我们实现的锁支持可重入吗?

答:不可重入,因为我们每次只把state更新为1。如果要支持可重入也很简单,获取锁时检测锁是不是被当前线程占有着,如果是就把state的值加1,释放锁时每次减1即可,减为0时表示锁已释放。

(2)我们实现的锁是公平锁还是非公平锁?

答:非公平锁,因为获取锁的时候我们先尝试了一次,这里并不是严格的排队,所以是非公平锁。

(3)完整源码

关注我的公众号“彤哥读源码”,后台回复“mylock”获取本章完整源码。

注:下一章我们将开始分析传说中的AQS,这章是基础,请各位老铁务必搞明白。

推荐阅读

  1. 死磕 java魔法类之Unsafe解析

  2. 死磕 java同步系列之JMM(Java Memory Model)

  3. 死磕 java同步系列之volatile解析

  4. 死磕 java同步系列之synchronized解析


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java同步系列之自己动手写一个锁Lock的更多相关文章

  1. 死磕 java线程系列之自己动手写一个线程池

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...

  2. 死磕 java线程系列之自己动手写一个线程池(续)

    (手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的 ...

  3. 死磕 java同步系列之AQS起篇

    问题 (1)AQS是什么? (2)AQS的定位? (3)AQS的实现原理? (4)基于AQS实现自己的锁? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为Jav ...

  4. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  5. 死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

  6. 死磕 java同步系列之zookeeper分布式锁

    问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应 ...

  7. 死磕 java同步系列之redis分布式锁进化史

    问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...

  8. 死磕 java同步系列之终结篇

    简介 同步系列到此就结束了,本篇文章对同步系列做一个总结. 脑图 下面是关于同步系列的一份脑图,列举了主要的知识点和问题点,看过本系列文章的同学可以根据脑图自行回顾所学的内容,也可以作为面试前的准备. ...

  9. 死磕 java同步系列之StampedLock源码解析

    问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...

随机推荐

  1. Codeforces Round #271 (Div. 2)-B. Worms

    http://codeforces.com/problemset/problem/474/B B. Worms time limit per test 1 second memory limit pe ...

  2. tkinter学习-文本框

    阅读目录 Entry 输入框 Text 文本框 Entry: 说明:输入控件,用于显示简单的文本内容 属性:在输入框中用代码添加和删除内容,同样也是用insert()和delete()方法 from ...

  3. 继上次编译openwrt之后,添加web界面

    上编博客写了关于openwrt编译环境和编译一个默认配置的openwrt系统. 现在我正在做如何添加web界面.(hiwooya自带的luci web) 方法如下: 首先在编译环境中配置 make m ...

  4. day22面向对象

    面向对象编程: 1.什么是面向对象 面向过程(编程思想): 过程,解决问题的步骤,流程即第一步做什么,第二步做什么 将复杂问题,拆成若干小问题,按照步骤一一解决,将复杂问题流程化(为其制定固定的实现流 ...

  5. STM32L0开发——ADC多通道采集,IDE和IAR开发注意事项

    keil开发L0系列是免费的,官方提供许可的.因此建议Keil开发,L011F3由于flash只有8K,因此不建议HAL库,建议使用cubemx+LL(或snippets库).0.起初,可以参考官方库 ...

  6. JQuery中xxx is not a function或者can not find $

    在项目中,遇到以上两个错误,反复折腾了好久,js代码写得没有问题,jquery的文件也引入了,就是反复的报告错误,xxx is not a function.如图: 就是这样的错误,shake is ...

  7. FastJson生成json时,显示Null属性

    FastJson生成json时,默认不会输出null字段. 移动端,有时候,需要后端提供完整的字段说明. Map < String , Object > jsonMap = new Has ...

  8. HDU 3507 斜率优化 DP Print Article

    在kuangbin巨巨博客上学的. #include <iostream> #include <cstdio> #include <cstring> #includ ...

  9. python算法-二叉树广度优先遍历

    广度优先遍历:优先遍历兄弟节点,再遍历子节点 算法:通过队列实现-->先进先出 广度优先遍历的结果: 50,20,60,15,30,70,12 程序遍历这个二叉树: # encoding=utf ...

  10. eclipse错误日志

    一.普通错误:(必现) 1.空指针: 2. 数组下标溢出,越界 3. 数组下标定义为双精度不妥,应该是整数 4. 类型转换错误(与手机分辨率有关) 二.  数据库错误:(必现) 1. 数据库,报错(数 ...