一、乐观锁(Optimistic Locking)
- 原理:乐观锁假设在大多数情况下,多个线程之间不会发生冲突。在读取数据时,每个线程会获得一个标识符(如版本号或时间戳)。在提交修改之前,会比较当前标识符与之前读取的标识符是否相等,如果相等则提交成功,否则说明数据已被其他线程修改,需要进行冲突处理。
- 实现方式:通常使用版本号或时间戳来实现,可以在数据库中添加一个额外的字段作为标识符,并在更新操作时进行比较。
- 应用场景:适用于读操作频繁而写操作较少的场景,可以减少锁的使用,提高并发性能。
代码示例
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private static AtomicInteger version = new AtomicInteger(0);
private static int sharedData = 0;
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
int currentVersion = version.get(); // 读取当前版本号
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查版本号是否仍为之前读取的版本号
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} else {
System.out.println("Thread 1: Failed to update shared data due to concurrent modification");
}
});
Thread thread2 = new Thread(() -> {
int currentVersion = version.get(); // 读取当前版本号
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查版本号是否仍为之前读取的版本号
if (version.compareAndSet(currentVersion, currentVersion + 1)) {
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} else {
System.out.println("Thread 2: Failed to update shared data due to concurrent modification");
}
});
thread1.start();
thread2.start();
}
}
这个例子中,我们使用了 AtomicInteger 类的 compareAndSet() 方法来实现乐观锁。首先,我们定义了一个版本号 version
用于追踪共享数据的变化。然后,我们创建了两个线程,每个线程都读取当前版本号并对共享数据执行修改操作。在提交修改之前,线程会再次检查当前版本号是否仍为之前读取的版本号,如果是,则提交修改成功;否则,说明数据已被其他线程修改,需要进行相应处理。
二、悲观锁(Pessimistic Locking)
- 原理:悲观锁假设在多线程环境下,对共享资源的访问会产生冲突,因此默认认为每次访问都会发生冲突,需要加锁保证独占访问。
- 实现方式:可以使用synchronized关键字或Lock接口的具体实现(如ReentrantLock)来实现。
- 应用场景:适用于写操作频繁的场景,因为它能够确保数据一致性和线程安全。
代码示例
import java.util.concurrent.locks.ReentrantLock;
public class PessimisticLockExample {
private static int sharedData = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 释放锁
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 释放锁
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,我们使用了 ReentrantLock
类来实现悲观锁。首先,我们创建了一个名为 lock
的 ReentrantLock
对象来保护共享数据。然后,我们创建了两个线程,在涉及到共享数据的代码块中分别调用 lock()
方法获取锁,并在修改共享数据后调用 unlock()
方法释放锁。
在这个示例中,使用悲观锁的方式是通过显式地获取和释放锁来实现的。当一个线程获取到锁时,其他线程会被阻塞,直到锁被释放。这样可以确保同一时间只有一个线程能够访问共享资源,保证了数据的一致性和线程安全性。
三、可重入锁(Reentrant Lock)
- 原理:可重入锁是一种特殊类型的锁,允许同一个线程多次获得锁,也称为可重入性。当一个线程已经持有锁时,再次请求获取该锁是允许的,而不会导致线程被阻塞,这种机制可以避免死锁。
- 实现方式:在Java中,ReentrantLock类和synchronized关键字都是可重入锁的实现。
- 应用场景:适用于某个线程需要递归地调用同步方法或代码块的场景,提高代码的灵活性。
代码示例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static int sharedData = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
updateSharedData(); // 调用可重入方法
} finally {
lock.unlock(); // 释放锁
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
updateSharedData(); // 调用可重入方法
} finally {
lock.unlock(); // 释放锁
}
});
thread1.start();
thread2.start();
}
private static void updateSharedData() {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Shared data updated inside the reentrant method to " + sharedData);
} finally {
lock.unlock(); // 释放锁
}
}
}
在这个示例中,我们使用了 ReentrantLock
类来实现可重入锁。首先,我们创建了一个名为 lock
的 ReentrantLock
对象来保护共享数据。然后,我们创建了两个线程,在涉及到共享数据的代码块中分别调用 lock()
方法获取锁,并在修改共享数据后调用 unlock()
方法释放锁。
值得注意的是,可重入锁允许同一个线程多次获取锁。在示例中,当线程1获取到锁后,在修改共享数据期间又调用了 updateSharedData()
方法,该方法中也需要获取锁。由于可重入锁的特性,线程1可以再次获取锁,而不会造成死锁。
可重入锁在Java中有多种实现方式,其中最常见的是 ReentrantLock
类。可重入锁提供了一种灵活且强大的机制,用于管理并保护共享资源的访问。
四、公平锁(Fair Lock)
- 原理:公平锁是一种保证线程获取锁的顺序与其申请锁的顺序相同的锁机制。它会按照线程的申请顺序来分配锁资源,避免某个线程饥饿地等待锁。
- 实现方式:可以使用ReentrantLock类的构造函数指定为公平锁。
- 应用场景:当多个线程竞争同一个资源时,希望公平地分配锁资源,避免某个线程长时间无法获取到锁的场景。
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private static int sharedData = 0;
private static ReentrantLock lock = new ReentrantLock(true); // 创建公平锁
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 释放锁
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 释放锁
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,我们使用了 ReentrantLock
类来实现公平锁。通过在创建 ReentrantLock
对象时传递参数 true
,我们创建了一个公平锁,即等待时间最长的线程会最先获取到锁。
在公平锁中,当多个线程竞争同一个锁时,锁会按照线程等待的顺序分配给它们。这可以确保较早等待的线程优先获得锁,避免了饥饿情况的发生,即某些线程一直无法获得锁。
需要注意的是,公平锁可能会牺牲一定的性能,因为它需要维护一个队列来管理等待的线程。因此,当性能要求较高且没有特殊需求时,可以使用非公平锁。
在实际开发中,公平锁的选择应根据具体的业务需求和性能要求综合考虑。
五、互斥锁(Mutex)
- 原理:互斥锁是一种用于保护共享资源不被并发访问的锁机制。它通过对代码块或方法进行加锁,确保同一时刻只有一个线程能够执行被保护的代码。
- 实现方式:在Java中,可以使用synchronized关键字或ReentrantLock类来实现互斥锁。
- 应用场景:适用于需要保护临界区的代码,确保数据一致性和线程安全的场景。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MutexLockExample {
private static int sharedData = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 释放锁
}
});
Thread thread2 = new Thread(() -> {
lock.lock(); // 获取锁
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.unlock(); // 释放锁
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,我们使用了 ReentrantLock
类来实现互斥锁。通过创建一个 ReentrantLock
对象,我们获得了一个可重入锁。这意味着同一个线程可以多次获取同一个锁而不会发生死锁。
在示例中,当一个线程获取到锁后,其他线程将被阻塞直到锁被释放。这样确保了同时只有一个线程能够修改共享数据,从而避免了数据竞争和并发问题。
需要注意的是,在使用互斥锁时,务必在合适的地方调用 unlock()
方法来释放锁,以避免死锁和资源泄漏。
互斥锁是一种常见且有效的保护共享资源的机制,在并发编程中被广泛使用。使用互斥锁可以确保共享数据的一致性和线程安全。
六、自旋锁(Spin Lock)
- 原理:自旋锁是一种忙等待的锁机制,在线程尝试获得锁时不会立即阻塞,而是循环检测锁的状态,直到成功获取锁或达到最大自旋次数。
- 实现方式:在Java中,可以使用AtomicInteger类的compareAndSet方法来实现简单的自旋锁。
- 应用场景:适用于锁保持时间非常短暂的情况,避免线程频繁地阻塞和唤醒。
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLockExample {
private static int sharedData = 0;
private static AtomicBoolean lock = new AtomicBoolean(false);
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
while (!lock.compareAndSet(false, true)) {
// 自旋等待锁释放
}
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 1: Shared data updated to " + sharedData);
} finally {
lock.set(false); // 释放锁
}
});
Thread thread2 = new Thread(() -> {
while (!lock.compareAndSet(false, true)) {
// 自旋等待锁释放
}
try {
int newValue = sharedData + 1; // 对共享数据进行修改
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sharedData = newValue; // 提交修改
System.out.println("Thread 2: Shared data updated to " + sharedData);
} finally {
lock.set(false); // 释放锁
}
});
thread1.start();
thread2.start();
}
}
在这个示例中,我们使用了 AtomicBoolean
类来实现自旋锁。自旋锁是一种在获取锁时反复检查锁状态的锁机制。
在示例中,每个线程使用 compareAndSet
方法来尝试获取锁。如果锁当前的状态是未锁定(false
),则将其设置为锁定(true
),从而成功获取到锁。如果锁已经被其他线程获取,则会一直进行自旋等待,直到锁被释放。
需要注意的是,在使用自旋锁时,要避免死锁和活锁的情况发生。因此,合理选择自旋次数和自旋等待时间非常重要。过长的自旋时间可能导致性能下降,而过短的自旋时间可能导致过多的线程切换开销。
自旋锁适用于对共享数据的访问时间较短,且竞争不是很激烈的场景。在高并发情况下,自旋锁可能会导致CPU资源的浪费,因此需要根据具体业务场景和性能要求综合考虑是否使用自旋锁。
七、闭锁(Latch)
- 原理:闭锁是一种用于等待其他线程完成操作的同步工具。它允许一个或多个线程等待其他线程执行完特定任务后再继续执行。
- 实现方式:在Java中,CountDownLatch和CyclicBarrier是常见的闭锁实现。
- 应用场景:适用于需要等待其他线程完成某个任务后再继续执行的场景。
import java.util.concurrent.CountDownLatch;
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3; // 工作线程数目
CountDownLatch latch = new CountDownLatch(workerCount);
// 创建工作线程并启动
for (int i = 0; i < workerCount; i++) {
Thread thread = new Thread(() -> {
// 模拟工作
System.out.println("Worker thread start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker thread finish");
latch.countDown(); // 工作完成,计数减一
});
thread.start();
}
System.out.println("Main thread waiting for workers to finish");
latch.await(); // 主线程等待所有工作线程完成
System.out.println("All workers have finished");
// 继续主线程的后续操作
}
}
在这个示例中,我们使用了 CountDownLatch
类来实现闭锁。闭锁是一种同步工具,它可以使一个或多个线程等待其他线程完成某些操作后再继续执行。
在示例中,主线程首先创建了一个 CountDownLatch
对象,并指定需要等待的工作线程数目为 workerCount
。然后,主线程创建了多个工作线程,并在每个工作线程开始和结束时调用 countDown()
方法,表示工作完成。
主线程在调用 await()
方法后会被阻塞,直到计数器减至零,即所有工作线程都完成了工作。然后,主线程可以继续执行接下来的操作。
闭锁适用于一组线程需要等待某个条件满足后再同时继续执行的场景。通过闭锁,可以更好地控制线程的并发执行。
八、信号量(Semaphore)
- 原理:信号量是一种用于控制同时访问某个资源的线程数的同步工具。它可以指定能同时访问资源的线程个数,并提供了获取和释放许可的机制。
- 实现方式:在Java中,Semaphore类是信号量的实现。
- 应用场景:适用于需要限制并发访问某个资源的线程数或控制流量的场景。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int workerCount = 5; // 工作线程数目
Semaphore semaphore = new Semaphore(2); // 信号量,初始许可证数量为2
// 创建工作线程并启动
for (int i = 0; i < workerCount; i++) {
Thread thread = new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证,如果没有可用的许可证,则阻塞等待
System.out.println("Worker thread start");
// 模拟工作
Thread.sleep(1000);
System.out.println("Worker thread finish");
semaphore.release(); // 释放许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
在这个示例中,我们使用了 Semaphore
类来实现信号量。信号量是一种同步工具,它可以控制对某个资源的访问数量。
在示例中,创建了一个初始许可证数量为2的信号量(semaphore
)。然后,创建了多个工作线程并启动。
每个工作线程在开始工作之前调用 acquire()
方法来获取许可证。如果当前有可用的许可证,则线程获取到许可证并继续执行工作。如果当前没有可用的许可证,则线程会阻塞等待,直到有其他线程释放许可证。
工作线程完成工作后调用 release()
方法来释放许可证,使得其他等待的线程可以获取许可证继续执行工作。
通过信号量,我们可以控制同时访问某个资源的线程数量,实现对并发访问的控制和限制。