java
public class Test {
public static void main(String[] args) throws InterruptedException {
int[] cnt = {1000};
Runnable task = () -> {
for (int i = 0;i < 100;i ++){
LockSupport.parkNanos(10);
cnt[0]--;
}
};
List<Thread> threadList = new ArrayList<>();
for (int i = 0;i < 10;i ++){
Thread thread = new Thread(task);
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList) {
thread.join();
}
System.out.println(cnt[0]);
}
}
上面代码是一个典型的线程不安全的例子,cnt[0]
的结果大概率不是 0,因为 cnt[0]--
操作不是原子的。
如何解决呢? 对 cnt[0]-- 操作加锁,使之变为 原子操作即可。
java
public class Test {
public static void main(String[] args) throws InterruptedException {
int[] cnt = {1000};
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
for (int i = 0;i < 100;i ++){
LockSupport.parkNanos(10);
cnt[0]--;
}
lock.unlock();
};
List<Thread> threadList = new ArrayList<>();
for (int i = 0;i < 10;i ++){
Thread thread = new Thread(task);
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList) {
thread.join();
}
System.out.println(cnt[0]);
}
}
现在我的需求:自己实现一个 Lock,完成上述功能。
最简单的实现方式:
java
public class MyLock {
// false: 没有线程获取锁
AtomicBoolean flag = new AtomicBoolean(false);
void lock(){
while (true){
// 成功从 false -> true,表示获取锁
if (flag.compareAndSet(false,true)){
return;
}
}
}
void unlock(){
while (true){
if (flag.compareAndSet(true,false)){
return;
}
}
}
}
上述 锁的实现方式,能够达成相同的效果。
思考两个问题:
unlock()
,不是持有锁的线程 释放锁 怎么办 ?
- owner 变量 记录当前持有锁的线程,释放锁 前判断一下是否是持有锁的线程。
lock()
方法,没有获取到锁线程,一直尝试获取锁,CPU 空转,并没有阻塞,如何解决?
- 把 没有获取锁的线程 包装成 节点,放在 链表最后。
- 阻塞,由 释放锁的线程 唤醒。
- 唤醒链表中 第一个节点 的 线程,不会全部唤醒。 全部唤醒 又会进入 激烈的锁竞争中。
MyLock
java
public class MyLock {
AtomicBoolean flag = new AtomicBoolean(false);
Thread owner;
// dummy 节点
AtomicReference<Node> head = new AtomicReference<>(new Node());
AtomicReference<Node> tail = new AtomicReference<>(head.get());
void lock(){
if (flag.compareAndSet(false,true)){
owner = Thread.currentThread();
return;
}
// 没有获取到锁
// 把线程包装成 Node,放在链表尾
Node current = new Node();
current.thread = Thread.currentThread();
while (true){
// CAS 加到链表
Node currentTail = tail.get();
if (tail.compareAndSet(currentTail,current)){
current.pre = currentTail;
currentTail.next = current;
break;
}
}
while (true){
// 在真正地阻塞前,自己在最后获取一次锁
// head --> A --> B
if (current.pre == head.get() && flag.compareAndSet(false,true)){
owner = Thread.currentThread();
head.set(current);
current.pre.next = null;
current.pre = null;
return;
}
LockSupport.park();
}
}
void unlock(){
if (Thread.currentThread() != owner){
throw new IllegalStateException("当前线程并不持有锁!");
}
// 唤醒 head 下一个节点
Node headNode = head.get();
Node next = headNode.next;
// 释放锁,之后是线程不安全的
flag.set(false);
if (next != null){
LockSupport.unpark(next.thread);
}
}
class Node{
Node pre;
Node next;
Thread thread;
}
}
问题 1:head、tail
为什么用 AtomicReference
?
- 多线程环境下,引用改变 是线程不安全的。
- 需要使用
AtomicReference
的 CAS 操作。
问题 2:
java
Node current = new Node();
current.thread = Thread.currentThread();
// CAS 加到链表
while (true){
// 多线程环境下,tail 是会变的。
// 每次 CAS,都需要重新获取 tail。
Node currentTail = tail.get();
if (tail.compareAndSet(currentTail,current)){
// 链表操作
current.pre = currentTail;
currentTail.next = current;
break;
}
}
问题 3:
java
// 在真正地阻塞前,自己在最后获取一次锁
while (true){
// head --> A(current) --> B
// if (当前节点 是 第一个节点 && CAS获取锁成功)
if (current.pre == head.get() && flag.compareAndSet(false,true)){
owner = Thread.currentThread();
// 设置 head
// 不用CAS,因为获取锁的线程只会有一个
head.set(current);
// 断开 head <--> A 之间的连接
current.pre.next = null;
current.pre = null;
return;
}
LockSupport.park();
}
下面的代码是说:阻塞的线程,只能等待其他线程的唤醒,才能够去获取锁。
- 如果,没有其他线程唤醒的话,就一直阻塞下去。
- 所以,在阻塞前,需要 获取一次锁,防止没有其他线程唤醒。
java
while (true){
LockSupport.park();
if (current.pre == head.get() && flag.compareAndSet(false,true)){
owner = Thread.currentThread();
head.set(current);
current.pre.next = null;
current.pre = null;
return;
}
}
构建链表(节点连接)的过程 存在延迟,导致 持有锁的线程 没有获取到下一个需要唤醒的线程。
<font style="color:rgb(24, 25, 28);">currentTail.next = current</font>
还没有执行
此时若线程是先 park 的,则有可能导致线程无法被唤醒;
而先自己尝试解锁,失败了再 park ,可以在 持有锁的线程 唤醒失败的情况下自己获取锁,线程不会被park ,从而避免这一情况。

