Java中的同步与锁机制详解
作为Java程序员,我们都知道在编写多线程程序时,需要确保线程之间的同步与互斥。本文将详细介绍Java中的同步与锁机制。
1. 为什么需要同步与锁?
在多线程环境中,如果多个线程同时访问共享资源,可能会导致数据不一致或其他不可预料的结果。为了解决这个问题,Java提供了同步与锁机制来确保线程安全地访问共享资源。
2. Java中的同步
在Java中,同步可以通过以下两种方式实现:
- 同步方法:使用
synchronized
关键字修饰方法 - 同步块:使用
synchronized
关键字创建一个同步代码块
2.1 同步方法
当一个方法被synchronized
关键字修饰时,同一时间只有一个线程可以执行该方法。其他线程必须等待当前线程执行完毕后才能继续执行。示例如下:
public synchronized void synchronizedMethod() {
// 方法体
}
2.2 同步块
当需要同步的代码只是方法的一部分时,可以使用同步块。示例如下:
public void someMethod() {
// 非同步代码
synchronized (this) {
// 同步代码块
}
// 非同步代码
}
3. Java中的锁
Java提供了java.util.concurrent.locks
包,其中包含了多种锁机制。最常用的锁是ReentrantLock
,它实现了Lock
接口。使用ReentrantLock
可以实现更灵活的锁定策略,包括可重入锁、公平锁和非公平锁。
可重入锁、公平锁和非公平锁是 Java 并发编程中的几种锁类型
- 可重入锁(Reentrant Lock)
可重入锁是指在一个线程已经获得锁的情况下,该线程可以再次获得同一个锁。这种锁的优点是可以避免死锁和提高代码的可重用性。Java 中的 synchronized
和 ReentrantLock
都是可重入锁的实现。
class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
void methodA() {
lock.lock();
try {
// 执行方法 A 的代码
methodB();
} finally {
lock.unlock();
}
}
void methodB() {
lock.lock();
try {
// 执行方法 B 的代码
} finally {
lock.unlock();
}
}
}
在这个示例中,当线程执行 methodA()
并获得锁时,它可以在 methodA()
内部调用 methodB()
并再次获得同一个锁,因为 ReentrantLock
是可重入的
2.公平锁(Fair Lock)
公平锁是指在锁的获取顺序上遵循先进先出(FIFO)原则,先请求锁的线程会先获得锁。这种锁可以避免线程饥饿现象,但是相对于非公平锁,公平锁的性能开销较大。在 Java 中,ReentrantLock
可以通过构造函数参数设置为公平锁。
class FairLockExample {
private final ReentrantLock fairLock = new ReentrantLock(true);
void method() {
fairLock.lock();
try {
// 执行方法的代码
} finally {
fairLock.unlock();
}
}
}
在这个示例中,ReentrantLock
的构造函数接收一个布尔参数 true
,表示锁是公平的。这样,锁的获取顺序将遵循先进先出原则。
3.非公平锁(Unfair Lock)
非公平锁与公平锁相反,它不保证锁获取的顺序。当一个线程释放锁后,其他等待线程中可能有优先级更高的线程优先获得锁。非公平锁的优点是性能较好,因为它不需要维护等待线程的顺序。Java 中的 ReentrantLock
默认为非公平锁。
class UnfairLockExample {
private final ReentrantLock unfairLock = new ReentrantLock(false);
void method() {
unfairLock.lock();
try {
// 执行方法的代码
} finally {
unfairLock.unlock();
}
}
}
在这个示例中,ReentrantLock
的构造函数接收一个布尔参数 false
,表示锁是非公平的。这样,锁的获取顺序将不遵循先进先出原则,可能导致线程饥饿现象,但在大多数情况下性能较好。
简单来说就是
- 可重入锁:允许线程在已经获得锁的情况下再次获得同一个锁,可以避免死锁和提高代码的可重用性。
- 公平锁:遵循先进先出原则,先请求锁的线程会先获得锁,避免线程饥饿现象,但性能开销较大。
- 非公平锁:不保证锁获取的顺序,性能较好,但可能导致线程饥饿现象。
在实际应用中,选择锁类型取决于具体场景和性能需求。在大多数情况下,默认的非公平锁性能较好,可以满足需求;但如果需要确保线程不会饥饿,可以选择公平锁。
4. 死锁与如何避免
死锁是指多个线程相互等待对方释放资源的情况。这会导致程序无法继续执行。为了避免死锁,可以采取以下策略:
- 按顺序请求资源
- 设置超时释放锁
- 使用死锁检测算法
常见线程面试题
什么是线程饥饿?
线程饥饿是指一个线程因为竞争资源而长时间得不到执行的现象。在某些情况下,由于调度策略或者其他因素,一些线程可能长时间无法获得所需资源,导致这些线程一直处于等待状态,无法继续执行。这种现象称为线程饥饿。
以一个简单的银行排队场景为例。假设银行有两个窗口(资源),分别由两个线程(窗口1和窗口2)负责。顾客(任务)会在队列中等待,按照到达顺序依次进行服务。在正常情况下,顾客会按照到达顺序进行服务,不会出现线程饥饿现象。
但是,假设银行的窗口调度策略有问题,窗口1总是优先服务队列中的新到达顾客,窗口2则按照顺序服务。这样一来,窗口1可能会不断抢占新到达的顾客,导致队列前面的顾客一直得不到服务。这种情况下,队列前面的顾客就会出现饥饿现象,无法得到服务。
为了解决线程饥饿问题,可以采用公平的调度策略。在上述银行排队场景中,如果两个窗口都按照先进先出的原则服务顾客,那么线程饥饿现象将不会发生,每个顾客都能按照到达顺序得到服务。
在Java中,使用公平锁(如ReentrantLock
的公平锁模式)可以避免线程饥饿现象。公平锁会确保等待时间最长的线程最先获得锁,从而避免某些线程长时间得不到锁,导致线程饥饿。但是,公平锁的性能开销通常比非公平锁要大,因为需要维护一个队列来记录等待线程的顺序。
什么是Java中的同步?
答:Java中的同步是一种机制,用于确保多个线程在共享资源上按照预期的顺序访问,以防止竞争条件和数据不一致。
什么是锁?
答:锁是一种并发控制工具,用于确保同一时刻只有一个线程可以访问共享资源。Java提供了内置锁(通过synchronized
关键字)和显式锁(通过java.util.concurrent.locks
包中的类,例如ReentrantLock
)。
解释Java中的synchronized
关键字。
答:synchronized
是Java中的一个关键字,用于表示一个方法或代码块需要同步执行。当一个线程进入一个synchronized
方法或代码块时,它会获得与该对象或类关联的锁,而其他线程必须等待该锁被释放才能执行相同的方法或代码块。
什么是死锁?如何避免死锁?
答:死锁是一种并发问题,发生在两个或多个线程相互等待对方释放资源的情况下。避免死锁的方法包括:
- 避免循环等待:按照一定的顺序请求锁。
- 使用锁超时:尝试获取锁时设置超时时间,超时后线程可以释放已持有的锁并重试。
- 使用可中断锁:使用
java.util.concurrent.locks
包中的锁,如ReentrantLock
,它们可以响应中断信号,从而允许线程放弃等待并释放锁。
什么是可重入锁?
答:可重入锁(ReentrantLock)是一种锁,允许线程多次获取同一个锁而不会导致死锁。当一个线程已经持有锁时,它可以再次获取该锁而不会被阻塞。Java中的synchronized
关键字和ReentrantLock
类提供了可重入锁的实现。
解释java.util.concurrent.locks
包中的Lock
接口和ReentrantLock
类。
答:Lock
接口是Java并发包中提供的一个显式锁机制。它定义了用于获取和释放锁的方法。 ReentrantLock
是Lock
接口的一个实现,它提供了可重入的互斥锁功能。与synchronized
关键字相比, ReentrantLock
提供了更高的灵活性。
转载自:https://juejin.cn/post/7216570675704037432