死磕 java同步系列之自己动手写一个锁Lock
问题
(1)自己动手写一个锁需要哪些知识?
(2)自己动手写一个锁到底有多简单?
(3)自己能不能写出来一个完美的锁?
简介
本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁、解锁操作。
本篇文章的目标二是通过自己动手写一个锁,能更好地理解后面章节将要学习的AQS及各种同步器实现的原理。
分析
自己动手写一个锁需要准备些什么呢?
首先,在上一章学习synchronized的时候我们说过它的实现原理是更改对象头中的MarkWord,标记为已加锁或未加锁。
但是,我们自己是无法修改对象头信息的,那么我们可不可以用一个变量来代替呢?
比如,这个变量的值为1的时候就说明已加锁,变量值为0的时候就说明未加锁,我觉得可行。
其次,我们要保证多个线程对上面我们定义的变量的争用是可控的,所谓可控即同时只能有一个线程把它的值修改为1,且当它的值为1的时候其它线程不能再修改它的值,这种是不是就是典型的CAS操作,所以我们需要使用Unsafe这个类来做CAS操作。
然后,我们知道在多线程的环境下,多个线程对同一个锁的争用肯定只有一个能成功,那么,其它的线程就要排队,所以我们还需要一个队列。
最后,这些线程排队的时候干嘛呢?它们不能再继续执行自己的程序,那就只能阻塞了,阻塞完了当轮到这个线程的时候还要唤醒,所以我们还需要Unsfae这个类来阻塞(park)和唤醒(unpark)线程。
基于以上四点,我们需要的神器大致有:一个变量、一个队列、执行CAS/park/unpark的Unsafe类。
大概的流程图如下图所示:
关于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,这章是基础,请各位老铁务必搞明白。
推荐阅读
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。
死磕 java同步系列之自己动手写一个锁Lock的更多相关文章
- 死磕 java线程系列之自己动手写一个线程池
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...
- 死磕 java线程系列之自己动手写一个线程池(续)
(手机横屏看源码更方便) 问题 (1)自己动手写的线程池如何支持带返回值的任务呢? (2)如果任务执行的过程中抛出异常了该怎么处理呢? 简介 上一章我们自己动手写了一个线程池,但是它是不支持带返回值的 ...
- 死磕 java同步系列之AQS起篇
问题 (1)AQS是什么? (2)AQS的定位? (3)AQS的实现原理? (4)基于AQS实现自己的锁? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为Jav ...
- 死磕 java同步系列之CyclicBarrier源码解析——有图有真相
问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...
- 死磕 java同步系列之Phaser源码解析
问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...
- 死磕 java同步系列之zookeeper分布式锁
问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应 ...
- 死磕 java同步系列之redis分布式锁进化史
问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...
- 死磕 java同步系列之终结篇
简介 同步系列到此就结束了,本篇文章对同步系列做一个总结. 脑图 下面是关于同步系列的一份脑图,列举了主要的知识点和问题点,看过本系列文章的同学可以根据脑图自行回顾所学的内容,也可以作为面试前的准备. ...
- 死磕 java同步系列之StampedLock源码解析
问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...
随机推荐
- java后台验证码的生成
前台代码: <tr> <td>验证码</td> <td><input name="checkCode" type=" ...
- Ukulele 原来你也在这里
- Swift在1.2版本的变化
从Xcode 6.3 Beta Release Notes看出,Xcode 6.3 Beta包含了很多颇为值得开发者期待的改变,共计50多处改动,同时修改了Objective-C的语法,足见苹果对Sw ...
- MySQL中的字符串
MySQL的字符串是从1开始编号的,这与计算机编程语言有所不同,在MySQL中1代表第一个字符,-1代表最后一个字符,以此类推. MySQL中百分号“%”代表的是任意个字符,下划线“_”代表的是任意一 ...
- web前端常用的封装方法
1.放大镜 //页面加载完毕后执行 window.onload = function () { var oDemo = document.getElementById('demo'); var oMa ...
- SpringMVC里静态网页不能加载到.js .css文件的问题
在写SpringMVC项目时候,写的js css文件打不开,网上查了一下,解决办法: 在web.xml里面: <servlet> <servlet-name>dispatche ...
- PyCharm学习笔记(二) 调试配置
选择PyCharm编译器 注意工程默认使用的解释器可能是Pycharm自带的,而不是单独安装的.
- (转)automaticallyAdjustsScrollViewInsets(个人认为iOS7中略坑爹的属性)
转自http://m.blog.csdn.net/blog/humingtao2013/27662093 automaticallyAdjustsScrollViewInsets(个人认为iOS7中略 ...
- 落谷p3376 最大流EdmondsKarp增广路模板
参考: https://blog.csdn.net/txl199106/article/details/64441994 分析: 该算法是用bfs求出是否有路从s到t, 然后建立反向边(关于反向边), ...
- MIP求解方法总结
*本文主要记录和分享学习到的知识,算不上原创 *参考文献见链接 本文主要简述了求解MIP问题的两大类(精确求解和近似求解),或者更细致地,三大类方法(精确算法,ε-近似算法和启发式算法).由于暂时不太 ...