线程 1 解锁获取链表的 next 但此时获取到的是空,就不会唤醒线程。
如果 cpu 的时间片刚好给到了线程 2,那么此时线程 2 在链表中被阻塞,就无法唤醒了。
如果后面又进来了一个线程 3 也来获取这个锁,
- 在公平锁的情况下,是会直接唤醒线程 2。
- 如果是非公平锁的情况下,就是先是线程 3 获取到锁,然后再释放线程 3 的锁时(假设这个过程中也没有其他线程进来竞争锁了) 就唤醒线程2 了。
问题 4:
java
void unlock(){
if (Thread.currentThread() != owner){
throw new IllegalStateException("当前线程并不持有锁!");
}
Node headNode = head.get();
Node next = headNode.next;
// 唤醒线程之前,释放锁
// 不释放,会导致:线程被唤醒,获取不到锁,又阻塞了
// 释放锁不需要 CAS。 因为一定是持有锁的线程,才能释放锁。
flag.set(false);
// 释放锁之后,线程就不安全了
if (next != null){
LockSupport.unpark(next.thread);
}
}
问题 5:
MyLock 中获取锁的方式?
- 直接获取锁。
- 被唤醒后,获取锁。
非公平锁:获取锁的线程,不用放到 链表中,可以 直接获取锁。
公平锁:获取锁的线程,全部放到 链表 中,挨个唤醒头结点。
上述是 非公平锁的实现。
java
if (flag.compareAndSet(false,true)){
owner = Thread.currentThread();
return;
}
去掉这块代码之后就是公平锁了。
测试 MyLock
java
public static void main(String[] args) throws InterruptedException {
int[] cnt = {1000};
MyLock lock = new MyLock();
Runnable task = () -> {
lock.lock();
for (int i = 0;i < 100;i ++){
LockSupport.parkNanos(1);
cnt[0]--;
}
lock.unlock();
};
List<Thread> threadList = new ArrayList<>();
for (int i = 0;i < 10;i ++){
Thread thread = new Thread(task);
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList) {
thread.join();
}
System.out.println(cnt[0]);
}
plain
Thread-5加入到链表尾
Thread-7加入到链表尾
Thread-1加入到链表尾
Thread-2加入到链表尾
Thread-3加入到链表尾
Thread-4加入到链表尾
Thread-8加入到链表尾
Thread-9加入到链表尾
Thread-6加入到链表尾
Thread-0获取到锁
Thread-2被唤醒获取到锁
Thread-0唤醒了Thread-2
Thread-2唤醒了Thread-1
Thread-1被唤醒获取到锁
Thread-1唤醒了Thread-3
Thread-3被唤醒获取到锁
Thread-4被唤醒获取到锁
Thread-3唤醒了Thread-4
Thread-6被唤醒获取到锁
Thread-4唤醒了Thread-6
Thread-5被唤醒获取到锁
Thread-6唤醒了Thread-5
Thread-5唤醒了Thread-8
Thread-8被唤醒获取到锁
Thread-7被唤醒获取到锁
Thread-8唤醒了Thread-7
Thread-7唤醒了Thread-9
Thread-9被唤醒获取到锁
0
补充
(一)

