1. synchronized和ReentrantLock的区别
- 实现机制
- synchronized 是Java中的关键字,它是基于JVM内部实现的。JVM会自动对 synchronized 修饰的方法或代码块进行加锁和解锁操作。例如:
public class SynchronizedExample {
public synchronized void method() {
// 代码块
}
}
当一个线程访问 method 方法时,JVM会自动在方法的入口处获取对象锁,在方法执行结束后自动释放锁。
- ReentrantLock 是一个Java类,它属于 java.util.concurrent.locks 包。它通过显式地调用 lock 方法来获取锁,调用 unlock 方法来释放锁。例如:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 代码块
} finally {
lock.unlock();
}
}
}
这种显式的加锁和解锁操作需要开发者自己保证 unlock 方法一定会被调用,通常会放在 finally 块中,以防止出现异常时锁无法释放。
- 功能特性
- ReentrantLock 提供了更灵活的功能。它可以实现公平锁和非公平锁,通过构造函数 ReentrantLock(boolean fair) 来指定。例如, new ReentrantLock(true) 创建一个公平锁。公平锁是指按照线程请求锁的顺序来分配锁,而非公平锁则不保证这一点,允许插队。 synchronized 是一种非公平锁。
- ReentrantLock 提供了 tryLock 方法。 tryLock 方法可以尝试获取锁,如果获取成功则返回 true ,否则返回 false ,而不会像 synchronized 那样一直阻塞等待。它还有 tryLock(long timeout, TimeUnit unit) 方法,可以在指定的时间内尝试获取锁。
- 性能方面
- 在低竞争情况下(即很少有线程同时竞争锁), synchronized 的性能和 ReentrantLock 差不多。但在高竞争情况下, ReentrantLock 的性能可能会更好,因为它可以通过更灵活的策略来处理锁的竞争。不过, synchronized 在后续的Java版本中不断优化,性能差距在逐渐缩小。
2. Atomic的原理
- 原子性操作
- Atomic 类(如 AtomicInteger 、 AtomicLong 等)是Java提供的用于实现原子性操作的类。原子性操作是指不可被中断的操作,在多线程环境下,一个原子操作一旦开始,就会一直执行到结束,不会被其他线程干扰。以 AtomicInteger 为例,它提供了一些原子性的方法,如 getAndIncrement 、 incrementAndGet 等。
- 基于CAS(Compare - And - Swap)机制
- 其核心原理是CAS。CAS操作包含三个操作数:内存位置(V)、旧的预期值(A)和新值(B)。在更新变量时,首先会比较内存位置的值与旧的预期值,如果相等,则将内存位置的值更新为新值;如果不相等,则说明有其他线程已经修改了该值,操作就会失败。例如, AtomicInteger 的 getAndIncrement 方法的实现大致如下:
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
这里, compareAndSet 方法就是实现CAS操作的方法。它会不断地尝试,直到成功地将值更新为 next 。这种方式避免了使用锁带来的开销,在高并发场景下,当竞争不是特别激烈时,可以提供高效的原子操作。
3. 乐观锁和悲观锁的理解、实现及实现方式
- 理解乐观锁和悲观锁
- 乐观锁:乐观地认为在大部分情况下,数据在并发访问时不会产生冲突。它不会对数据进行加锁,而是在更新数据时,通过一种机制来检查数据是否被其他线程修改过。如果没有被修改,则进行更新;如果被修改了,则根据具体的策略(如重试或者抛出异常)来处理。
- 悲观锁:悲观地认为在数据的并发访问过程中,很容易产生冲突。所以在访问数据时,就会先对数据进行加锁,以防止其他线程对数据进行访问。只有持有锁的线程才能对数据进行操作,当操作完成后才会释放锁。
- 实现方式
- 乐观锁实现
- 版本号机制:在数据库表中添加一个版本号(version)字段。例如,在更新数据时,先查询数据的版本号,在更新操作中带上版本号的条件判断。假设要更新一条用户记录,SQL语句可能如下:
UPDATE user_table SET name = 'new_name', version = version + 1 WHERE id = 1 AND version = 1;
这里,只有当 id = 1 的记录的版本号是 1 时,才会更新成功,并且更新后版本号会加 1 。
- CAS机制(如前面Atomic类中提到的):在Java中, Atomic 类就是基于CAS实现的乐观锁。在不使用锁的情况下,通过不断地比较和交换来实现对数据的原子性操作。
- 悲观锁实现
- 在Java中使用 synchronized 关键字:当 synchronized 修饰方法或者代码块时,就是一种悲观锁的实现。例如:
public class PessimisticLockExample {
private int data;
public synchronized void updateData() {
// 对data进行修改操作
}
}
当一个线程访问 updateData 方法时,会获取对象锁,其他线程必须等待该线程释放锁后才能访问。
- 在数据库中使用排它锁(FOR UPDATE):在SQL中,例如在关系型数据库(如MySQL)中,当执行 SELECT...FOR UPDATE 语句时,会对查询到的数据行加排它锁。例如:
SELECT * FROM user_table WHERE id = 1 FOR UPDATE;
这条SQL语句会对 id = 1 的记录加排它锁,其他事务对同一记录的读取和修改操作都会被阻塞,直到当前事务释放锁。
4. synchronized是哪种锁的实现
- synchronized 是一种可重入的互斥锁(排他锁)实现。
- 可重入性:例如,一个线程在进入一个被 synchronized 修饰的方法后,如果这个方法内部又调用了另一个被 synchronized 修饰的方法(属于同一个对象),该线程可以直接进入而不需要重新获取锁。这是因为 Java 在实现 synchronized 时,会记录锁的持有线程和持有次数。
- 互斥性:当一个线程获取了对象的 synchronized 锁后,其他线程想要访问同一个对象的被 synchronized 修饰的方法或者代码块时,必须等待该线程释放锁。例如,在一个多线程的 Java 类中:
public class SynchronizedMutexExample {
public synchronized void method1() {
// 代码块
}
public synchronized void method2() {
// 代码块
}
}
如果线程A正在执行 method1 ,线程B想要执行 method1 或者 method2 ,就必须等待线程A释放对象锁。
5. new ReentrantLock()创建的是公平锁还是非公平锁
- new ReentrantLock() 创建的是一个非公平锁。
- 非公平锁的特点是当锁可用时,新到达的线程可以立即获取锁,而不必等待等待队列中的线程。这样在高并发情况下可能会导致某些线程长时间等待,但是非公平锁的性能通常比公平锁要好,因为它减少了线程切换和等待的开销。如果需要创建公平锁,可以使用 new ReentrantLock(true) 。