多个线程同时访问共享资源时,如果没有正确的同步机制,可能会导致数据不一致或竞争条件(race condition)。为了避免这些问题,Java 提供了以下几种同步机制:
1.1. 使用 synchronized 关键字
synchronized 是 Java 中最基础的同步机制,主要用于防止多个线程同时访问共享资源。可以使用 synchronized 来修饰方法或者代码块。
修饰实例方法:表示当前对象的所有实例方法都使用同一个锁。
修饰静态方法:表示当前类的所有静态方法使用同一个锁。
修饰代码块:可以指定特定的资源作为锁,从而实现更细粒度的控制。
class Counter {
private int count = 0;
// 使用 synchronized 修饰方法,保证线程安全
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class SynchronizedExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建多个线程来测试线程安全
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终计数:" + counter.getCount()); // 输出 2000
}
}
解析:
synchronized:通过使用 synchronized 保证同一时间只有一个线程可以访问 increment() 和 getCount() 方法,从而避免了竞态条件。
1.2. 使用 ReentrantLock
ReentrantLock 是 java.util.concurrent.locks 包下的一个类,它比 synchronized 提供了更灵活的锁机制。ReentrantLock 允许线程在访问共享资源时显式地获取和释放锁,它支持可中断的锁请求、定时锁请求等特性。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
// 使用 ReentrantLock 显式获取和释放锁
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 确保释放锁
}
}
public int getCount() {
return count;
}
}
public class LockExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建多个线程来测试线程安全
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("最终计数:" + counter.getCount()); // 输出 2000
}
}
解析:
ReentrantLock
:与 synchronized
不同,ReentrantLock
允许线程在获取锁后能做更多的控制,如可以通过 lock.tryLock()
来尝试获取锁,或者设置超时等。2. 并发工具类
Java 提供了许多并发工具类,这些类帮助我们在复杂的并发环境下,协调线程的执行顺序,简化并发编程的难度。
2.1. CountDownLatch
CountDownLatch 是一种同步工具类,它允许一个或多个线程等待直到在其他线程中执行的一组操作完成。CountDownLatch 维护一个计数器,每当一个线程完成某个任务时,计数器就会减 1,直到计数器减到 0,等待的线程才能继续执行。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int taskCount = 3;
CountDownLatch latch = new CountDownLatch(taskCount);
// 创建并启动 3 个线程
for (int i = 0; i < taskCount; i++) {
final int taskId = i + 1;
new Thread(() -> {
System.out.println("任务 " + taskId + " 正在执行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown(); // 任务完成,计数器减 1
System.out.println("任务 " + taskId + " 完成");
}).start();
}
latch.await(); // 等待直到计数器为 0
System.out.println("所有任务完成,继续执行主线程");
}
}
解析:
CountDownLatch:在上述示例中,主线程等待所有子线程完成工作后再继续执行。latch.countDown() 将计数器减 1,latch.await() 会阻塞主线程直到计数器为 0。
2.2. CyclicBarrier
CyclicBarrier 类允许一组线程相互等待,直到所有线程都到达某个公共屏障点。CyclicBarrier 可以重复使用,因此适用于需要反复等待一组线程到达某个点的场景。
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) throws InterruptedException {
int parties = 3; // 线程数量
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
System.out.println("所有线程到达屏障,执行一些操作...");
});
// 启动多个线程
for (int i = 0; i < parties; i++) {
final int threadId = i + 1;
new Thread(() -> {
System.out.println("线程 " + threadId + " 到达屏障");
try {
barrier.await(); // 等待其他线程到达屏障
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程 " + threadId + " 继续执行");
}).start();
}
}
}
解析:
CyclicBarrier:每个线程在 barrier.await() 处阻塞,直到所有线程都到达屏障,屏障才会被解除,并执行可选的 Runnable 操作。
2.3. Semaphore
Semaphore 是一种计数信号量,它用来控制同时访问特定资源的线程数量。可以用来限制并发线程的数量,适用于资源共享的场景。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(2); // 限制最多 2 个线程同时访问
// 启动多个线程
for (int i = 0; i < 5; i++) {
final int threadId = i + 1;
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println("线程 " + threadId + " 正在执行...");
Thread.sleep(1000); // 模拟任务执行
System.out.println("线程 " + threadId + " 执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
}
}
解析:
Semaphore:在这个例子中,Semaphore 被初始化为 2,意味着最多允许 2 个线程同时访问受限资源。每个线程在执行任务前调用 acquire() 方法来获取许可,执行完任务后调用 release() 来释放许可。
当有多个线程尝试访问时,acquire() 会使线程阻塞,直到有空余的许可,只有当许可被释放时,其他线程才可以获得许可并继续执行。
除了 CountDownLatch、CyclicBarrier 和 Semaphore,Java 还提供了许多其他的并发工具类,能够帮助开发者解决不同的并发编程问题。
Exchanger:Exchanger 是一个简单的同步点,两个线程通过 exchange() 方法交换数据。它适用于需要两个线程互相交换数据的场景。
import java.util.concurrent.Exchanger;
public class ExchangerExample {
public static void main(String[] args) {
Exchanger exchanger = new Exchanger<>();
// 线程 1
new Thread(() -> {
try {
String message = "来自线程1的消息";
System.out.println("线程1: 交换消息 -> " + message);
message = exchanger.exchange(message); // 交换数据
System.out.println("线程1: 接收到消息 -> " + message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 线程 2
new Thread(() -> {
try {
String message = "来自线程2的消息";
System.out.println("线程2: 交换消息 -> " + message);
message = exchanger.exchange(message); // 交换数据
System.out.println("线程2: 接收到消息 -> " + message);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
解析:
Exchanger:这个工具类允许两个线程交换数据。在这个例子中,线程 1 和线程 2 通过 exchange() 方法交换字符串信息,exchange() 会阻塞线程,直到两个线程都准备好交换数据。
Phaser:Phaser 是一种更灵活的线程同步工具,类似于 CountDownLatch 和 CyclicBarrier,但是支持更多的功能,例如可以动态调整参与的线程数。
在今天的学习中,我们深入探讨了 Java 中的几种同步与并发工具类:
同步机制:通过 synchronized 关键字和 ReentrantLock 来保证线程安全。
并发工具类:
CountDownLatch 用于控制线程等待某些操作完成后再继续执行。
CyclicBarrier 用于协调多个线程在同一时刻继续执行,适合处理一组线程的协作。
Semaphore 用于限制并发线程数,适合处理有限资源的并发访问。
Exchanger 用于两个线程之间交换数据,适合需要线程间通信的场景。