目的
从设计者的角度考量如何实现一个较为完整的 JDK 锁(Lock 接口)。
预期的效果:
CAS 机制
Compare And Set(Swap),即比较并设置(替换):对于变量,将预期值 Old 通过内存地址 V 于变量进行比较,当通过校验时,将新值 New 赋值给内存 V。
使用场景
- 数据库乐观锁更新;
- 网站访问量;
- Atomic 原子类;
- JVM 内部的 Unsafe 类;
限制
- 只能保证单个变量在多线程下的安全性,对代码块无效;
- ABA 问题:即仅对结果进行预期控制,无法约束过程,可能在执行过程中发生如: A->B->A 这样的状态变化,解决方法是追加版本号;
Lock 接口
Lock 接口位于 J.U.C(java.util.concurrent)的 locks 包下,区别于 JVM 内置锁(synchronized),该接口通过手动实现 JDK 锁,而能够提供灵活的锁管理机制,如:尝试非阻塞式获取锁、可中断获取锁、可超时获取锁。
不可重入锁
- 简单的实现了获取锁/释放锁的操作;
- 通过 CAS 机制,判断当前线程是否持有锁;
- 需要频繁读写等待队列,此处使用
LinkedBlockingQueue<Thread>
来保证在线程安全的前提下提供更好的性能;
- 此处使用了 Lock 来修改了线程状态,因此并不是自旋锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Override public void lock() { while (!owner.compareAndSet(null, Thread.currentThread())) { waiter.add(Thread.currentThread()); LockSupport.park(); waiter.remove(Thread.currentThread()); } }
@Override public void unlock() { if (owner.compareAndSet(Thread.currentThread(), null)) { for (Thread thread : waiter) { LockSupport.unpark(thread); } } }
|
测试
- 在 number 自增时添加锁;
- 创建 5 个线程,每个线程遍历 1000 次,对 number = 0,进行自增,预期结果为 5000;
- 在线程切换时,需要添加休眠时间,否则程序执行过快,无法看到预期效果;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| private Lock simpleLock = new SimpleLock();
private void increment() { simpleLock.lock(); try { number++; } finally { simpleLock.unlock(); } } public static void main(String[] args) throws InterruptedException { SimpleLockTest simpleLockTest = new SimpleLockTest(); int threadNumber = 5; int max = 1000; for (int i = 0; i < threadNumber; i++) { new Thread(() -> { for (int j = 0; j < max; j++) { simpleLockTest.increment(); simpleLockTest.doubleLock();
} }).start(); } Thread.sleep(100); System.out.printf("simpleLockTest.number = %d", simpleLockTest.number); }
|
结果
1 2 3
| simpleLockTest.number = 5000
Process finished with exit code 0
|
自旋锁
- 当线程未能获取锁时,让线程不停的执行循环体,而不修改线程状态,通过循环来阻塞无锁线程,称之为自旋锁;
- 但是由于每个线程都需要执行循环,当线程数不停增加时,性能下降明显,所以自旋锁仅适用于线程竞争不激烈(线程数量少),持有锁的时间短(频繁切换线程)的场景;
可重入锁
在普通的不可重入锁的基础上,为线程获取锁的状态添加一个计数器,一旦线程持有锁则计数器自增,在锁未被释放之前,只有持有锁的线程可以再次获取锁,使得计数器不断增加。
在线程释放锁时,每一次都使得计数器自减,直到其值为 0 时,表示线程完全释放了锁,此时将锁持有者线程置空即可。
使用场景
- 当同步方法嵌套使用时,需要使用可重入锁,用以避免死锁问题;
代码分析
- 无锁的线程通过循环遍历进行挂起;
- 线程一旦持有锁,则退出循环,并将其同步计数器自增;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Override public void lock() { while (!(owner.get() == Thread.currentThread() || owner.compareAndSet(null, Thread.currentThread()))) { waiter.add(Thread.currentThread()); LockSupport.park(); waiter.remove(Thread.currentThread()); } count.incrementAndGet(); }
private void print() { System.out.printf("ownerThread:%s,currentThread:%s%n", owner.get() == null ? "null" : owner.get().getName(), Thread.currentThread().getName()); }
@Override public void unlock() { if (owner.get() == Thread.currentThread()) { if (count.decrementAndGet() == 0) { owner.set(null); for (Thread thread : waiter) { LockSupport.unpark(thread); } } } }
|
测试
在测试代码内部添加两重嵌套的同步方法。
1 2 3 4 5 6 7 8 9 10 11 12
| private int count = 0;
private void doubleLock() { simpleLock.lock(); try { count ++; increment(); } finally { simpleLock.unlock(); } }
|
结果
程序成功执行并退出,且结果符合预期,两重同步锁嵌套未发生死锁。
1 2 3
| simpleLockTest.number = 5000, count = 5000
Process finished with exit code 0
|
公平锁
- 按照进入 lock 的先后顺序(即等待队列),依次给线程获取锁;
注意事项