目的

从设计者的角度考量如何实现一个较为完整的 JDK 锁(Lock 接口)。
预期的效果:

  • 不可重入锁:当线程持有锁后再次尝试获取锁会被阻塞;
  • 可重入锁:线程可重复获取锁,多次持有将其同步状态计数器累计加一,释放则减一,直到计数器为零时,表示锁完全被释放,其他线程可恢复;
  • 公平锁:按照线程进入 lock 的先后顺序,依次获取锁;
  • 独占锁(排他锁):只有一个线程可以持有锁;
  • 完全实现 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++) {
// Files.readAllLines(Paths.get("C:\\Users\\daiwenzh5\\Downloads\\激活码 (3).txt"));
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()) {
// 对同步状态计数器每次自减后判断其值是否为 0
// 0 表示锁被释放
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 ++;
// number ++ 自增的同步方法
increment();
} finally {
simpleLock.unlock();
}
}

结果

程序成功执行并退出,且结果符合预期,两重同步锁嵌套未发生死锁。

1
2
3
simpleLockTest.number = 5000, count = 5000

Process finished with exit code 0

公平锁

  • 按照进入 lock 的先后顺序(即等待队列),依次给线程获取锁;

注意事项

评论