如下是一个参照AQS进行的一个加锁及解锁的简单实现:
- 多线程并发进行同步业务操作;
- 加锁:尝试进行cas 0->1操作;
- 如果加锁成功则进行业务处理,然后进行锁释放 1->0,然后将列头的线程进行唤醒;
- 如果加锁失败则先将线程加入到队列中,然后再进行一次CAS加锁尝试及判断;
- 在确认此刻锁是被占用状态(cas仍然失败)后方可将当前线程进行park操作;
- 线程被唤醒,重新进行cas加锁操作;
需要区别于【无限CAS】判断操作,如果线程过多或大量的耗费cpu资源。park于unpark的运用可以起到阻塞线程跟唤醒线程的功效,可以不会造成cpu资源的大量浪费。
public class Lock {
private volatile int status;
private static final Unsafe unsafe;
// 对于 Java AbstractQueuedSynchronizer (AQS) 中的 stateOffset 字段,
// 通常会使用 static final 修饰是因为这个偏移量值在整个类中都是不变的,且所有实例都需要使用相同的偏移量来进行 CAS 操作。
// 即:不管有多少个 Lock 对象,对于Bean对象中的status操作的偏移量值是固定的,可以使用 static final
// statusOffset 只是一个相对的偏移量,跟status具体的值无关,所以可以多实例共享statusOffset
private static final long statusOffset;
// 模拟一个先进先出的队列
private static final Queue<Thread> threadQueue = new ConcurrentLinkedQueue<>();
static {
try {
// sun.misc 中的class无法像AQS中那样通过 private static final Unsafe unsafe = Unsafe.getUnsafe(); 直接调用
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
statusOffset = unsafe.objectFieldOffset(Lock.class.getDeclaredField("status"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void lock() {
int expect = 0;
int update = 1;
while (true) {
// 进行cas尝试
if (unsafe.compareAndSwapInt(this, statusOffset, expect, update)) {
System.out.println(Thread.currentThread().getName() + ":加锁成功");
break;
} else {
// CAS失败,阻塞线程并放入队列
threadQueue.offer(Thread.currentThread());
// 队头元素
Thread peek = threadQueue.peek();
// 在进行线程park前,这里一定需要再进行一次cas尝试,否则可能会出现并发情况下一些线程无法被唤醒的可能
if(peek == Thread.currentThread()
// 如果这里cas失败,那么说明锁肯定还被某个线程占用着没有释放,而当前线程已经确认加入到队列中,
// 那么就能确保,解锁逻辑中poll出来的线程肯定有一个是当前线程的,就不会存在线程不会被唤醒的情况了,
// 所以else逻辑中当前线程可以尽情park,肯定会有人叫醒它
&& unsafe.compareAndSwapInt(this, statusOffset, expect, update)) {
// 如果队头元素就是刚刚放进去的元素,并且cas成功了,那么也代表获取锁成功
System.out.println(Thread.currentThread().getName() + ":加锁成功");
break;
} else {
try {
// 放大被park的延迟,以更好的观测极端情况
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
unsafe.park(false, 0);
}
}
}
}
protected void unlock() {
int expect = 1;
int update = 0;
unsafe.compareAndSwapInt(this, statusOffset, expect, update);
System.out.println(Thread.currentThread().getName() + ":释放锁成功");
// 获取队列头并唤醒线程
Thread thread = threadQueue.poll();
// 这里可能没有拿到,但是 下一刻 队列中就加入了一个新的线程
// 需要考虑并发情况下被遗漏的线程,被遗漏的线程会一直得不到唤醒:见lock逻辑中的再次cas操作判断
if (thread != null) {
unsafe.unpark(thread);
}
}
// 非线程安全的list 被用来作加锁线程安全测试
static List<Integer> businessData = new ArrayList<>();
public static void business(Integer i) {
// CAS成功,执行业务逻辑
businessData.add(i);
System.out.println(Thread.currentThread().getName() + ":Business logic executed");
}
public static void main(String[] args) throws InterruptedException {
Lock lock = new Lock();
// 模拟多个线程并发进行CAS操作
for (int i = 0; i < 100; i++) {
// 进行线程并发,模拟进行加锁及业务处理
int finalI = i;
Thread thread = new Thread(() -> {
lock.lock();
try {
business(finalI);
} finally {
lock.unlock();
}
});
thread.setName("线程:" + (i + 1));
thread.start();
}
// 这里简单进行睡眠,确保线程都已经执行完
TimeUnit.SECONDS.sleep(5);
System.out.println(businessData.size());
System.out.println(businessData);
// 逻辑验证一下结果
HashSet<Integer> set = new HashSet<>(businessData);
for (int i = 0; i < businessData.size(); i++) {
if(!set.contains(i)) {
throw new RuntimeException("自建锁线程安全测试失败");
}
}}
}
运行结果如图:
这段代码实现了一个简单的自定义锁 Lock
,通过 CAS 操作来实现加锁和解锁的功能,并在加锁失败时将线程放入队列进行阻塞等待。下面是对代码的简要分析:
-
在静态代码块中,使用
Unsafe
类获取对status
字段的偏移量statusOffset
,并初始化了一个空的线程队列threadQueue
。 -
lock()
方法实现了加锁的逻辑:- 首先尝试通过 CAS 操作将
status
从 0 改为 1,如果成功表示加锁成功。 - 如果 CAS 失败,则将当前线程加入队列,并尝试再次通过 CAS 将
status
改为 1,如果成功则表示当前线程获取到了锁。 - 如果 CAS 失败,说明锁还被其他线程占用,当前线程会被阻塞并进入休眠状态,直到被唤醒后再次尝试获取锁。
- 首先尝试通过 CAS 操作将
-
unlock()
方法实现了解锁的逻辑:- 通过 CAS 操作将
status
从 1 改为 0,表示释放锁成功。 - 从队列中取出一个线程,并唤醒该线程。
- 通过 CAS 操作将
-
business(Integer i)
方法模拟了业务逻辑,向一个非线程安全的列表中添加元素。 -
main()
方法模拟了多个线程并发进行 CAS 操作,加锁后执行业务逻辑,最后输出业务数据列表的大小和内容,并验证数据的正确性。
总体来说,这段代码实现了简单的自定义锁,并在多线程环境下进行了加锁和解锁操作,可以保证业务逻辑的线程安全性。但需要注意,这里只是一个简单的示例,实际场景中若涉及更复杂的业务逻辑和线程交互,可能需要更完善的设计和测试。