目录
1. 前言
在《[Java进阶] 并发编程实战大全(上篇)》中主要讲述了线程、线程池和协程的实战,列举了它们的多种使用方式和代码案例。本章我们继续讲解Java并发编程实战的主要知识点,主要会对同步这块做深入的讲解,会列举Java中常用的一些同步工具,同时给出代码实战的案例。
2. 概述
Java同步是多线程编程中的核心概念,它旨在确保多个线程在并发访问共享资源时能够保持数据的一致性和完整性。以下列举一些Java中常用的一些同步的方法。
3. synchronized关键字
synchronized
关键字是Java中实现线程同步的一种重要手段。它可以通过不同的方式应用,主要分为同步方法和同步代码块两种形式。下面详细介绍这两种用法,并给出相应的使用案例。
3.1 同步方法
3.1.1 同步实例方法
当一个方法被声明为 synchronized
时,该方法在同一时刻只能被一个线程访问。锁是当前实例对象。
示例代码:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
使用案例:
假设有一个 Counter
对象,多个线程同时调用 increment
和 decrement
方法。由于这些方法是同步的,因此在任何时候只有一个线程可以执行这些方法,从而保证了线程安全。
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.decrement();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount()); // 应输出 0
}
}
3.1.2 同步静态方法
当一个静态方法被声明为 synchronized
时,该方法在同一时刻也只能被一个线程访问。锁是类的 Class
对象。
示例代码:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
使用案例:
单例模式中的懒汉式实现,确保在多线程环境下只有一个实例被创建。
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
Singleton s1 = Singleton.getInstance();
System.out.println(s1);
});
Thread t2 = new Thread(() -> {
Singleton s2 = Singleton.getInstance();
System.out.println(s2);
});
t1.start();
t2.start();
}
}
3.2 同步代码块
3.2.1 同步实例对象
使用 synchronized
关键字包裹需要同步的代码块,锁对象可以是任何对象,通常是当前实例对象。
示例代码:
public class BankAccount {
private double balance;
public BankAccount(double balance) {
this.balance = balance;
}
public void deposit(double amount) {
synchronized (this) {
double newBalance = balance + amount;
balance = newBalance;
}
}
public void withdraw(double amount) {
synchronized (this) {
if (amount <= balance) {
double newBalance = balance - amount;
balance = newBalance;
} else {
System.out.println("Insufficient funds");
}
}
}
public double getBalance() {
return balance;
}
}
使用案例:
假设有一个 BankAccount
对象,多个线程同时调用 deposit
和 withdraw
方法。由于这些方法内部的代码块是同步的,因此在任何时候只有一个线程可以执行这些代码块,从而保证了线程安全。
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
account.deposit(10);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
account.withdraw(10);
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final balance: " + account.getBalance()); // 应输出 1000
}
}
3.2.2 同步类对象
如果需要同步静态方法中的代码块,可以使用类的 Class
对象作为锁。
示例代码:
public class Logger {
private static final Logger INSTANCE = new Logger();
private Logger() {}
public static Logger getInstance() {
return INSTANCE;
}
public void log(String message) {
synchronized (Logger.class) {
System.out.println(message);
}
}
}
使用案例:
假设有一个 Logger
类,多个线程同时调用 log
方法。由于 log
方法内部的代码块是同步的,并且锁是 Logger.class
,因此在任何时候只有一个线程可以执行这个代码块,从而保证了线程安全。
public class Main {
public static void main(String[] args) {
Logger logger = Logger.getInstance();
Thread t1 = new Thread(() -> {
logger.log("Thread 1 logging");
});
Thread t2 = new Thread(() -> {
logger.log("Thread 2 logging");
});
t1.start();
t2.start();
}
}
4. 显式锁(Explicit Locks)
Java 提供了多种显式锁(Explicit Locks),这些锁相比内置锁(如 synchronized
)提供了更多的功能和灵活性。以下是一些主要的显式锁及其实战案例:
4.1 ReentrantLock
ReentrantLock
是一个可重入的互斥锁,提供了与 synchronized
类似的功能,但更加灵活。
特点:
- 可重入性:允许同一个线程多次获取同一个锁。
- 公平性和非公平性:可以选择是否按照请求锁的顺序来获取锁。
- 锁的中断:可以在等待锁的过程中响应中断。
- 尝试加锁:可以尝试获取锁而不阻塞。
实战案例:
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
count--;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.decrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount()); // 应输出 0
}
}
4.2 ReentrantReadWriteLock
ReentrantReadWriteLock
将锁分为读锁和写锁,允许多个读线程同时访问,但写线程独占锁。
特点:
- 读写分离:读锁允许多个读线程同时访问,写锁独占。
- 可重入性:读锁和写锁都是可重入的。
- 公平性和非公平性:可以选择是否按照请求锁的顺序来获取锁。
实战案例:
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.HashMap;
import java.util.Map;
public class Cache {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private final Map<String, String> cache = new HashMap<>();
public String get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
Cache cache = new Cache();
Thread t1 = new Thread(() -> {
cache.put("key1", "value1");
System.out.println(cache.get("key1"));
});
Thread t2 = new Thread(() -> {
cache.put("key2", "value2");
System.out.println(cache.get("key2"));
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Cache content: " + cache.cache);
}
}
4.3 StampedLock
StampedLock
是 Java 8 引入的一种乐观锁,适用于读多写少的场景。它提供了读锁、写锁和乐观读锁三种模式。
特点:
- 乐观读锁:在读取数据时假设不会发生冲突,如果检测到冲突则重新读取。
- 读锁:允许多个读线程同时访问。
- 写锁:独占锁,不允许其他读写操作。
实战案例:
import java.util.concurrent.locks.StampedLock;
import java.util.concurrent.locks.LockSupport;
public class StampedLockExample {
private double x, y;
private final StampedLock stampedLock = new StampedLock();
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead();
double currentX = x, currentY = y;
if (!stampedLock.validate(stamp)) {
stamp = stampedLock.readLock();
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
public static void main(String[] args) throws InterruptedException {
StampedLockExample example = new StampedLockExample();
Thread t1 = new Thread(() -> {
example.move(1, 2);
System.out.println("Distance from origin: " + example.distanceFromOrigin());
});
Thread t2 = new Thread(() -> {
example.move(3, 4);
System.out.println("Distance from origin: " + example.distanceFromOrigin());
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final distance from origin: " + example.distanceFromOrigin());
}
}
4.4 总结
ReentrantLock
:适用于需要更多控制和灵活性的场景,如锁的中断、尝试加锁等。ReentrantReadWriteLock
:适用于读多写少的场景,允许多个读线程同时访问,提高并发性能。StampedLock
:适用于读多写少的场景,提供了乐观读锁,进一步提高了读操作的性能。
这些显式锁提供了丰富的功能,可以根据具体的业务需求选择合适的锁类型,以提高程序的并发性能和线程安全性。
5. 高级同步工具
5.1 CountDownLatch
CountDownLatch是Java多线程并发中的一种同步器,是JDK内置的同步工具类。它允许一个或多个线程等待其他线程完成一系列操作。通过它可以定义一个倒计数器,当倒计数器的值大于0时,所有调用await方法的线程都会等待;而调用countDown方法则可以让倒计数器的值减一,当倒计数器值为0时,所有等待的线程都将被唤醒并继续执行。
5.1.1 工作原理
CountDownLatch的工作原理基于AQS(AbstractQueuedSynchronizer)同步器。其主要包含以下几个要素:
- 倒计数器的初始值:在构建CountDownLatch对象时指定,表示需要等待的条件个数。
- await方法:让线程进入等待状态,等待的条件是倒计数器的值大于0。当倒计数器的值变为0时,等待的线程被唤醒并继续执行。
- countDown方法:用于将倒计数器的值减一,表示有一个线程完成了一项操作。
5.1.2 应用场景
- 多线程任务协同:一个主线程等待其他多个线程完成任务,每个子线程完成一部分工作后调用countDown()方法减少计数值,主线程通过await()方法等待计数值变为零。
- 并行初始化:在系统启动时,有时需要等待多个服务初始化完成后再继续执行。每个服务初始化完成后调用countDown()。
- 分布式系统中的协同:在分布式系统中,CountDownLatch可以用于等待多个节点的某个事件的发生,以协同分布式系统的操作。
5.1.3 实战案例
以下是一个简单的CountDownLatch实战案例,模拟了一个拼团活动:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) {
pt();
}
private static void pt() {
// 模拟拼团活动,假设需要10人拼团成功
final CountDownLatch countDownLatch = new CountDownLatch(10);
List<String> ids = new ArrayList<>();
// 模拟有30个用户请求拼团
for (int i = 0; i < 30; i++) {
new Thread(() -> {
if (countDownLatch.getCount() > 0) {
synchronized (ids) {
if (countDownLatch.getCount() > 0) {
ids.add(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + "拼团成功!!!");
countDownLatch.countDown();
}
}
}
System.out.println(Thread.currentThread().getName() + "请求拼团!!!商品剩余" + countDownLatch.getCount());
}, String.valueOf(i + 1)).start();
}
// 主线程等待拼团成功
new Thread(() -> {
try {
countDownLatch.await();
System.out.println("拼团结束============================================================");
System.out.println("拼团人员id: " + ids);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "拼团主线程").start();
}
}
在这个案例中,我们创建了一个CountDownLatch对象,并设置其初始值为10,表示需要10个用户拼团成功。然后,我们启动了30个线程来模拟用户的拼团请求。每个线程在完成拼团操作后,会调用countDown()方法将倒计数器的值减一。主线程通过await()方法等待,直到倒计数器的值变为0,即10个用户拼团成功后,主线程才会继续执行并输出拼团结束的信息和拼团人员的id列表。
这个案例展示了CountDownLatch在多线程任务协同方面的应用,也体现了其一次性的特性:一旦倒计数器的值变为0,所有等待的线程都会被唤醒并继续执行,而CountDownLatch本身则无法再回到初始状态或被重新使用。
5.2 CyclicBarrier
CyclicBarrier是Java并发包(java.util.concurrent)中的一个同步工具类,它允许一组线程互相等待,直到所有线程都到达某个公共屏障点(barrier),然后这些线程才能继续执行后续任务。CyclicBarrier的“Cyclic”意味着它可以被循环使用,一旦所有等待线程都到达同步点,屏障就会被重置,以便下一轮使用。
5.2.1 工作原理
- 一个计数器&一个锁:CyclicBarrier的工作原理基于内部维护的一个计数器和一个锁(通常是ReentrantLock)。
- await方法:当线程调用await()方法时,计数器会减一,如果计数器变为0,表示所有线程都已到达屏障点,此时会执行一个可选的屏障动作(如果指定了的话),然后唤醒所有等待的线程,让它们继续执行。如果计数器不为0,则线程会被阻塞在await()方法上,直到所有线程都到达屏障点。
5.2.2 应用场景
- 多线程任务分解成多个阶段并行执行:每个阶段的任务需要等待其他线程完成。
- 控制多个线程在某个屏障点同步执行:例如,计算任务的结果合并。
5.2.3 实战案例
以下是一个使用CyclicBarrier的实战案例,模拟了一个多线程任务分解成多个阶段并行执行的场景:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private static final int THREAD_COUNT = 3; // 线程数量
private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
// 所有线程到达屏障点时执行的动作
System.out.println("所有线程都已到达屏障点,执行屏障动作!");
});
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 正在等待屏障点...");
barrier.await(); // 等待其他线程到达屏障点
System.out.println(Thread.currentThread().getName() + " 已通过屏障点!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
在这个案例中,我们创建了一个CyclicBarrier对象,并指定需要等待的线程数量为3。然后,我们启动了3个线程,每个线程在到达屏障点时都会调用await()方法等待其他线程。当所有线程都到达屏障点时,会执行我们指定的屏障动作(打印一条消息),然后所有线程都会继续执行后续任务(打印已通过屏障点的消息)。
这个案例展示了CyclicBarrier在多线程任务分解成多个阶段并行执行方面的应用,以及如何在所有线程到达屏障点时执行一个公共动作。通过CyclicBarrier,我们可以方便地实现多个线程之间的同步和协作。
5.3 Semaphore
Semaphore(信号量)是Java中一种重要的并发编程工具,它位于java.util.concurrent
包中。Semaphore通过控制许可数量,实现了对并发线程数的精细管理,有效避免了资源竞争和过载问题,能显著提升系统吞吐量和响应速度。以下是对Java中Semaphore的详细介绍及实战案例:
5.3.1 工作原理
- 指定许可数:构造Semaphore(信号量)时指定许可数,线程在访问资源之前必须从Semaphore中获取许可,访问完成后释放许可。
acquire
:获取一个许可,如果没有可用许可,则当前线程会被阻塞,直到有许可可用。也可以用于一次性获取多个许可。release
:释放一个许可,增加可用许可数。也可以释放多个许可。- 底层实现:Semaphore的底层实现主要依赖于AbstractQueuedSynchronizer(AQS)。AQS是一个用于构建锁和同步器的框架,提供了FIFO队列来管理线程的等待状态。Semaphore通过继承AQS并重写其方法来实现许可管理。
5.3.2 应用场景
- 资源池:控制对有限资源的访问,例如数据库连接池、线程池等。
- 流量控制:限制系统中同时执行的任务数量,避免资源过载。
5.3.3 实战案例
数据库连接限制器
在高并发环境中,数据库连接是宝贵的资源,过多的连接可能导致数据库压力过大,影响性能。使用Semaphore可以限制同时访问数据库的连接数。以下是一个使用Semaphore限制数据库连接数的示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class DatabaseConnectionLimiter {
private static final int MAX_CONNECTIONS = 5;
private static final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
semaphore.acquire();
accessDatabase();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executor.shutdown();
}
private static void accessDatabase() {
System.out.println(Thread.currentThread().getName() + " accessing database...");
try {
Thread.sleep(2000); // 模拟数据库操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " done accessing database.");
}
}
在这个例子中,创建了一个Semaphore实例来限制同时访问数据库的连接数为5。然后创建了一个固定大小的线程池来模拟多个数据库连接请求。每个线程在访问数据库之前都会尝试获取Semaphore的许可,如果获取成功则继续执行数据库操作,否则将等待直到有可用的许可。操作完成后释放许可以供其他线程使用。
6. 总结
本文主要对Java中常用的一些同步工具做了详细的讲解,包括synchronized关键字(隐式锁)、显示锁(Locks)、高级的同步工具(如CountDownLatch、CyclicBarrier、Semaphore),并通过代码案例来讲解了各种同步工具的使用以及工作原理。