同步锁:同步实现的几种方式
Java中同步实现的主要几种方式,以及不同的同步锁
在 Java 中,同步是多线程编程中非常重要的概念,它确保了多个线程可以安全地访问共享资源,避免数据不一致和竞态条件。Java 提供了多种同步实现的方式,并且每种方式背后都有不同的同步锁。下面将详细介绍 Java 中同步实现的几种方式,以及 不同的同步锁。
一、Java 中同步实现的主要几种方式
- 使用 synchronized 关键字(方法同步和代码块同步)
- 使用 ReentrantLock(显式锁)
- 使用 ReadWriteLock(读写锁)
- 使用 volatile 关键字
- 使用 Semaphore 和 CountDownLatch(并发工具类)
二、synchronized
关键字
synchronized
是 Java 中实现同步的最基本方式。它可以用于方法或者代码块的同步,确保同一时刻只有一个线程可以访问同步代码块或方法。
1. 方法同步
通过 synchronized
修饰实例方法或者静态方法,确保同一时刻只有一个线程可以执行该方法。
- 实例方法同步:锁是对象本身。
- 静态方法同步:锁是类的
Class
对象。
示例代码:
class Counter {
private int count = 0;
// 使用 synchronized 修饰实例方法
public synchronized void increment() {
count++;
}
// 使用 synchronized 修饰静态方法
public static synchronized void staticIncrement() {
// 对类的静态资源进行同步
}
}
2. 代码块同步
synchronized
还可以修饰代码块,在方法内部控制代码的同步。在这种方式下,我们可以指定一个对象作为锁,从而只锁住特定的代码块。
示例代码:
class Counter {
private int count = 0;
public void increment() {
synchronized(this) { // 锁定当前对象
count++;
}
}
}
说明:
- 优点:简单直观,Java 内置支持。
- 缺点:不灵活,无法指定多个锁对象;会导致性能瓶颈(锁的粒度较大)。
三、ReentrantLock
(显式锁)
ReentrantLock
是 Java java.util.concurrent.locks
包中的一个显式锁,它比 synchronized
更加灵活。ReentrantLock
提供了比 synchronized
更加丰富的功能,例如 可重入锁、可中断锁、定时锁 等。
示例代码:
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁
}
}
}
说明:
- 优点:
- 可以精细地控制锁的获取和释放。
- 提供了
tryLock()
、lockInterruptibly()
等方法,支持锁的中断操作。 - 可以通过
ReentrantLock
实现公平锁。
- 缺点:
- 需要手动释放锁,否则可能会导致死锁。
四、ReadWriteLock
(读写锁)
ReadWriteLock
是 java.util.concurrent.locks
包中的一个接口,它为多线程访问资源提供了更精细的控制。它有两个主要的锁:读锁 和 写锁。
- 读锁:多个线程可以同时持有读锁,允许并发读取。
- 写锁:一个线程持有写锁时,其他线程不能读取也不能写入。
示例代码:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Counter {
private int count = 0;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作:使用读锁
public int getCount() {
rwLock.readLock().lock(); // 获取读锁
try {
return count;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}
// 写操作:使用写锁
public void increment() {
rwLock.writeLock().lock(); // 获取写锁
try {
count++;
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
}
说明:
- 优点:
- 适用于读操作远多于写操作的场景,能够提高并发性能。
- 写锁互斥,保证数据一致性;读锁并发,提升读性能。
- 缺点:
- 实现复杂,读写锁的竞争较激烈时可能导致性能下降。
五、volatile
关键字
volatile
关键字是 Java 中的一个轻量级同步机制,确保变量的值在多个线程之间保持一致。对于标记为 volatile
的变量,每次读取都会从主内存中读取,而不是从线程的本地缓存中读取。
示例代码:
class Counter {
private volatile int count = 0;
public void increment() {
count++; // 因为 count 是 volatile,这里可以保证最新的值被读取和写入
}
}
说明:
- 优点:
- 比
synchronized
更轻量级,不涉及锁的管理,性能较好。
- 比
- 缺点:
- 只能确保单个变量的可见性,并不能保证复合操作(如
count++
)的原子性。
- 只能确保单个变量的可见性,并不能保证复合操作(如
六、并发工具类:Semaphore
和 CountDownLatch
除了显式锁之外,Java 还提供了许多并发工具类来帮助实现同步和线程间的协调。
1. Semaphore
Semaphore
是一个计数信号量,它用于限制某些资源的并发访问数量。
示例代码:
import java.util.concurrent.Semaphore;
class Counter {
private final Semaphore semaphore = new Semaphore(3); // 限制最大并发数为3
public void accessResource() throws InterruptedException {
semaphore.acquire(); // 获取信号量
try {
// 访问共享资源的代码
System.out.println(Thread.currentThread().getName() + " accessing resource");
} finally {
semaphore.release(); // 释放信号量
}
}
}
2. CountDownLatch
CountDownLatch
是一个同步辅助工具,它允许一个或多个线程等待其他线程完成某些操作后再继续执行。通常用于多个线程并行工作,最后一起等待结束。
示例代码:
import java.util.concurrent.CountDownLatch;
class Counter {
private final CountDownLatch latch = new CountDownLatch(1);
public void waitForFinish() throws InterruptedException {
latch.await(); // 等待
System.out.println("Task is finished.");
}
public void finishTask() {
latch.countDown(); // 完成任务
System.out.println("Task finished.");
}
}
七、不同同步锁的对比
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
synchronized | 简单易用,Java 内置支持 | 性能差,锁粒度大,不能灵活控制 | 简单的线程同步,确保线程互斥 |
ReentrantLock | 更灵活,支持可中断、定时锁、尝试锁等高级功能 | 需要手动释放锁,容易引发死锁 | 需要灵活控制的复杂并发场景 |
ReadWriteLock | 读操作多时性能较高,适合读写分离的场景 | 写操作竞争时性能差 | 读多写少的场景,数据库缓存等 |
volatile | 轻量级,性能好,适用于单一变量的共享 | 只保证可见性,不保证原子性 | 单一变量的共享,不能用于复杂操作 |
Semaphore | 限制并发数,控制线程访问资源的数量 | 不能保证线程的执行顺序 | 控制访问数量,限制资源访问并发数 |
CountDownLatch | 线程协调,等待其他线程完成任务后再继续执行 | 只能使用一次,不能重用 | 需要线程等待并发任务完成的场景 |
八、总结
Java 提供了多种同步实现方式,不同的同步机制适用于不同的应用场景。选择合适的同步方式可以提高多线程程序的并发性和性能。