哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
我们都知道一个真相,在 Java 里,synchronized
和 Lock
都是用于实现线程同步的机制。它们的主要目的是确保多个线程在并发执行时不会对共享资源进行冲突的访问,从而保证数据的一致性。虽然 synchronized
和 Lock
都能解决多线程的同步问题,但它们在一些特性和使用场景上存在明显的差异。到底有哪些差异呢?我们接着往下看。
1. synchronized
与 Lock
的基本概念
1.1 synchronized
synchronized
关键字,它是 Java 中的一种内置同步机制,用于控制线程对共享资源的访问。它可以用于方法或代码块中,确保某个时间内只有一个线程能够执行特定的代码,从而避免并发问题。
public synchronized void method() {
// 只有一个线程可以访问该方法
}
synchronized
关键字是 Java 内置的同步工具,通常比较简单易用,但它存在一些限制,比如无法实现灵活的锁机制、无法中断等。
1.2 Lock
而 Lock
是 Java 5 中引入的一个接口,属于 java.util.concurrent
包。它提供了比 synchronized
更加灵活的同步控制。常用的实现类包括 ReentrantLock
。
Lock
它提供了更多的功能,比如可以尝试加锁、可中断的加锁等,允许程序员在更细粒度上控制锁的行为。例如:
Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 执行临界区操作
} finally {
lock.unlock(); // 释放锁
}
2. Lock
相比于 synchronized
的优势
虽然 synchronized
和 Lock
都能达到线程同步的目的,但 Lock
提供了更多的灵活性和控制能力,适用于更复杂的多线程应用场景。以下是 Lock
优于 synchronized
的几个关键点:
2.1 可中断的锁(Interruptible)
synchronized
在执行时,如果当前线程无法获得锁,它将会一直等待直到获取锁为止。这种方式的一个问题是,如果线程被阻塞在 synchronized
上,它不能响应中断。
而 Lock
允许通过 lock.lockInterruptibly()
方法来实现可中断的锁。如果线程在获取锁时被中断,它会抛出 InterruptedException
异常,从而可以让线程在被中断时做一些清理工作,提升系统的响应性。
代码示例
如下是示例代码,仅供参考:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author 喵手
* @date: 2025-04-14
*/
public class InterruptibleLockExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 创建一个线程,模拟一个需要锁的临界区
Thread workerThread = new Thread(new Worker());
// 启动 workerThread
workerThread.start();
// 主线程睡眠一段时间,确保 workerThread 先尝试获取锁
Thread.sleep(500);
// 中断 workerThread,模拟中断场景
System.out.println("Main thread is interrupting worker thread...");
workerThread.interrupt();
}
static class Worker implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " is trying to acquire the lock...");
// 尝试获取锁,如果线程被中断,抛出 InterruptedException
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
// 模拟执行临界区代码
Thread.sleep(2000); // 模拟任务执行,时间较长
} finally {
// 确保在任务完成后释放锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
} catch (InterruptedException e) {
// 处理线程中断的逻辑
System.out.println(Thread.currentThread().getName() + " was interrupted while waiting for the lock.");
// 可以进行必要的清理工作
}
}
}
}
运行截图展示
如下是正式环境演示截图:
代码解析
如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。如上案例代码是一个完整的示例程序,演绎了如何使用 lock.lockInterruptibly()
实现一个可中断的锁机制。在这个例子中,线程会在锁定期间被中断,演示了如何捕获 InterruptedException
并进行相应的处理中断。
- 创建锁对象:
private static final Lock lock = new ReentrantLock();
使用 ReentrantLock
类创建一个可中断的锁。与 synchronized
不同,ReentrantLock
提供了更强的灵活性,例如可中断的锁。
lock.lockInterruptibly()
:lock.lockInterruptibly();
这是关键点,lock.lockInterruptibly()
方法会尝试获取锁。如果当前线程在等待锁时被中断,抛出 InterruptedException
异常。相比于 synchronized
,它允许线程在获取锁的过程中响应中断。
-
模拟中断: 在
main()
方法中,首先启动了一个工作线程workerThread
,它会尝试获取锁并执行一些任务。接着,主线程通过workerThread.interrupt()
来中断工作线程,这模拟了一个中断的场景。 -
处理中断:
catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + " was interrupted while waiting for the lock."); }
在 Worker
线程中,如果它在等待锁时被中断,会抛出 InterruptedException
异常,捕获后可以进行一些清理工作或其他逻辑处理。
- 锁的释放:
lock.unlock();
无论任务是否成功完成,finally
块中都会释放锁,确保不会发生死锁。unlock()
是 lock()
的配对方法,必须在临界区代码执行完毕后调用。
程序执行流程:
workerThread
启动并尝试获取锁,但主线程通过sleep
保证它的获取锁尝试会稍微被延迟。- 主线程中断
workerThread
,此时workerThread
在等待锁的时候会被中断,并抛出InterruptedException
。 workerThread
捕获异常,并在异常处理中输出 "Thread was interrupted while waiting for the lock"。- 线程最终释放锁,确保没有遗留资源。
可能的输出:
Thread-0 is trying to acquire the lock...
Thread-0 acquired the lock.
Main thread is interrupting worker thread...
Thread-0 released the lock.
Thread-0 was interrupted while waiting for the lock.
总结:
lock.lockInterruptibly()
方法使得线程能够在等待锁时响应中断。- 通过捕获
InterruptedException
,可以在被中断时做出合理的响应,例如进行资源清理、终止操作等。 finally
块保证了在任务完成后,锁总是会被正确释放,从而避免死锁。
2.2 尝试获取锁(Try Lock)
synchronized
在获取锁时是阻塞的,也就是说,如果当前线程无法获取锁,它将一直等待,直到可以获取锁。这在某些场景下会导致线程不必要的阻塞。
而 Lock
提供了 tryLock()
方法,允许线程在获取锁时不阻塞,而是尝试获取锁,如果获取不到就返回 false
,或者通过指定超时时间来等待一定的时间再放弃。
代码示例
如下是示例代码,仅供参考:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author 喵手
* @date: 2025-04-14
*/
public class TryLockExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 创建并启动两个线程,模拟并发争夺锁的场景
Thread thread1 = new Thread(new Task(), "Thread-1");
Thread thread2 = new Thread(new Task(), "Thread-2");
thread1.start();
thread2.start();
// 主线程稍作休眠,让两个线程并发执行
Thread.sleep(100);
}
static class Task implements Runnable {
@Override
public void run() {
// 尝试获取锁
if (lock.tryLock()) {
try {
// 获取到锁后执行临界区代码
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
// 模拟任务执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 确保在完成任务后释放锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
} else {
// 如果无法获取到锁,执行其他操作
System.out.println(Thread.currentThread().getName() + " could not acquire the lock, doing other tasks.");
}
}
}
}
如果当前锁不可用,线程可以选择不等待直接执行其他任务,避免了不必要的阻塞。
运行截图展示
如下是正式环境演示截图:
代码解析
如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。如上这段案例代码我主要是演示了如何使用 ReentrantLock
和 tryLock
来处理线程的锁竞争。具体来说,tryLock
方法允许线程在一定时间内尝试获取锁,如果无法立即获得锁,它将不会阻塞,而是返回 false
,从而允许线程执行其他操作。
- 锁的创建:
private static final Lock lock = new ReentrantLock();
这里创建了一个 ReentrantLock
实例,作为锁对象。ReentrantLock
是 Java 中提供的显式锁,相比于 synchronized
,它允许更细粒度的控制,如超时获取锁等功能。
- 线程启动:
Thread thread1 = new Thread(new Task(), "Thread-1"); Thread thread2 = new Thread(new Task(), "Thread-2"); thread1.start(); thread2.start();
代码创建并启动了两个线程(thread1
和 thread2
),每个线程都执行 Task
类中的 run
方法。
-
线程任务 (
Task
类):每个线程的
run
方法尝试通过tryLock
获取锁:if (lock.tryLock()) {
-
tryLock()
:如果锁没有被其他线程占用,当前线程将立即获得锁并返回true
,否则返回false
。这种方式不会让线程阻塞,如果锁不可用,线程将继续执行后续代码而不是等待。 -
如果获取到锁,线程会进入临界区执行任务:
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
-
这里模拟了任务执行,通过 Thread.sleep(1000)
假设任务执行时间为 1 秒。
- 完成任务后,线程释放锁:
lock.unlock(); System.out.println(Thread.currentThread().getName() + " released the lock.");
-
锁未被获取的处理:
如果线程无法获取到锁,它会输出一条日志并进行其他操作:
System.out.println(Thread.currentThread().getName() + " could not acquire the lock, doing other tasks.");
这部分代码演示了
tryLock
的优势:线程不会因为等待锁而被阻塞,能够继续执行其他非临界区的代码,提升系统的并发性能。 -
主线程的睡眠:
Thread.sleep(100);
主线程在启动子线程后稍作休眠,以确保子线程能够并发执行。虽然这个睡眠时间很短,但它能让线程有足够的时间来争夺锁。
总结:
- 使用
ReentrantLock
和tryLock
可以避免传统synchronized
锁的阻塞行为。如果线程无法立即获取到锁,它会执行其他任务而不是一直等待,这对于高并发场景下的系统性能提升非常有帮助。 - 这段代码展示了
tryLock
的基本使用方式,通过灵活的锁竞争管理,避免了线程的死锁和资源浪费。
2.3 可重入锁(Reentrant)
虽然 synchronized
本身也是可重入的,即一个线程可以多次获得同一个锁,但使用 Lock
通过 ReentrantLock
可以更加明确地控制可重入性。
ReentrantLock
提供了 getHoldCount()
方法,它可以帮助开发人员查看当前线程持有锁的次数,进一步增强了调试和排错的能力。
代码示例
如下是示例代码,仅供参考:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author 喵手
* @date: 2025-04-14
*/
public class ReentrantLockExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 创建并启动一个线程,模拟多次获取同一把锁的情况
Thread thread = new Thread(new Task());
thread.start();
// 主线程稍作休眠,确保子线程先执行
Thread.sleep(100);
// 主线程获取锁并输出锁的持有次数
lock.lock();
System.out.println("Main thread: Lock hold count after locking: " + ((ReentrantLock) lock).getHoldCount());
lock.unlock();
}
static class Task implements Runnable {
@Override
public void run() {
// 获取锁并模拟多次加锁
lock.lock();
System.out.println(Thread.currentThread().getName() + ": Lock hold count after first lock: " + ((ReentrantLock) lock).getHoldCount());
// 再次获取同一把锁
lock.lock();
System.out.println(Thread.currentThread().getName() + ": Lock hold count after second lock: " + ((ReentrantLock) lock).getHoldCount());
// 执行临界区代码
try {
Thread.sleep(1000); // 模拟任务执行
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 确保在完成任务后释放锁
lock.unlock();
System.out.println(Thread.currentThread().getName() + ": Lock hold count after first unlock: " + ((ReentrantLock) lock).getHoldCount());
lock.unlock();
System.out.println(Thread.currentThread().getName() + ": Lock hold count after second unlock: " + ((ReentrantLock) lock).getHoldCount());
}
}
}
}
运行截图展示
如下是正式环境演示截图:
代码解析
如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。如上这段案例代码展示了使用 ReentrantLock
在多次获取锁时如何管理锁的持有计数。通过 ReentrantLock
,同一线程可以多次获取同一把锁,而每次获取锁时都会增加锁的持有计数 (hold count
)。当调用 unlock()
时,锁的持有计数会减少,直到计数归零,锁才会被真正释放。
-
锁的创建
private static final Lock lock = new ReentrantLock();
这里通过
ReentrantLock
创建了一个显式的锁对象lock
。与synchronized
关键字不同,ReentrantLock
提供了更多的控制功能,比如能够查询锁的持有计数、尝试获取锁等。 -
主线程获取锁并查询持有计数
lock.lock(); System.out.println("Main thread: Lock hold count after locking: " + ((ReentrantLock) lock).getHoldCount()); lock.unlock();
- 主线程在执行时先获取锁,并通过
(ReentrantLock) lock.getHoldCount()
查询当前锁的持有计数。因为主线程只获取了锁一次,持有计数应该是1
。 - 然后,主线程释放锁,持有计数减到
0
。
- 主线程在执行时先获取锁,并通过
-
子线程中的多次加锁
lock.lock(); System.out.println(Thread.currentThread().getName() + ": Lock hold count after first lock: " + ((ReentrantLock) lock).getHoldCount()); lock.lock(); System.out.println(Thread.currentThread().getName() + ": Lock hold count after second lock: " + ((ReentrantLock) lock).getHoldCount());
- 子线程在获取锁时,第一次获取锁后,持有计数变为
1
。 - 然后,子线程再次获取同一把锁,持有计数增加至
2
。 ReentrantLock
允许同一线程多次加锁,这是它与synchronized
的一个重要区别,synchronized
不允许一个线程多次获取同一把锁。
- 子线程在获取锁时,第一次获取锁后,持有计数变为
-
临界区代码的执行
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
在子线程成功加锁之后,模拟执行临界区任务。这里使用
Thread.sleep(1000)
来模拟任务的执行时间。 -
释放锁和持有计数的变化
lock.unlock(); System.out.println(Thread.currentThread().getName() + ": Lock hold count after first unlock: " + ((ReentrantLock) lock).getHoldCount()); lock.unlock(); System.out.println(Thread.currentThread().getName() + ": Lock hold count after second unlock: " + ((ReentrantLock) lock).getHoldCount());
- 子线程执行完成后,第一次调用
unlock()
将持有计数减少至1
,因为它仍然持有一把锁。 - 第二次调用
unlock()
后,持有计数减至0
,此时锁才真正释放。
- 子线程执行完成后,第一次调用
总结:
这段代码展示了如何在使用 ReentrantLock
时管理同一线程多次获取锁的情况,并追踪锁的持有计数。通过 ReentrantLock
,同一线程可以多次获取同一把锁,适用于那些需要反复获取同一资源的场景。同时,getHoldCount()
可以帮助开发者更清楚地了解锁的状态,提升调试效率。
2.4 锁的公平性(Fairness)
在 synchronized
中,锁的获取顺序是不可预测的,可能导致某些线程永远无法获得锁(比如线程饿死)。然而,ReentrantLock
提供了一个可选的公平性策略,确保请求锁的线程按照请求顺序获取锁,避免了线程饥饿问题。
可以通过 ReentrantLock
的构造函数来指定是否使用公平锁:
package com.example.javase.ms.jsjh.Q202504.demo4;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Author 喵手
* @date: 2025-04-14
*/
public class FairnessExample {
private static final int NUM_THREADS = 5;
public static void main(String[] args) {
// 使用公平锁
Lock fairLock = new ReentrantLock(true);
// 使用非公平锁
Lock nonFairLock = new ReentrantLock(false);
// 创建线程,分别使用公平锁和非公平锁
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
testLock(fairLock, "Fair Lock Thread " + threadId);
testLock(nonFairLock, "Non-Fair Lock Thread " + threadId);
});
threads[i].start();
}
}
// 测试锁
private static void testLock(Lock lock, String threadName) {
lock.lock();
try {
System.out.println(threadName + " acquired the lock");
Thread.sleep(100); // 模拟任务
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(threadName + " releasing the lock");
lock.unlock();
}
}
}
公平锁确保了最先请求的线程优先获取锁。
运行截图展示
如下是正式环境演示截图:
代码解析
如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。如上这段案例代码主要是展示了如何使用 ReentrantLock
的公平锁和非公平锁,并通过多线程来比较两者的行为。具体来说,它通过创建多个线程,分别对公平锁和非公平锁进行测试,帮助我们理解两种锁的区别和行为特性。
- 锁的创建
Lock fairLock = new ReentrantLock(true);
Lock nonFairLock = new ReentrantLock(false);
fairLock
是一个 公平锁,通过将ReentrantLock
的构造参数设置为true
,表示锁是公平的。- 公平锁遵循先来先服务(FIFO)原则,即线程请求锁的顺序与线程获得锁的顺序相同。
nonFairLock
是一个 非公平锁,通过将构造参数设置为false
,表示锁是非公平的。- 非公平锁的特点是,它会尽量让当前线程直接获取锁,而不是遵循请求顺序。这样做的优点是可以提高性能,但缺点是可能导致某些线程长期无法获取锁(即“饿死”现象)。
- 线程创建和启动
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
testLock(fairLock, "Fair Lock Thread " + threadId);
testLock(nonFairLock, "Non-Fair Lock Thread " + threadId);
});
threads[i].start();
}
- 程序创建了 5 个线程(
NUM_THREADS = 5
)。 - 每个线程会依次执行两次
testLock
方法,第一次使用公平锁(fairLock
),第二次使用非公平锁(nonFairLock
)。 - 每个线程执行时会尝试获取并释放两个不同的锁,并打印获取锁和释放锁的相关信息。
- 锁的测试和任务模拟
private static void testLock(Lock lock, String threadName) {
lock.lock(); // 获取锁
try {
System.out.println(threadName + " acquired the lock");
Thread.sleep(100); // 模拟任务
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(threadName + " releasing the lock");
lock.unlock(); // 释放锁
}
}
-
lock.lock()
:线程尝试获取锁。如果锁已被其他线程占用,线程将会等待(在公平锁中按照先后顺序等待;在非公平锁中,如果当前线程能够直接获取锁,它就不会被阻塞)。 -
Thread.sleep(100)
:模拟线程在获取锁后的任务执行,暂停 100 毫秒。 -
lock.unlock()
:线程执行完任务后释放锁,其他线程可以获取到锁并进入临界区。
- 锁的行为
-
公平锁:所有线程按照请求锁的顺序依次获取锁。例如,线程
Thread-0
会先于Thread-1
获取锁,并依此类推。 -
非公平锁:线程不会严格按照请求顺序获取锁。在某些情况下,后到的线程(如
Thread-1
)可能会抢先于先到的线程(如Thread-0
)获取锁。这是由于非公平锁允许线程抢占锁,而不是排队等候。
总结:
公平锁 vs 非公平锁
特性 | 公平锁 (Fair Lock) | 非公平锁 (Non-Fair Lock) |
---|---|---|
构造函数参数 | new ReentrantLock(true) | new ReentrantLock(false) |
锁获取顺序 | 先到先得,遵循 FIFO 顺序 | 可能会有线程抢占锁,导致后到的线程先得锁 |
性能 | 较低(由于排队等候机制带来的性能开销) | 较高(线程可能频繁获得锁,性能较好) |
适用场景 | 要求严格公平的场景,如票务系统 | 适合需要高性能的场景,如高并发计算任务 |
2.5 更细粒度的锁控制(Condition)
synchronized
提供了 wait()
和 notify()
方法,但这些方法只能通过 Object
对象来进行通信。与之不同,Lock
接口提供了 Condition
类,允许线程在锁的控制下进行更精细的条件等待和通知机制。
通过 Condition
,线程可以在满足特定条件时被唤醒,而不需要其他线程通过 notify()
或 notifyAll()
来唤醒它。
代码示例
如下是示例代码,仅供参考:
package com.example.javase.ms.jsjh.Q202504.demo4;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
/**
* @Author 喵手
* @date: 2025-04-14
*/
public class ProducerConsumerExample {
private static final Lock lock = new ReentrantLock(); // 创建锁
private static final Condition condition = lock.newCondition(); // 创建条件变量
private static int data = 0; // 模拟共享数据
private static final int MAX_DATA = 10;
public static void main(String[] args) {
// 启动生产者线程
Thread producer = new Thread(new Producer());
producer.start();
// 启动消费者线程
Thread consumer = new Thread(new Consumer());
consumer.start();
}
// 生产者任务
static class Producer implements Runnable {
@Override
public void run() {
while (true) {
try {
lock.lock(); // 获取锁
while (data == MAX_DATA) {
// 如果数据已满,生产者线程等待
System.out.println("Producer: Data is full, waiting...");
condition.await(); // 等待消费
}
// 模拟生产数据
data++;
System.out.println("Producer: Produced data. Current data: " + data);
// 唤醒消费者线程
condition.signal(); // 通知消费者线程可以消费数据
Thread.sleep(500); // 模拟生产延迟
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
}
// 消费者任务
static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
try {
lock.lock(); // 获取锁
while (data == 0) {
// 如果没有数据,消费者线程等待
System.out.println("Consumer: No data available, waiting...");
condition.await(); // 等待生产
}
// 模拟消费数据
data--;
System.out.println("Consumer: Consumed data. Current data: " + data);
// 唤醒生产者线程
condition.signal(); // 通知生产者线程可以生产数据
Thread.sleep(500); // 模拟消费延迟
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
}
}
运行截图展示
如下是正式环境演示截图:
代码解析
如下是我对上述案例代码的详细剖析,目的就是为了帮助大家更好的理解代码,吃透原理。
一、程序目标简述
如上这段案例代码我主要是实现生产者-消费者问题,是并发编程中非常典型的一个案例。它描述了两个线程之间的协作关系:一个线程负责“生产”数据,另一个线程负责“消费”数据,并且两者要协调好,不允许“生产太多”或“消费太快”。
二、核心机制解析
- 使用了什么机制?
程序中使用了 ReentrantLock
(可重入锁)和 Condition
(条件变量)这两个类来实现线程间的同步与通信。
- ReentrantLock:保证同一时间内只有一个线程能够访问共享数据,确保数据安全。
- Condition:允许线程在特定条件不满足时挂起(等待),等到条件满足后再唤醒继续执行,实现线程之间的精细协作。
- 共享资源是什么?
程序中有一个共享变量 data
,代表当前缓冲区中保存的数据数量。还有一个上限 MAX_DATA
,比如设为10,表示“最多可以容纳10个数据”。
- 生产者线程干了什么?
- 它会反复尝试向缓冲区“放入”数据,也就是给
data
加一。 - 但它在加数据之前会先判断当前数据是否已经满了:
- 如果满了,它就调用
await()
让自己休眠(释放锁,等待消费者来消费)。 - 如果没满,就正常加一,然后通过
signal()
通知消费者“我生产好了,可以消费了”。
- 如果满了,它就调用
- 每次生产后,都会有一个短暂的休眠时间(模拟生产的延迟)。
- 消费者线程干了什么?
- 它会反复尝试从缓冲区“取出”数据,也就是把
data
减一。 - 但在减之前会先判断当前是否有数据:
- 如果没有(等于0),它就进入等待状态(也是通过
await()
)。 - 如果有数据,它就正常消费,然后用
signal()
通知生产者“我消费完了,你可以继续生产了”。
- 如果没有(等于0),它就进入等待状态(也是通过
三、线程配合流程
- 一开始生产者开始运行,发现空间够,它生产一个数据并唤醒消费者。
- 消费者被唤醒,看到有数据了,于是消费掉,然后再通知生产者可以继续生产。
- 这个过程不断循环进行,直到达到某些临界状态,比如“满了”或“空了”,才会发生等待与唤醒。
四、代码中体现的并发思想
- 互斥控制:通过锁,确保只有一个线程能修改
data
,避免竞态条件。 - 条件判断:通过
while
判断是否可以生产或消费(防止虚假唤醒)。 - 线程通信:用
await()
和signal()
来控制线程的启动与暂停,节省系统资源。
2.6 性能优化
在某些复杂的并发场景下,ReentrantLock
可能比 synchronized
更具性能优势。ReentrantLock
提供了尝试锁定、锁中断等多种机制,这些机制能够根据实际场景进行优化,减少线程的阻塞时间和上下文切换,提升程序的并发性能。
3. Lock
与 synchronized
的对比
特性 | synchronized | Lock |
---|---|---|
是否可中断 | 不可中断 | 可中断,通过 lockInterruptibly() 方法实现 |
是否公平 | 默认公平(无明确策略) | 可选择公平性,ReentrantLock(true) |
可重入性 | 支持可重入 | ReentrantLock 支持可重入,且可查看持有次数 |
灵活性 | 不如 Lock 灵活 | 提供了更多的控制方法,比如 tryLock() 等 |
性能 | 对于简单的同步,性能较好,但缺少更多优化机制 | 提供了更细粒度的控制和优化,适合复杂的同步场景 |
是否支持条件变量 | 不支持 | 支持,通过 Condition 对象实现条件变量操作 |
4. 总结
尽管 synchronized
已经足够满足大部分简单的线程同步需求,但在一些复杂的多线程场景中,Lock
提供的更多功能使得它更加灵活和强大。特别是在需要可中断的锁、尝试获取锁、可重入锁、条件变量等功能时,Lock
显示出了它的优势。对于复杂的业务逻辑,Lock
能够提供更细粒度的控制,避免线程饥饿、死锁等问题。
因此,当需要更多的控制、灵活性和性能优化时,Lock
是一个更好的选择。对于简单的同步需求,synchronized
依然是一个简单有效的解决方案。
所以,现在你们明白了Lock
比 synchronized
强在哪里了吧?
... ...
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
... ...
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!