在多线程编程中,同步和互斥是两个核心概念,它们确保了线程之间能够正确地共享数据,避免了数据不一致性和竞争条件。Java作为一种广泛使用的编程语言,提供了多种机制来实现线程同步和互斥。本文探讨Java中多线程同步和互斥的方法,包括synchronized
关键字、ReentrantLock
、ReadWriteLock
、Semaphore
、CountDownLatch
、CyclicBarrier
、Exchanger
、volatile
关键字、AtomicInteger/AtomicLong
以及StampedLock
。帮助开发同学理解每种方法的工作原理和使用场景。
1. synchronized关键字
synchronized
是Java中最基本也是最常见的同步机制。它可以修饰方法或代码块,确保同一时刻只有一个线程能够访问被同步的方法或代码块。
工作原理:
- 当一个线程访问一个对象的
synchronized
方法时,它会自动获得该对象的锁,其他线程则无法访问该对象的任何synchronized
方法,直到持有锁的线程释放锁。 - 对于
synchronized
代码块,线程需要显式地指定锁对象。当一个线程进入synchronized
代码块时,它会自动获得指定对象的锁,其他线程则无法进入该代码块,直到持有锁的线程退出代码块并释放锁。
优点:
- 简单易用,适合简单的同步需求。
- 由JVM直接支持,性能较高。
缺点:
- 不够灵活,无法实现一些高级的同步需求,如尝试获取锁、定时获取锁等。
- 可能导致线程阻塞和死锁。
代码示例:
public class SynchronizedExample {
private int count = 0;
// synchronized 方法
public synchronized void increment() {
count++;
}
// synchronized 代码块
public void incrementBlock() {
synchronized (this) {
count++;
}
}
// 省略了main方法等其余部分,与上文相同
}
2. ReentrantLock
ReentrantLock
是java.util.concurrent.locks
包中的一个显式锁实现,它提供了比synchronized
更灵活的锁操作。
工作原理:
ReentrantLock
通过显式地锁定和解锁来实现同步。它提供了lock()
和unlock()
方法来获取和释放锁。ReentrantLock
支持重入,即同一个线程可以多次获得锁。ReentrantLock
还提供了许多高级功能,如尝试获取锁(tryLock()
)、定时获取锁(tryLock(long timeout, TimeUnit unit)
)和可中断获取锁(lockInterruptibly()
)。
优点:
- 提供了丰富的锁操作,能够满足复杂的同步需求。
- 支持公平锁和非公平锁,可以根据需要选择。
缺点:
- 使用相对复杂,需要手动管理锁的获取和释放。
- 性能可能稍低于
synchronized
,因为ReentrantLock
是基于Java代码实现的,而synchronized
是由JVM直接支持的。
代码示例:
// 与上文相同,省略了导入语句和main方法等部分
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
// 省略了main方法等其余部分,与上文相同
}
3. ReadWriteLock
ReadWriteLock
是一种读写锁,它允许多个读线程同时访问共享资源,但在写线程访问时,所有其他线程(包括读线程)都被阻塞。
工作原理:
ReadWriteLock
提供了读锁和写锁两种锁。- 读锁是共享的,多个读线程可以同时获得读锁,从而同时访问共享资源。
- 写锁是独占的,只有一个写线程可以获得写锁,从而修改共享资源。
- 当有写锁时,所有其他锁(包括读锁和写锁)都会被阻塞,直到写锁被释放。
优点:
- 提高了读写并发性能,因为多个读线程可以同时访问共享资源。
- 适用于读多写少的场景。
缺点:
- 使用相对复杂,需要分别管理读锁和写锁。
- 可能导致写线程饥饿(长时间无法获得写锁)。
代码示例:
// 与上文相同,省略了导入语句和main方法等部分
public class ReadWriteLockExample {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private int value;
public void read() {
readWriteLock.readLock().lock();
try {
// 省略了输出语句,与上文相同
} finally {
readWriteLock.readLock().unlock();
}
}
public void write(int value) {
readWriteLock.writeLock().lock();
try {
// 省略了赋值和输出语句,与上文相同
} finally {
readWriteLock.writeLock().unlock();
}
}
// 省略了main方法等其余部分,与上文相同
}
4. Semaphore
Semaphore
是一个计数信号量,用于控制对某个资源的并发访问数量。
工作原理:
Semaphore
维护了一个许可证计数器,表示当前可用的资源数量。- 线程可以通过调用
acquire()
方法来获取一个许可证,如果许可证可用,则线程可以继续执行;如果许可证不可用,则线程会被阻塞,直到有许可证可用。 - 线程通过调用
release()
方法来释放一个许可证,从而允许其他线程获取许可证并访问资源。
优点:
- 能够控制对资源的并发访问数量,从而避免资源过载。
- 适用于资源池等场景。
缺点:
- 使用相对复杂,需要管理许可证的获取和释放。
- 可能导致线程阻塞和死锁。
代码示例:
// 与上文相同,省略了导入语句和main方法等部分
public class SemaphoreExample {
private final Semaphore semaphore = new Semaphore(3); // 允许最多3个线程同时访问
public void accessResource() {
try {
semaphore.acquire(); // 获取一个许可
// 省略了输出和模拟资源访问时间的语句,与上文相同
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放一个许可
}
}
// 省略了main方法等其余部分,与上文相同
}
5. CountDownLatch
CountDownLatch
是一个同步辅助类,用于让一个或多个线程等待一系列指定操作的完成。
工作原理:
CountDownLatch
内部维护了一个计数器,表示需要完成的操作数量。- 线程可以通过调用
await()
方法来等待计数器的值变为0。 - 每次完成一个操作时,线程可以调用
countDown()
方法来减少计数器的值。 - 当计数器的值变为0时,所有等待的线程都会被唤醒并继续执行。
优点:
- 简化了线程之间的等待和通知机制。
- 适用于等待多个任务完成的场景。
缺点:
- 使用相对复杂,需要管理计数器的初始值和操作完成时的计数减少。
- 无法重新计数,一旦计数器变为0,就无法再增加其值。
代码示例:
// 与上文相同,省略了导入语句和main方法等部分
public class CountDownLatchExample {
private final CountDownLatch latch = new CountDownLatch(3); // 等待3个操作完成
public void doWork() {
try {
// 省略了输出和模拟工作时间的语句,与上文相同
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 完成一个操作
}
}
public void awaitWork() throws InterruptedException {
latch.await(); // 等待所有操作完成
// 省略了输出语句,与上文相同
}
// 省略了main方法等其余部分,与上文相同
}
6. CyclicBarrier
CyclicBarrier
是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点(common barrier point)。
工作原理:
CyclicBarrier
内部维护了一个计数器,表示需要到达屏障点的线程数量。- 线程可以通过调用
await()
方法来等待其他线程到达屏障点。 - 当所有线程都到达屏障点时,
CyclicBarrier
会触发一个可选的回调(Runnable
),然后所有线程都会被唤醒并继续执行。 CyclicBarrier
是可循环的,即线程可以多次调用await()
方法来等待新的屏障点。
优点:
- 简化了线程之间的协调机制。
- 适用于需要多次等待和协调的场景。
缺点:
- 使用相对复杂,需要管理屏障点的线程数量和回调。
- 如果某个线程在到达屏障点前被中断或取消,可能会导致其他线程永远等待下去(除非发生超时或其他处理)。
代码示例:
import java.util.concurrent.*;
public class CyclicBarrierExample {
private final CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println("All threads have reached the barrier point.");
}
});
public void doWork() {
try {
// 模拟线程工作
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " is ready to await the barrier.");
barrier.await(); // 等待其他线程到达屏障点
System.out.println(Thread.currentThread().getName() + " has crossed the barrier.");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
CyclicBarrierExample example = new CyclicBarrierExample();
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
executor.execute(example::doWork);
}
executor.shutdown();
}
}
7. Exchanger
Exchanger
是一个同步辅助类,它允许两个线程在某个同步点(rendezvous point)交换数据。
工作原理:
Exchanger
提供了exchange()
方法,两个线程可以在该方法中交换数据。- 当一个线程调用
exchange()
方法时,它会被阻塞,直到另一个线程也调用exchange()
方法。 - 然后,两个线程会交换它们持有的数据,并继续执行。
优点:
- 简化了两个线程之间的数据交换机制。
- 适用于需要在线程之间传递数据的场景。
缺点:
- 使用相对复杂,需要确保两个线程都正确调用
exchange()
方法。 - 如果其中一个线程被中断或取消,另一个线程可能会无限等待。
代码示例:
import java.util.concurrent.*;
public class ExchangerExample {
private final Exchanger<String> exchanger = new Exchanger<>();
public void exchangeData(String data) {
try {
String response = exchanger.exchange(data);
System.out.println("Exchanged data: " + response);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
ExchangerExample example = new ExchangerExample();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> example.exchangeData("Data from Thread 1"));
executor.execute(() -> example.exchangeData("Data from Thread 2"));
executor.shutdown();
}
}
8. volatile关键字
volatile
是Java中的一个关键字,用于修饰变量,确保变量的可见性(visibility)和有序性(ordering)。
工作原理:
- 当一个字段被声明为
volatile
时,JVM会在读写该字段时加入内存屏障,确保所有线程都能看到最新的变量值。 volatile
还禁止了指令重排序,从而保证了有序性。
优点:
- 简单易用,适合简单的共享数据同步需求。
- 性能较高,因为
volatile
只是确保了变量的可见性和有序性,并没有引入锁机制。
缺点:
- 不适合复杂的同步需求,因为
volatile
无法保证原子性(atomicity)。 - 无法用于替代
synchronized
或ReentrantLock
等锁机制。
代码示例:
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag() {
flag = true;
}
public void checkFlag() {
while (!flag) {
// 等待flag变为true
}
System.out.println("Flag is now true.");
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
Thread setterThread = new Thread(example::setFlag);
Thread checkerThread = new Thread(example::checkFlag);
setterThread.start();
Thread.sleep(1000); // 模拟一些延迟
checkerThread.start();
}
}
9. AtomicInteger/AtomicLong
AtomicInteger
和AtomicLong
是Java中的原子类,它们提供了对整数和长整数的原子操作。
工作原理:
AtomicInteger
和AtomicLong
内部使用了volatile
变量和CAS(Compare-And-Swap)操作来确保原子性。- 它们提供了一系列以
atomic
开头的方法,如atomicIncrementAndGet()
、atomicDecrementAndGet()
等,这些方法在多线程环境下是线程安全的。
优点:
- 简单易用,适合对整数和长整数的原子操作需求。
- 性能较高,因为原子类是无锁的,使用了CAS操作来避免锁的开销。
缺点:
- 功能有限,只能对整数和长整数进行原子操作。
- 对于复杂的同步需求,原子类可能无法满足。
代码示例:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerExample {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.atomicIncrementAndGet();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerExample example = new AtomicIntegerExample();
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(example::increment);
threads[i].start();
}
for (Thread thread : threads) {
thread.join(); // 等待所有线程完成
}
System.out.println("Final count: " + example.getCount());
}
}
10. StampedLock
StampedLock
是Java 8引入的一种锁,它支持乐观读锁、悲观读锁和写锁三种模式。
工作原理:
StampedLock
在每次锁操作时都会返回一个称为戳记(stamp)的值,用于在后续的解锁或转换为其他锁模式时验证锁的状态是否未发生变化。- 乐观读锁允许多个线程同时读取数据,但在写入数据时可能会发生冲突,此时需要重新获取锁。
- 悲观读锁和写锁的行为类似于
ReentrantLock
,但它们在获取锁时会返回一个戳记。
优点:
- 提供了乐观读锁,提高了读取数据的并发性能。
- 支持锁模式的转换,灵活性较高。
缺点:
- 使用相对复杂,需要管理戳记和锁模式的转换。
- 性能可能稍低于
ReentrantLock
,因为StampedLock
需要额外的戳记验证。
代码示例:
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock stampedLock = new StampedLock();
private int count = 0;
public void increment() {
long stamp = stampedLock.writeLock();
try {
count++;
} finally {
stampedLock.unlockWrite(stamp);
}
}
public int getCount() {
long stamp = stampedLock.tryOptimisticRead();
int tempCount = count;
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock();
try {
tempCount = count;
} finally {
stampedLock.unlockRead(stamp);
}
}
return tempCount;
}
public static void main(String[] args) throws InterruptedException {
StampedLockExample example = new StampedLockExample();
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(example::increment);
threads[i].start();
}
for (Thread thread : threads) {
thread.join(); // 等待所有线程完成
}
System.out.println("Final count: " + example.getCount());
}
}
总结
同步机制 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
synchronized | 简单的同步需求,方法或代码块级别的互斥 | 易于使用,确保原子性和可见性 | 可能导致性能瓶颈,无法中断或超时控制 |
volatile | 频繁读取,不需要原子性操作的变量 | 确保变量的可见性和有序性,性能开销小 | 不提供原子性操作,不适用于复杂同步场景 |
ReentrantLock | 复杂的同步需求,需要灵活的锁控制 | 支持公平/非公平锁,可中断,可超时,多条件等待 | 使用复杂,需要手动管理锁的获取和释放 |
ReadWriteLock | 并发读取多,写入少的场景 | 分离读锁和写锁,提高并发读取性能 | 写锁会阻塞读锁,可能导致读线程饥饿 |
Semaphore | 需要限制并发线程数的资源访问控制 | 灵活控制资源访问数量,支持公平/非公平模式 | 使用复杂,需要管理信号量的获取和释放 |
CountDownLatch | 一个或多个线程等待其他线程完成后再继续执行 | 简单易用,适用于等待多个线程完成的场景 | 无法重用,一次性使用 |
CyclicBarrier | 一组线程在某个同步点等待,共同继续执行 | 支持循环使用,适用于需要多次同步的场景 | 复杂场景下难以调试,需要处理线程中断和超时 |
Exchanger | 两个线程在某个同步点交换数据 | 适用于线程间数据交换的场景 | 使用场景有限,仅适用于两个线程之间的数据交换 |
原子类(如AtomicInteger) | 需要高效执行简单原子操作的场景 | 性能高,无锁设计,线程安全 | 仅适用于简单的原子操作,不支持复杂逻辑 |
StampedLock | 高并发读取,偶尔写入的场景,需要锁模式转换 | 结合了乐观读锁、悲观读锁和写锁,提高并发读取性能 | 使用复杂,需要管理锁戳和锁模式的转换 |