主要原因在 ++链表链接++ 和 ++创建节点++ 的过程不是原子的
(二)
java 的 <font style="color:rgb(24, 25, 28);">park</font>
和 <font style="color:rgb(24, 25, 28);">unpark</font>
是没有先后的约束的,先<font style="color:rgb(24, 25, 28);">unpark</font>
再<font style="color:rgb(24, 25, 28);">park</font>
是<font style="color:rgb(24, 25, 28);">park</font>
不住的,可以继续执行。
这是个标记,存在jvm层面的。 同时,多次 <font style="color:rgb(24, 25, 28);">unpark</font>
也只会标记 1 ,不会一直 +1.
(三)
- 判断指向比 CAS 资源消耗少
- 判断逻辑不光是在 ++阻塞被唤醒++ 的时候,还有 ++刚把自己插入队尾之后++ ,判断一下是不是可以不阻塞直接启动,判断引用比 CAS 消耗少
- 虚假唤醒。虚假唤醒是CPU的问题,是可能完全无征兆的被唤醒,这个是应用程序(我们的代码)无法控制的,所以所有的 wait 永远都应该放在 while 里面,这是JDK注释中要求的
思考

java
public class MyLock {
AtomicInteger state = new AtomicInteger(0);
Thread owner;
AtomicReference<Node> head = new AtomicReference<>(new Node());
AtomicReference<Node> tail = new AtomicReference<>(head.get());
void lock(){
// 没有线程持有锁
if (state.get() == 0){
// 成功 CAS -> 加锁成功
if (state.compareAndSet(0,1)){
owner = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "获取到锁");
return;
}
}else {
// 持有锁的线程是否是自己
if (Thread.currentThread() == owner){
state.set(state.get() + 1);
return;
}
}
Node current = new Node();
current.thread = Thread.currentThread();
while (true){
Node currentTail = tail.get();
if (tail.compareAndSet(currentTail,current)){
current.pre = currentTail;
currentTail.next = current;
System.out.println(Thread.currentThread().getName() + "加入到链表尾");
break;
}
}
while (true){
if (current.pre == head.get() && state.compareAndSet(0,1)){
owner = Thread.currentThread();
head.set(current);
current.pre.next = null;
current.pre = null;
System.out.println(Thread.currentThread().getName() + "被唤醒获取到锁");
return;
}
LockSupport.park();
}
}
void unlock(){
if (Thread.currentThread() != owner){
throw new IllegalStateException("当前线程并不持有锁!");
}
// 重入的次数
int stateCnt = state.get();
if (stateCnt > 1){
state.set(stateCnt - 1);
return;
}
if (stateCnt <= 0){
throw new IllegalStateException("重入锁 解锁错误!!!");
}
// i = 1
Node headNode = head.get();
Node next = headNode.next;
owner = null;
state.set(0);
if (next != null){
LockSupport.unpark(next.thread);
System.out.println(Thread.currentThread().getName() + "唤醒了" + next.thread.getName());
}
}
class Node{
Node pre;
Node next;
Thread thread;
}
}
解释 1:
java
Node headNode = head.get();
Node next = headNode.next;
owner = null;
state.set(0);
++如果不把 owner 置 null++,会在以下情况出问题:
- 线程A 解了锁,然后线程B 秒抢锁,但还没来得及把 owner 改成B,就被 A 重入了,然后B 才执行 owner = B;
- 这时候 A,B 都有锁,A执行 unlock 的时候才发现锁是B的,就抛异常了