Java中Lock的类别及应用场景
Java中Lock的类别及应用场景
在Java并发编程领域,Lock
接口为控制多线程对共享资源的访问提供了一种灵活且强大的方式。相较于传统的synchronized
关键字,Lock
接口提供了更细粒度的控制、更灵活的锁获取和释放机制以及更多的高级特性。Java提供了多种实现Lock
接口的类,每种都有其独特的应用场景。下面我们将详细探讨这些Lock
的类别及其应用场景,并通过代码示例加深理解。
1. ReentrantLock
1.1 基本概念
ReentrantLock
是Java.util.concurrent包下最常用的锁实现之一,它是一种可重入的互斥锁。“可重入”意味着同一个线程可以多次获取同一个锁而不会造成死锁,每次获取锁时,锁的持有计数会增加,每次释放锁时,持有计数会减少,当持有计数为0时,锁被完全释放。
1.2 特性
- 可中断的获取锁:
ReentrantLock
提供了lockInterruptibly()
方法,允许在获取锁的过程中响应中断,而synchronized
关键字不具备此特性。当一个线程在等待获取synchronized
锁时,是不能被中断的,除非持有锁的线程释放锁。 - 公平性选择:
ReentrantLock
可以选择公平锁或非公平锁模式。默认情况下,ReentrantLock
是非公平的,这意味着在锁可用时,等待时间最长的线程不一定会优先获取锁,新到达的线程可能会“插队”获取锁。而公平锁则会按照线程等待的先后顺序分配锁,保证先来先得。虽然公平锁看起来更公平,但在高并发场景下,由于线程切换带来的开销,其性能通常不如非公平锁。
1.3 应用场景
- 高竞争场景下的优化:在高竞争场景中,非公平的
ReentrantLock
由于减少了线程切换的开销,通常能提供更好的性能。例如,在一个多线程访问共享资源的服务器应用中,大量线程频繁竞争锁,如果使用公平锁,频繁的线程上下文切换会导致性能下降。而非公平锁允许新到达的线程有机会快速获取锁,减少了线程等待时间,提高了整体吞吐量。 - 需要可中断获取锁的场景:在一些任务执行过程中,可能需要在等待锁的过程中响应中断信号,以便及时取消任务。例如,在一个长时间运行的计算任务中,用户可能希望通过外部操作(如点击取消按钮)来中断任务。如果任务在获取锁时使用
ReentrantLock
的lockInterruptibly()
方法,就可以在等待锁的过程中响应中断,及时停止任务。
1.4 代码示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static final ReentrantLock lock = new ReentrantLock();
private static int sharedResource = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 5; i++) {
sharedResource++;
System.out.println(Thread.currentThread().getName() + " incremented sharedResource to " + sharedResource);
}
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 5; i++) {
sharedResource--;
System.out.println(Thread.currentThread().getName() + " decremented sharedResource to " + sharedResource);
}
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final value of sharedResource: " + sharedResource);
}
}
在上述代码中,ReentrantLock
确保了在多线程环境下对sharedResource
的安全访问。lock()
方法用于获取锁,unlock()
方法用于释放锁,并且在finally
块中释放锁,以确保即使在代码块中发生异常,锁也能被正确释放。
2. ReentrantReadWriteLock
2.1 基本概念
ReentrantReadWriteLock
是一种读写锁,它将对共享资源的访问分为读操作和写操作。允许多个线程同时进行读操作,因为读操作不会修改共享资源,不会产生数据不一致问题。而写操作则需要独占锁,以防止数据冲突。该锁也是可重入的,同一个线程可以多次获取读锁或写锁。
2.2 特性
- 读写分离:读锁可以被多个线程同时持有,而写锁则是独占的。这种分离机制大大提高了并发性能,特别是在读多写少的场景中。
- 锁降级:
ReentrantReadWriteLock
支持锁降级,即持有写锁的线程可以在不释放写锁的情况下获取读锁,然后释放写锁,这样就实现了从写锁到读锁的降级。锁降级通常用于在写操作完成后,需要继续进行读操作的场景,避免了先释放写锁再获取读锁可能带来的数据不一致问题。
2.3 应用场景
- 读多写少的场景:在许多应用中,数据的读取操作远远多于写入操作,如数据库缓存、配置文件读取等场景。例如,一个在线新闻网站,大量用户同时访问新闻内容(读操作),而只有管理员偶尔更新新闻(写操作)。使用
ReentrantReadWriteLock
可以让多个用户并发读取新闻内容,而在管理员更新新闻时,通过获取写锁来保证数据的一致性。 - 数据缓存场景:在缓存系统中,缓存数据被频繁读取,而只有在数据更新时才需要写入操作。例如,一个电商应用的商品信息缓存,多个线程会频繁读取商品的价格、库存等信息,而只有在商品信息发生变化时才会进行写入操作。使用读写锁可以显著提高缓存系统的并发性能。
2.4 代码示例
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private static int sharedData = 0;
public static void main(String[] args) {
Thread writerThread = new Thread(() -> {
writeLock.lock();
try {
sharedData++;
System.out.println(Thread.currentThread().getName() + " updated sharedData to " + sharedData);
} finally {
writeLock.unlock();
}
});
Thread readerThread1 = new Thread(() -> {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " read sharedData as " + sharedData);
} finally {
readLock.unlock();
}
});
Thread readerThread2 = new Thread(() -> {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " read sharedData as " + sharedData);
} finally {
readLock.unlock();
}
});
writerThread.start();
readerThread1.start();
readerThread2.start();
try {
writerThread.join();
readerThread1.join();
readerThread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述代码中,writeLock
用于保护写操作,确保在写操作进行时,其他线程不能进行读写操作。readLock
用于保护读操作,允许多个线程同时进行读操作。
3. StampedLock
3.1 基本概念
StampedLock
是Java 8引入的一种新型锁,它提供了一种乐观读的机制,进一步提高了并发性能。StampedLock
使用一个时间戳(stamp)来表示锁的状态,每次获取锁时会返回一个时间戳,释放锁时需要使用这个时间戳。
3.2 特性
- 乐观读:
StampedLock
允许线程在没有获取写锁的情况下进行乐观读操作。线程首先尝试获取一个乐观读的时间戳,然后读取数据。在读取完成后,通过验证时间戳来判断在读取过程中数据是否被修改。如果没有被修改,则读操作成功;如果被修改,则需要重新获取读锁或写锁进行读取。 - 读锁升级为写锁:
StampedLock
支持读锁升级为写锁,与ReentrantReadWriteLock
的锁降级相反。这种机制在某些场景下非常有用,例如在对数据进行读取后,发现需要对数据进行修改,此时可以直接将读锁升级为写锁,而不需要先释放读锁再获取写锁,减少了锁竞争。
3.3 应用场景
- 高并发且数据一致性要求不是特别严格的场景:在一些实时性要求较高,但对数据一致性要求相对宽松的场景中,
StampedLock
的乐观读机制可以显著提高并发性能。例如,在一个股票交易系统中,实时显示股票价格的功能可以使用乐观读,因为即使在显示过程中股票价格发生了微小变化,对用户体验影响不大,但可以大大提高系统的并发处理能力。 - 需要读锁升级为写锁的场景:在一些业务逻辑中,先读取数据,然后根据读取结果决定是否需要修改数据。例如,在一个用户信息管理系统中,先读取用户的基本信息,然后根据业务规则判断是否需要更新用户的某些属性。使用
StampedLock
可以方便地将读锁升级为写锁,避免了复杂的锁获取和释放操作。
3.4 代码示例
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private static final StampedLock lock = new StampedLock();
private static int sharedValue = 0;
public static void main(String[] args) {
Thread writerThread = new Thread(() -> {
long stamp = lock.writeLock();
try {
sharedValue++;
System.out.println(Thread.currentThread().getName() + " updated sharedValue to " + sharedValue);
} finally {
lock.unlockWrite(stamp);
}
});
Thread readerThread = new Thread(() -> {
long stamp = lock.tryOptimisticRead();
int value = sharedValue;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
value = sharedValue;
System.out.println(Thread.currentThread().getName() + " read sharedValue as " + value);
} finally {
lock.unlockRead(stamp);
}
} else {
System.out.println(Thread.currentThread().getName() + " optimistically read sharedValue as " + value);
}
});
writerThread.start();
readerThread.start();
try {
writerThread.join();
readerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述代码中,writerThread
获取写锁来更新sharedValue
。readerThread
首先尝试乐观读,如果乐观读验证失败,则获取读锁进行读取。
4. Condition
4.1 基本概念
Condition
是与Lock
接口紧密相关的一个接口,它提供了一种更灵活的线程间通信机制,类似于传统synchronized
关键字中的wait()
和notify()
方法。每个Lock
对象可以创建多个Condition
对象,每个Condition
对象可以用于线程之间的特定通知和等待。
4.2 特性
- 精准通知:与
synchronized
中的notifyAll()
方法不同,Condition
的signal()
方法可以精准地唤醒在该Condition
上等待的一个线程,signalAll()
方法可以唤醒在该Condition
上等待的所有线程。这使得线程间的通信更加灵活和高效。 - 等待队列:每个
Condition
都有自己的等待队列,线程在调用await()
方法后会进入该队列等待,当调用signal()
或signalAll()
方法时,相应的线程会从队列中被唤醒。
4.3 应用场景
- 生产者 - 消费者模型:在生产者 - 消费者模型中,生产者线程生产数据并放入共享队列,消费者线程从队列中取出数据。当队列满时,生产者线程需要等待;当队列空时,消费者线程需要等待。使用
Condition
可以很方便地实现这种线程间的协作。例如,在一个消息队列系统中,生产者线程将消息放入队列,当队列达到最大容量时,生产者线程等待;消费者线程从队列中取出消息,当队列为空时,消费者线程等待。 - 多线程协作任务:在一些复杂的多线程任务中,不同线程需要根据特定条件进行协作。例如,在一个分布式计算任务中,一个主线程负责分配任务给多个子线程,当所有子线程完成任务后,主线程再进行下一步操作。使用
Condition
可以实现主线程等待所有子线程完成任务的通知,然后进行统一处理。
4.4 代码示例
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static boolean flag = false;
public static void main(String[] args) {
Thread waitingThread = new Thread(() -> {
lock.lock();
try {
while (!flag) {
System.out.println(Thread.currentThread().getName() + " is waiting...");
condition.await();
}
System.out.println(Thread.currentThread().getName() + " has been signaled.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread signalingThread = new Thread(() -> {
lock.lock();
try {
flag = true;
System.out.println(Thread.currentThread().getName() + " is signaling...");
condition.signal();
} finally {
lock.unlock();
}
});
waitingThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
signalingThread.start();
try {
waitingThread.join();
signalingThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述代码中,waitingThread
在flag
为false
时调用condition.await()
方法进入等待状态,signalingThread
在设置flag
为true
后调用condition.signal()
方法唤醒waitingThread
。
5. Semaphore
5.1 基本概念
Semaphore
是一种计数信号量,它通过一个计数器来控制同时访问共享资源的线程数量。当一个线程获取Semaphore
时,计数器会减1;当一个线程释放Semaphore
时,计数器会加1。当计数器为0时,其他线程无法获取Semaphore
,只能等待。
5.2 特性
- 控制并发访问数量:
Semaphore
的主要作用是控制同时访问某个资源或执行某个操作的线程数量。例如,可以设置Semaphore
的初始计数为5,那么最多允许5个线程同时获取Semaphore
,从而同时访问共享资源。 - 公平性选择:与
ReentrantLock
类似,Semaphore
也可以选择公平或非公平模式。公平模式下,线程按照等待的先后顺序获取Semaphore
;非公平模式下,新到达的线程可能会优先获取Semaphore
。
5.3 应用场景
- 资源限流:在一些系统中,某些资源的数量是有限的,如数据库连接池中的连接数量、线程池中的线程数量等。使用
Semaphore
可以限制同时使用这些资源的线程数量,防止资源耗尽。例如,在一个Web应用中,数据库连接池有100个连接,为了避免过多线程同时请求数据库连接导致连接池耗尽,可以使用Semaphore
来限制同时获取数据库连接的线程数量为100。 - 控制并发任务数量:在一些任务执行场景中,可能需要限制同时执行的任务数量,以避免系统资源过度消耗。例如,在一个文件下载系统中,为了防止过多的下载任务同时占用网络带宽和磁盘I/O资源,可以使用
Semaphore
来限制同时进行的下载任务数量。
5.4 代码示例
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " acquired semaphore.");
// 模拟任务执行
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " released semaphore.");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
在上述代码中,Semaphore
的初始计数为3,意味着最多允许3个线程同时获取Semaphore
。5个线程尝试获取Semaphore
,前3个线程可以立即获取并执行任务,后2个线程需要等待前面的线程释放Semaphore
。
6. CountDownLatch
6.1 基本概念
CountDownLatch
是一个同步辅助类,它允许一个或多个线程等待,直到其他一组线程完成一系列操作。CountDownLatch
通过一个计数器来工作,初始化时设置一个初始值,每个线程在完成任务后调用countDown()
方法将计数器减1,当计数器减到0时,所有等待在CountDownLatch
上的线程将被释放。
6.2 特性
- 一次性使用:
CountDownLatch
是一次性使用的,一旦计数器减到0,就不能再重新设置初始值。如果需要重复使用类似的功能,可以考虑使用CyclicBarrier
。 - 线程等待:
await()
方法用于使当前线程等待,直到计数器为0。可以设置等待的超时时间,如果在超时时间内计数器没有减到0,await()
方法将返回false
。
6.3 应用场景
- 等待所有任务完成:在一些任务执行场景中,需要等待所有子任务完成后再进行下一步操作。例如,在一个数据处理系统中,有多个线程分别处理不同部分的数据,当所有线程完成数据处理后,需要将处理结果进行汇总。此时可以使用
CountDownLatch
,每个子任务完成后调用countDown()
方法,主线程在调用await()
方法等待所有子任务完成。 - 并发性能测试:在进行并发性能测试时,可能需要同时启动多个线程来模拟并发场景,并且希望在所有线程都准备好后再同时开始执行任务。可以使用
CountDownLatch
来实现这种同步,主线程在所有线程调用await()
方法后,调用countDown()
方法,使所有线程同时开始执行任务。
6.4 代码示例
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
private static final CountDownLatch latch = new CountDownLatch(3);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is working...");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " has finished.");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is working...");
Thread.sleep(1500);
System.out.println(Thread.currentThread().getName() + " has finished.");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread thread3 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is working...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " has finished.");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
thread3.start();
try {
System.out.println(Thread.currentThread().getName() + " is waiting for all threads to finish.");
latch.await();
System.out.println(Thread.currentThread().getName() + " all threads have finished.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上述代码中,3个线程分别模拟不同的任务执行,每个线程完成任务后调用latch.countDown()
方法。主线程调用latch.await()
方法等待所有线程完成任务,当所有线程都调用了countDown()
方法后,主线程继续执行。
7. CyclicBarrier
7.1 基本概念
CyclicBarrier
也是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共的屏障点(barrier point)。与CountDownLatch
不同的是,CyclicBarrier
可以重复使用。当所有线程都到达屏障点后,CyclicBarrier
会执行一个可选的Runnable
任务,然后所有线程可以继续执行,并且CyclicBarrier
可以被重新使用。
7.2 特性
- 可循环使用:
CyclicBarrier
在所有线程到达屏障点后,会将内部计数器重置,可以再次使用。这使得它适用于需要多次同步的场景。 - 屏障动作:
CyclicBarrier
可以设置一个Runnable
任务,当所有线程到达屏障点时,会执行这个任务。这个任务可以用于进行一些汇总、初始化等操作。
7.3 应用场景
- 多阶段任务同步:在一些复杂的任务中,可能需要将任务分为多个阶段,每个阶段需要所有线程都完成前一阶段的任务后才能继续。例如,在一个分布式机器学习训练过程中,每一轮训练都需要所有节点完成当前轮的计算后,才能开始下一轮训练。使用
CyclicBarrier
可以方便地实现这种多阶段任务的同步。 - 并发数据处理:在一些数据处理场景中,可能需要将数据分成多个部分,由多个线程并行处理,然后在每个处理阶段结束后,对结果进行汇总或其他操作。例如,在一个大数据分析任务中,将数据按行分成多个部分,由多个线程并行处理每行数据,每个线程处理完后,等待所有线程都处理完,然后进行结果汇总。
CyclicBarrier
可以用于实现这种并发数据处理过程中的同步。
7.4 代码示例
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private static final CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads have reached the barrier. Starting next phase...");
});
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is working...");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " has reached the barrier.");
barrier.await();
System.out.println(Thread.currentThread().getName() + " is continuing after the barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread thread2 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is working...");
Thread.sleep(1500);
System.out.println(Thread.currentThread().getName() + " has reached the barrier.");
barrier.await();
System.out.println(Thread.currentThread().getName() + " is continuing after the barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
Thread thread3 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " is working...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " has reached the barrier.");
barrier.await();
System.out.println(Thread.currentThread().getName() + " is continuing after the barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
thread3.start();
}
}
在上述代码中,3个线程分别模拟不同的任务执行,每个线程在完成任务后调用barrier.await()
方法等待其他线程。当所有线程都调用await()
方法后,CyclicBarrier
会执行设置的Runnable
任务,然后所有线程继续执行。
通过对以上各种Lock
类别及其应用场景的详细介绍和代码示例,希望能帮助开发者在Java并发编程中根据具体需求选择合适的锁机制,从而提高程序的性能和稳定性。在实际应用中,需要根据业务场景、并发量、数据一致性要求等多方面因素综合考虑,选择最适合的锁方案。