上周,我为“心情追忆”小程序添加了支付功能,用户可以通过支付0.1元来增加一次心情分析的机会。支付功能是一个非常重要的功能,我担忧可能出现的并发问题,例如当多个用户几乎同时进行支付操作时,可能会导致系统只记录了一次支付的情况。为了确保每个支付请求都能正确地增加分析次数,适当使用锁机制成为了必要的解决方案。
以下是两种基本的锁类型及其工作原理:
-
互斥锁(Mutex):当一个线程获取到锁后,其他试图获取同一锁的线程将直接返回失败。这种锁保证了在任意时刻只有一个线程能够访问被锁定的资源,从而避免了数据的不一致问题。互斥锁适用于那些不需要等待锁释放就能立即做出响应的场景。
-
信号量(Semaphore)或可重入锁(Reentrant Lock):当一个线程获取到了锁,其他线程会进入等待状态,直到第一个线程释放锁。之后,等待队列中的下一个线程将获得锁。这种方式可以保证所有等待的线程最终都能得到执行的机会,适用于需要长时间持有锁或有顺序执行要求的场景。
Synchronized关键字的底层原理
synchronized
是一个内置的同步机制,它可以用来控制多线程对共享资源的访问。synchronized
关键字通过在对象头中设置锁标志位来实现锁机制。当一个线程尝试进入由synchronized
修饰的方法或代码块时,它必须先获取该方法或代码块所属对象的锁。如果锁已经被其他线程持有,则当前线程将被阻塞,直到锁被释放。
Synchronized
的底层实现依赖于Java对象头中的Mark Word,其中包含了对象的哈希码、对象的分代年龄、锁标志位等信息。当线程尝试获取锁时,会检查Mark Word中的锁标志位来判断是否已经上锁以及锁的状态。
ReentrantLock
-
ReentrantLock:可重入锁是一种更灵活的锁机制,它允许同一个线程多次获取同一把锁而不会发生死锁。与
synchronized
不同的是,ReentrantLock
提供了更多的功能,比如尝试非阻塞地获取锁(tryLock)、可中断地获取锁(lockInterruptibly)等。此外,ReentrantLock
还支持公平锁和非公平锁的选择,前者按照请求锁的顺序分配锁,后者则允许插队,提高吞吐量。
特性 | synchronized | ReentrantLock |
---|---|---|
锁类型 | 内置锁 | 显式锁 |
可重入性 | 可重入 | 可重入 |
公平性 | 非公平(默认) | 可选公平或非公平 |
锁的释放 | 自动释放(方法结束或异常抛出时) | 手动释放(需要调用 unlock() 方法) |
等待可中断 | 不可中断 | 可中断(lockInterruptibly() 方法) |
尝试非阻塞获取锁 | 不支持 | 支持(tryLock() 方法) |
锁的绑定 | 绑定到对象 | 绑定到 ReentrantLock 实例 |
性能 | 早期版本性能较差,JDK 1.6+ 优化后性能接近 ReentrantLock | 通常性能较好,特别是在高竞争环境下 |
使用复杂度 | 简单,自动管理锁 | 较复杂,需要手动管理锁的获取和释放 |
适用场景 | 简单的同步需求,代码块或方法级同步 | 复杂的同步需求,需要更多灵活性和高级特性 |
下面是简单的测试代码
private int a = 0;
private synchronized void add(String name) {
System.out.println(name+"开始");
a++;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name+":"+a);
}
@Test
public void test() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(() -> {
System.out.println("a");
add("a");
});
executorService.submit(() -> {
System.out.println("b");
add("b");
});
executorService.submit(() -> {
System.out.println("c");
add("c");
});
executorService.submit(() -> {
System.out.println("d");
add("d");
});
// 关闭线程池,不再接受新的任务
executorService.shutdown();
// 等待所有已提交的任务完成,最长等待时间为1分钟
if (!executorService.awaitTermination(1, TimeUnit.MINUTES)) {
executorService.shutdownNow(); // 如果超时则强制关闭线程池
}
}
// 输出
a
a开始
b
c
d
a:1
d开始
d:2
c开始
c:3
b开始
b:4
总结
对于“心情追忆”小程序而言,考虑到支付操作的敏感性,采用ReentrantLock
可能是更为合适的选择。这不仅因为其提供了比synchronized
更强大的功能,还因为它可以更好地控制锁的获取方式,确保支付操作的原子性和一致性。在实际应用中,应当根据具体的业务需求选择最合适的锁策略,以达到最佳的并发控制效果。