synchronized
是用于实现线程同步的关键字。它提供了两种主要的方式来保证多个线程访问共享资源时的互斥性和可见性:同步块和同步方法。
同步块允许你指定一个对象
作为锁,并保护一段代码区域。这样,同一时刻只有一个线程可以执行这段被同步的代码。基本语法如下:
synchronized (lockObject) {
// lockObject 是一个对象引用,通常是某个实例变量或者 this 关键字,也可以是一个局部变量。
}
示例
设我们有两个线程需要访问一个共享的 StringBuilder 实例,并对其进行修改:
public class SyncExample {
private final StringBuilder sharedString = new StringBuilder();
public void append(String text) {
synchronized (sharedString) {
sharedString.append(text);
System.out.println(sharedString);
}
}
}
这个示例使用 sharedString 作为锁对象。当一个线程进入同步块时,它会获取 sharedString 的锁,其他线程必须等待这个线程释放锁才能进入同步块。
同步方法是指在方法声明前加上 synchronized 关键字的方法。当一个线程调用一个对象的同步方法时,它会自动获取该对象的锁。如果该对象的另一个同步方法已经被另一个线程调用,那么调用当前方法的线程将被阻塞,直到另一个线程释放锁。
示例
public class SyncExample {
private final StringBuilder sharedString = new StringBuilder();
public synchronized void append(String text) {
sharedString.append(text);
System.out.println(sharedString);
}
}
在这个例子中,append 方法是同步的。当一个线程调用 append 方法时,它会自动获取 SyncExample 实例的锁。这意味着如果一个线程正在执行 append 方法,其他线程将不能调用同一个实例上的任何其他同步方法,也不能调用同一个实例上的 append 方法。
Lock(锁)
Lock锁也称同步锁,用法特性如下:
它们之间的主要区别在于线程获取锁的顺序
非公平锁的优点在于吞吐量比公平锁要高,因为非公平锁更容易产生线程切换,从而增加系统的并发性。默认为非公平锁。
公平锁 (Fair Lock)
公平锁遵循先进先出 (FIFO) 的原则,即线程获取锁的顺序与它们请求锁的顺序相同。这意味着如果一个线程先于其他线程请求锁,那么它就有优先权获得锁。
非公平锁 (Non-Fair Lock)
非公平锁不保证线程获取锁的顺序,它允许一个后来请求锁的线程有可能比先请求锁的线程更快地获取到锁。这是因为非公平锁在尝试获取锁时可能会偏向于当前持有锁的线程
。
使用lock()
方法获取锁,使用unlock()
方法释放锁。务必
确保每次调用 lock() 之后都有对应的 unlock() 调用。
ReentrantLock 支持可重入性,这意味着一个已经持有锁的线程可以再次
获取锁,而不会导致死锁。每次获取锁都会增加锁的计数,每次释放锁都会减少计数。只有
当计数为零时,其他线程才能获取锁。
ReentrantLock lock = new ReentrantLock();
public void someMethod() {
lock.lock();
try {
System.out.println("First lock acquired.");
lock.lock();
try {
System.out.println("Second lock acquired.");
} finally {
lock.unlock(); // 释放第二个锁
}
System.out.println("Still inside the first lock.");
} finally {
lock.unlock(); // 释放第一个锁
}
}
例如
当一个方法需要在其内部递归调用自身时,可重入性可以确保线程在递归过程中仍然能够获取锁。
public class RecursiveMethod {
private final ReentrantLock lock = new ReentrantLock();
public void recursiveMethod(int n) {
lock.lock();
try {
if (n > 0) {
System.out.println("Recursive call " + n);
recursiveMethod(n - 1); // 递归调用
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
RecursiveMethod method = new RecursiveMethod();
method.recursiveMethod(5);
}
}
如果需要在等待锁的过程中设置超时,可以使用tryLock()
方法。tryLock() 可以带有或不带超时时间。用于尝试获取锁而不阻塞
。如果锁可用,则获取锁并返回 true;如果锁不可用,则不获取锁并返回 false。这使得你可以在不需要等待锁的情况下检查锁的状态,并根据锁是否可用采取不同的行动。
使用 tryLock() 的基本形式
public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
if (lock.tryLock()) {
try {
// 临界区代码
System.out.println("Inside the critical section.");
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire the lock.");
}
}
public static void main(String[] args) {
TryLockExample example = new TryLockExample();
example.performTask();
}
}
使用 tryLock() 的超时形式
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockWithTimeoutExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
if (lock.tryLock(10, TimeUnit.MILLISECONDS)) {
try {
// 临界区代码
System.out.println("Inside the critical section.");
} finally {
lock.unlock();
}
} else {
System.out.println("Could not acquire the lock within the timeout.");
}
}
public static void main(String[] args) {
TryLockWithTimeoutExample example = new TryLockWithTimeoutExample();
example.performTask();
}
}
tryLock(10, TimeUnit.MILLISECONDS)
表示尝试获取锁,等待时间最长为 10 毫秒。如果在这段时间内锁可用,则获取锁并执行临界区代码;否则返回 false,并在控制台上输出相应的消息。
ReentrantLock 提供了lockInterruptibly()
方法,它在获取锁时允许线程在等待锁的过程中响应中断信号。如果锁不可用,线程将阻塞并等待锁变得可用。如果在此期间线程被中断,lockInterruptibly() 方法将抛出 InterruptedException。
interrupt()
用于向线程发送中断信号。
Thread.isInterrupted()
来检查线程是否被中断。
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptiblyExample {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
try {
lock.lockInterruptibly();
try {
// 临界区代码
System.out.println("Inside the critical section.");
Thread.sleep(1000);
System.out.println("Leaving the critical section.");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 捕获中断异常
System.out.println("Thread was interrupted while waiting for the lock.");
}
}
public static void main(String[] args) throws InterruptedException {
LockInterruptiblyExample example = new LockInterruptiblyExample();
Thread thread = new Thread(example::performTask);
thread.start();
// 等待一段时间后中断线程
Thread.sleep(500);
thread.interrupt();
// 等待线程完成
thread.join();
}
}
输出:
Inside the critical section.
Thread was interrupted while waiting for the lock.
synchronized与Lock的对比
synchronized
特点
- 语法简洁:使用关键字 synchronized 来声明同步块或同步方法。
- 自动释放锁:当线程离开同步块或方法时,锁会自动释放。
- 死锁检测:JVM 自动处理锁的获取和释放,可以防止锁没有被释放的情况。
- 支持重入
- 不支持超时:无法指定获取锁的超时时间。
- 不支持尝试锁:无法尝试获取锁而不阻塞。
Lock
特点
- 显式锁:必须显式地获取和释放锁。
- 可中断的锁:支持通过 lockInterruptibly() 方法获取锁,允许响应中断。
- 超时锁:支持通过 tryLock() 方法尝试获取锁,并可以选择性地指定超时时间。
- 公平性和非公平性:ReentrantLock 支持公平锁和非公平锁。
- 锁的粒度控制:可以更细粒度地控制锁的获取和释放。
- 死锁检测:如果不正确地使用,可能导致死锁。
- 支持重入
对比总结
使用场景:
- synchronized 更适合简单的同步需求,代码更简洁。
- Lock 适用于更复杂的同步需求,提供了更多的控制选项。
性能:
- 在 JDK 1.6 之后,synchronized 的性能得到了显著提升,对于大部分场景来说,其性能与 ReentrantLock 相当。
- 对于更高级的锁控制需求,如尝试锁或可中断锁,ReentrantLock 提供了更好的解决方案。
灵活性:
Lock 提供了更高级的功能,如尝试获取锁、超时获取锁等,因此在灵活性方面优于 synchronized。
死锁检测:
- synchronized 由 JVM 自动管理,因此更安全。
- Lock 需要程序员手动管理,容易出错,可能导致死锁。