# 接口设计
在讲解操作系统篇的时候,很多 c 语言的伪代码,并没有什么 synchronized
关键字什么的,其上锁 / 解锁的步骤就是 lock/unlock
,方法,等待 / 唤醒也就是 wait/signal
,在 j.u.c
包中也就提供了这样的 API
。
# Lock
public interface Lock { | |
// 获取锁,拿不到锁会阻塞,等待其他线程释放锁,获取到锁后返回 | |
void lock(); | |
// 同上,但是等待过程中会响应中断 | |
void lockInterruptibly() throws InterruptedException; | |
// 尝试获取锁,但是不会阻塞,如果能获取到会返回 true,不能返回 false | |
boolean tryLock(); | |
// 尝试获取锁,但是可以限定超时时间,如果超出时间还没拿到锁返回 false,否则返回 true,可以响应中断 | |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; | |
// 释放锁 | |
void unlock(); | |
// 暂时可以理解为替代传统的 Object 的 wait ()、notify () 等操作的工具 | |
Condition newCondition(); | |
} |
j.u.c
同样提供了 Lock
的实现类,比如 ReentrantLock
,甚至你可以使用 AQS
框架自己定义一把锁。
示例:
public class Main { | |
private static int i = 0; | |
public static void main(String[] args) throws InterruptedException { | |
Lock testLock = new ReentrantLock(); | |
Runnable action = () -> { | |
for (int j = 0; j < 100000; j++) { | |
testLock.lock(); // 加锁,加锁成功后其他线程如果也要获取锁,会阻塞,等待当前线程释放 | |
i++; | |
testLock.unlock(); // 解锁,释放锁之后其他线程就可以获取这把锁了(注意在这之前一定得加锁,不然报错) | |
} | |
}; | |
new Thread(action).start(); | |
new Thread(action).start(); | |
Thread.sleep(1000); // 等上面两个线程跑完 | |
System.out.println(i); | |
} | |
} |
# Condition
public interface Condition { | |
// 进入到等待状态,但是这里需要调用 Condition 的 signal 或 signalAll 方法进行唤醒,等待状态下是可以响应中断的 | |
void await() throws InterruptedException; | |
// 同上,但不响应中断(看名字都能猜到) | |
void awaitUninterruptibly(); | |
// 等待指定时间,如果在指定时间(纳秒)内被唤醒,会返回剩余时间,如果超时,会返回 0 或负数,可以响应中断 | |
long awaitNanos(long nanosTimeout) throws InterruptedException; | |
// 等待指定时间(可以指定时间单位),如果等待时间内被唤醒,返回 true,否则返回 false,可以响应中断 | |
boolean await(long time, TimeUnit unit) throws InterruptedException; | |
// 可以指定一个明确的时间点,如果在时间点之前被唤醒,返回 true,否则返回 false,可以响应中断 | |
boolean awaitUntil(Date deadline) throws InterruptedException; | |
// 唤醒一个处于等待状态的线程,注意还得获得锁才能接着运行 | |
void signal(); | |
// 同上,但是是唤醒所有等待线程 | |
void signalAll(); | |
} |
Condition
对象通过 Lock
接口提供的 newCondition
方法获得,也就是的每个 Condition
对象都对应特定地 Lock
对象。
示例:
public static void main(String[] args) throws InterruptedException { | |
Lock testLock = new ReentrantLock(); | |
Condition condition = testLock.newCondition(); | |
new Thread(() -> { | |
testLock.lock(); // 和 synchronized 一样,必须持有锁的情况下才能使用 await | |
System.out.println("线程1进入等待状态!"); | |
try { | |
condition.await(); // 进入等待状态 | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
System.out.println("线程1等待结束!"); | |
testLock.unlock(); | |
}).start(); | |
Thread.sleep(100); // 防止线程 2 先跑 | |
new Thread(() -> { | |
testLock.lock(); | |
System.out.println("线程2开始唤醒其他等待线程"); | |
condition.signal(); // 唤醒线程 1,但是此时线程 1 还必须要拿到锁才能继续运行 | |
System.out.println("线程2结束"); | |
testLock.unlock(); // 这里释放锁之后,线程 1 就可以拿到锁继续运行了 | |
}).start(); | |
} |
# TimeUnit
这里顺便提一下 TimeUnit
工具类,因为 Condition
可以限时等待,所以可以使用 TimeUnit
来确定时间单位。
condition.await(3,TimeUnit.SECONDS)
限时等待 3 秒。- 支持时间转换:
TimeUnit.SECONDS.toMinutes(60)
。 - 直接调用等待方法:
public static void main(String[] args) throws InterruptedException { | |
synchronized (Main.class) { | |
System.out.println("开始等待"); | |
TimeUnit.SECONDS.timedWait(Main.class, 3); // 直接等待 3 秒 | |
System.out.println("等待结束"); | |
} | |
} |
在源码中,本质上还是调用 obj.wait
。
- 直接休眠:
TimeUnit.SECONDS.sleep(1)
。
# LockSupport
LockSupport
提供了 park/unpark
方法用于上锁和解锁, ReentrantLock
的实现就使用了 LockSupport
# park 函数
有两个重载版本
public static void park(); | |
public static void park(Object blocker) { | |
// 获取当前线程 | |
Thread t = Thread.currentThread(); | |
// 设置 Blocker | |
setBlocker(t, blocker); | |
// 获取许可 | |
UNSAFE.park(false, 0L); | |
// 重新可运行后再此设置 Blocker | |
setBlocker(t, null); | |
} |
Thread
类有一个变量为 parkBlocker
,对应的就是 LockSupport
的 park
等方法设置进去的阻塞对象。该参数主要用于问题排查和系统监控,在线程 dump 中会显示该参数的信息,有利于问题定位。
调用了 park
函数后,会禁用当前线程,除非许可可用。在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行:
- 其他某个线程将当前线程作为目标调用
unpark
。 - 其他某个线程中断当前线程,即
t.interrupt()
。 - 该调用不合逻辑地 (即毫无理由地) 返回。
还有两个关于限时禁用的函数:
parkNanos(long nanos)
最多等待指定的等待时间parkUntil(long deadline)
最多等待到指定时间,deadline
是绝对时间。
# unpark 函数
如果线程在 park 上受阻塞,则它将解除其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证此操作有任何效果。
public static void unpark(Thread thread) { | |
if (thread != null) // 线程为不空 | |
UNSAFE.unpark(thread); // 释放该线程许可 | |
} |
使用示例:
import java.util.concurrent.TimeUnit; | |
import java.util.concurrent.locks.LockSupport; | |
public class Main { | |
public static void main(String[] args) throws InterruptedException { | |
Thread.currentThread().setName("main线程"); | |
Thread t = new Thread() { | |
@Override | |
public void run() { | |
System.out.println("线程开始运行..."); | |
LockSupport.park();// 线程进入阻塞 | |
System.out.println("线程阻塞结束..."); | |
} | |
}; | |
t.setName("t线程"); | |
t.start(); | |
TimeUnit.SECONDS.sleep(2);// 主线程先休眠两秒,保证另一个线程一定先执行 LockSupport.park () | |
LockSupport.unpark(t); | |
} | |
} |
在 main
函数中的 LockSupport.unpark()
一行代码打断点,选择下栏的 Debug->Layout setting->Frames 查看 t 线程的状态(WAIT)
# 深入理解
park ()/unpark () 底层的原理是 “二元信号量”,你可以把它相像成只有一个许可证的 Semaphore,只不过这个信号量在重复执行 unpark () 的时候也不会再增加许可证,最多只有一个许可证。
LockSuport.park()
只是单独阻塞当前线程,可以被中断或者被其他线程唤醒,因为不存在获取锁,所以 unpark
也就不释放锁, Condition.await()
底层调用了 LockSupport.park
(以 AQS 为例),实际上,他在阻塞线程前还干了两件事,一是把当前线程添加到条件队列中,二是 “完全” 释放锁,也就是让 state 状态变量变为 0,然后才是调用 LockSupport.park()
阻塞当前线程。
至于
park
和sleep
,object.wait
的区别,我觉得比较简单,而且要写的比较多,就不赘述了。
# 参考
https://www.yuque.com/qingkongxiaguang/javase/cts2gq#a75b92b6
https://pdai.tech/md/java/thread/java-thread-x-lock-LockSupport.html
https://www.cnblogs.com/seve/p/14555740.html