系列文章目录
第二章 Java进阶篇之并发控制:掌握锁、信号量与屏障的艺术
第三章 Java进阶篇之并发工具类:深入Atomic、Concurrent与BlockingQueue
第四章 Java进阶篇之线程池(Executor框架)深度解析
第五章 Java进阶篇之Fork/Join 框架:并行处理的艺术
目录
(1)锁(Locks) vs. 信号量(Semaphores)
前言
在多线程编程的世界里,Java提供了丰富的工具和API来帮助我们控制并发,确保数据的一致性和程序的正确性。本文将深入探讨Java中用于并发控制的关键概念——锁、信号量以及屏障,这些是Java并发编程中的高级主题,也是构建高效、健壮的多线程应用程序的基石。
一、锁(Locks)
synchronized
和ReentrantLock
都是Java中用于线程同步的重要机制,它们允许在一个多线程环境中控制对共享资源的访问,从而避免竞态条件和数据不一致性问题。尽管它们都能实现线程同步,但在使用方式、灵活性和性能上有明显的差异。
synchronized关键字
synchronized
是Java语言的关键字,它可以直接作用于实例方法、静态方法或代码块。当一个线程进入synchronized
代码块或方法时,它会自动获取锁,并在退出时自动释放锁。这种机制简化了代码,但也可能导致以下局限性:
- 自动获取和释放锁:
synchronized
关键字不需要显式的调用锁的获取和释放操作,这简化了使用,但也意味着如果发生异常,锁会在异常抛出时自动释放,避免死锁。 - 非公平锁:
synchronized
总是实现非公平锁,即新来的线程可能在等待队列中的线程之前获取锁。 - 没有超时机制:
synchronized
没有提供尝试获取锁的超时选项,如果无法立即获取锁,线程将一直等待。 - 缺乏中断响应:在等待锁的过程中,线程不能响应中断请求。
- 性能:在Java早期版本中,
synchronized
的性能较差,因为它依赖于JVM级别的实现,但随着JDK版本的更新,synchronized
的性能得到了很大提升,尤其是在JDK 6之后。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
ReentranLock
ReentrantLock
是java.util.concurrent.locks
包中提供的一个接口实现,它提供了比synchronized
更强大的锁机制。ReentrantLock
允许更细粒度的控制:
- 手动获取和释放锁:使用
ReentrantLock
时,你需要显式地调用lock()
方法获取锁,以及unlock()
方法释放锁。这增加了灵活性,但也要求程序员必须小心处理,以避免死锁。 - 公平锁或非公平锁:
ReentrantLock
允许你选择是否使用公平锁,公平锁按照线程请求锁的顺序分配锁,而非公平锁可能会让新来的线程抢先获取锁。 - 超时机制:
ReentrantLock
提供了尝试获取锁的超时选项,如果在指定时间内未能获取锁,则返回失败,而不是无限期等待。 - 响应中断:在等待锁的过程中,
ReentrantLock
可以响应中断请求,使线程能够提前退出等待状态。 - Condition支持:
ReentrantLock
支持Condition
对象,可以更精细地控制线程的等待和唤醒,而synchronized
只支持单一的wait()
和notify()
或notifyAll()
。 - 性能:在某些情况下,
ReentrantLock
的性能优于synchronized
,尤其是当需要更复杂的锁机制时。
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
总结
synchronized
和ReentrantLock
各有优缺点,选择使用哪一个取决于具体的应用场景。如果你需要更简单的同步代码,并且对性能的要求不是非常高,synchronized
可能是一个合适的选择。然而,如果你需要更复杂的锁行为,如公平锁、响应中断、超时机制或使用Condition
,那么ReentrantLock
可能是更好的选择。在实际开发中,理解和评估这些差异对于编写高效、安全的多线程代码至关重要。
二、信号量(Semaphores)
信号量是一种控制多个线程对共享资源访问的同步工具。在Java中,java.util.concurrent.Semaphore
类提供了一个信号量的实现。
使用Semaphore
当我们需要限制同时访问某个资源的线程数量时,信号量就非常有用。例如,限制网络连接数或数据库连接数。
import java.util.concurrent.Semaphore;
public class ConnectionPool {
private final Semaphore semaphore = new Semaphore(10); // 限制最大连接数为10
public void acquireConnection() throws InterruptedException {
semaphore.acquire();
}
public void releaseConnection() {
semaphore.release();
}
}
三、屏障(Barriers)
屏障允许一组线程相互等待,直到每个线程都到达了屏障点。Java中java.util.concurrent.CyclicBarrier
和java.util.concurrent.CountDownLatch
是两种常用的屏障类。
CyclicBarrier
CyclicBarrier
支持重用,一旦所有线程到达屏障点后,它们将被释放并可以继续执行,而屏障则会被重置以供下一轮使用。
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;
public class Worker implements Runnable {
private final CyclicBarrier barrier;
public Worker(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
System.out.println("任务启动");
// 执行一些任务...
try {
barrier.await(); // 等待所有线程到达
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("所有工作任务结束");
}
}
CountDownLatch
CountDownLatch
是一个一次性使用的屏障,当计数器达到零时,所有等待的线程都会被释放。
import java.util.concurrent.CountDownLatch;
public class Worker implements Runnable {
private final CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
System.out.println("任务启动");
// 执行一些任务...
latch.countDown(); // 减少计数器
}
}
四、比较
(1)锁(Locks) vs. 信号量(Semaphores)
1. 用途与控制粒度:
- 锁主要用于保护临界区,确保同一时刻只有一个线程可以访问共享资源。锁通常用于细粒度的同步控制,比如保护一个变量或一段代码。
- 信号量则用于控制对一组相关资源的访问,它允许一定数量的线程同时访问资源。信号量适用于更粗粒度的资源管理,例如限制系统中的文件句柄数量或数据库连接数。
2. 可重入性与公平性:
- 锁可以是可重入的,这意味着已经拥有锁的线程可以再次获取同一个锁而不导致死锁。
ReentrantLock
支持设置为公平或非公平锁。 - 信号量也支持公平性设置,但通常不涉及可重入的概念,因为其主要关注的是资源的数量而非同一线程多次获取资源的问题。
3. API与使用方式:
- 锁通常通过
try/finally
块来保证即使发生异常也能释放锁,防止死锁。 - 信号量的
acquire()
和release()
方法分别用于获取和释放资源,其中acquire()
可能阻塞直到有足够的资源可用。
(2)屏障(Barriers) vs. 锁与信号量
1. 目的与场景:
- 锁和信号量主要用于同步线程对共享资源的访问,避免竞态条件。
- 屏障(如
CyclicBarrier
和CountDownLatch
)则用于协调多个线程的执行顺序,让一组线程在继续之前必须等待所有线程到达某个点。
2. 可重用性与一次性:
- 锁和信号量在释放后可以被再次获取,因此它们是可重用的。
CyclicBarrier
可以在每次所有线程到达屏障后自动重置,因此它可以重复使用。而CountDownLatch
在计数器到达零后不能重置,只能作为一次性屏障使用。
3. 行为特性:
- 锁和信号量的行为更加直接,主要是控制访问权限。
- 屏障除了控制线程执行外,还可能触发某些动作(如
CyclicBarrier
的barrierAction
),并且它们的使用通常与特定的多线程算法或模式紧密相关。
(3)总结
- 锁适用于细粒度的资源保护,确保原子性和排他性。
- 信号量适合于管理一组有限资源的访问,提供了一种定量控制并发访问的方法。
- 障碍物(如
CyclicBarrier
和CountDownLatch
)用于线程间的协调,特别适合于需要同步开始或结束的多线程场景。
结语
锁、信号量和屏障是Java并发编程中不可或缺的工具。理解并熟练运用它们,可以显著提高你的多线程应用程序的性能和稳定性。希望本文能帮助你更好地掌握Java的并发控制技术,为你的项目带来更高的并发处理能力。