一、Java锁机制的起点:synchronized
在Java编程里,多线程操作是很常见的。可当多个线程同时访问共享资源时,就容易出问题,像数据不一致啥的。这时候,就需要锁机制来保证数据的安全。synchronized 就是 Java 里最基础的锁机制。
基本用法
synchronized 可以修饰方法或者代码块。咱先看修饰方法的例子:
// Java 技术栈
class SynchronizedExample {
private int count = 0;
// 同步方法,保证同一时间只有一个线程能执行该方法
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个例子里,increment 方法被 synchronized 修饰了,这就意味着同一时间只能有一个线程执行这个方法。要是有其他线程想执行这个方法,就得等着当前线程执行完。
再看看修饰代码块的例子:
// Java 技术栈
class SynchronizedBlockExample {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// 同步代码块,使用 lock 对象作为锁
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
这里用了一个 lock 对象作为锁,把要同步的代码放在 synchronized 代码块里。这样,同一时间也只有一个线程能执行这个代码块。
应用场景
synchronized 适用于对共享资源的简单同步操作。比如多个线程同时对一个计数器进行操作,就可以用 synchronized 来保证计数器的正确性。
优缺点
优点:使用简单,能有效防止多个线程同时访问共享资源,保证数据的一致性。 缺点:性能比较低,因为它是一种独占锁,同一时间只能有一个线程访问被锁住的资源。而且一旦线程持有了锁,其他线程就得一直等待,可能会造成线程阻塞。
注意事项
使用 synchronized 时,要注意锁的范围。如果锁的范围太大,会影响性能;如果锁的范围太小,可能无法保证数据的一致性。
二、ReentrantLock:更灵活的锁
ReentrantLock 是 Java 里另一种常用的锁机制,它比 synchronized 更灵活。
基本用法
// Java 技术栈
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockExample {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
// 获取锁
lock.lock();
try {
count++;
} finally {
// 释放锁,确保在任何情况下锁都会被释放
lock.unlock();
}
}
public int getCount() {
return count;
}
}
在这个例子里,使用 ReentrantLock 的 lock 方法获取锁,用 unlock 方法释放锁。要注意的是,释放锁的操作要放在 finally 块里,这样能保证无论是否发生异常,锁都会被释放。
应用场景
ReentrantLock 适用于需要更灵活控制锁的场景。比如可以使用 tryLock 方法尝试获取锁,如果获取不到可以做其他操作,而不是一直等待。
// Java 技术栈
import java.util.concurrent.locks.ReentrantLock;
class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
if (lock.tryLock()) {
try {
// 获取到锁,执行操作
System.out.println("获取到锁,执行操作");
} finally {
lock.unlock();
}
} else {
// 没获取到锁,做其他操作
System.out.println("没获取到锁,做其他操作");
}
}
}
优缺点
优点:比 synchronized 更灵活,可以实现公平锁,还能使用 tryLock 方法尝试获取锁。
缺点:使用起来相对复杂,需要手动获取和释放锁,如果忘记释放锁,会导致死锁。
注意事项
使用 ReentrantLock 时,一定要确保在 finally 块里释放锁,避免死锁。
三、ReadWriteLock:读写分离的锁
在很多场景下,读操作是可以同时进行的,只有写操作才需要互斥。ReadWriteLock 就是为了这种场景设计的。
基本用法
// Java 技术栈
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReadWriteLockExample {
private int data = 0;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void writeData(int newData) {
// 获取写锁
lock.writeLock().lock();
try {
data = newData;
} finally {
// 释放写锁
lock.writeLock().unlock();
}
}
public int readData() {
// 获取读锁
lock.readLock().lock();
try {
return data;
} finally {
// 释放读锁
lock.readLock().unlock();
}
}
}
在这个例子里,使用 ReentrantReadWriteLock 创建了一个读写锁。写操作使用写锁,同一时间只能有一个线程进行写操作;读操作使用读锁,多个线程可以同时进行读操作。
应用场景
适用于读多写少的场景,比如缓存系统。读操作可以同时进行,提高了并发性能。
优缺点
优点:在读多写少的场景下,能显著提高并发性能。 缺点:如果写操作比较频繁,会导致读操作长时间等待,性能下降。
注意事项
使用 ReadWriteLock 时,要根据实际情况合理分配读写锁的使用,避免写操作饥饿。
四、StampedLock:新一代的锁机制
StampedLock 是 Java 8 引入的一种新的锁机制,它结合了读写锁和乐观锁的特点。
基本用法
// Java 技术栈
import java.util.concurrent.locks.StampedLock;
class StampedLockExample {
private int data = 0;
private final StampedLock lock = new StampedLock();
public void writeData(int newData) {
// 获取写锁
long stamp = lock.writeLock();
try {
data = newData;
} finally {
// 释放写锁
lock.unlockWrite(stamp);
}
}
public int readData() {
// 乐观读
long stamp = lock.tryOptimisticRead();
int currentData = data;
// 检查在读取过程中是否有写操作
if (!lock.validate(stamp)) {
// 有写操作,升级为悲观读
stamp = lock.readLock();
try {
currentData = data;
} finally {
// 释放读锁
lock.unlockRead(stamp);
}
}
return currentData;
}
}
在这个例子里,使用 StampedLock 进行读写操作。读操作先尝试乐观读,如果在读取过程中没有写操作,就直接返回数据;如果有写操作,就升级为悲观读。
应用场景
适用于读多写少,且读操作不要求实时性的场景。比如一些缓存数据的读取。
优缺点
优点:性能比 ReadWriteLock 更高,尤其是在读多写少的场景下。 缺点:使用起来比较复杂,不适合初学者。
注意事项
使用 StampedLock 时,要注意乐观读和悲观读的转换,避免出现数据不一致的问题。
文章总结
从 synchronized 到 StampedLock,Java 的锁机制不断演进,越来越灵活,性能也越来越好。synchronized 是最基础的锁机制,使用简单,但性能较低;ReentrantLock 更灵活,可以实现公平锁和尝试获取锁;ReadWriteLock 适用于读多写少的场景,提高了并发性能;StampedLock 结合了读写锁和乐观锁的特点,性能更高。在实际开发中,要根据具体的应用场景选择合适的锁机制。
评论