目录
一.什么是JUC?
使用 JUC(Java Util Concurrent) 进行开发,可以帮助我们在多线程和并发编程中更高效地管理线程和同步操作。JUC 提供了多种用于处理并发问题的工具,例如线程池、同步工具、原子类、锁等。在Java的并发编程中,合理使用这些工具可以提高应用的性能和稳定性。
二.Lock锁:
1.什么是Lock?
在Java中,Lock
接口是 java.util.concurrent.locks
包的一部分,提供了比 synchronized
关键字更灵活的锁机制。Lock
提供了可重入、可中断的加锁机制,并且可以手动控制锁的获取与释放。
2.Lock锁的特性:
- 可重入:线程可以重复获取自己已经持有的锁而不会发生死锁。
- 公平锁与非公平锁:可以指定锁是公平的(等待时间最长的线程优先获取锁)还是非公平的(可能有插队的现象)。
- 可中断的锁:获取锁时可以被中断,而
synchronized
不具备这种特性。 - 锁的手动控制:可以手动控制锁的获取和释放,避免像
synchronized
一样必须等到方法或代码块执行结束时才释放锁。
3.Lock的两个主要实现类:
- ReentrantLock:可重入锁,支持公平锁和非公平锁。
- ReadWriteLock:读写锁,允许多个线程同时读取,但只允许一个线程写入,适用于读多写少的场景。
(1) ReentrantLock:
ReentrantLock
是最常用的 Lock
实现类,它具有可重入的特性,支持公平和非公平模式。下面通过一个简单的例子,展示如何使用 ReentrantLock
控制线程对共享资源的访问。
ReentrantLock
是 Java 中最常用的锁之一,是一个可重入锁,类似于 synchronized
关键字,但提供了更灵活的锁机制。
特点:
- 可重入性:如果一个线程已经获得了锁,可以重复进入锁定的代码块,而不会导致死锁。
- 灵活的锁获取方式:支持锁的非阻塞获取 (
tryLock()
),允许设置获取锁的超时时间。 - 公平锁和非公平锁:支持公平锁(根据等待的顺序依次获取锁)和非公平锁(可能存在“插队”现象),默认情况下是非公平锁。
- 可中断性:可以在等待锁时中断线程,这个功能是
synchronized
无法提供的。 - Condition支持:与
Condition
一起使用,提供类似wait()
/notify()
的线程通信机制,支持多个条件队列。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
// 创建ReentrantLock实例
private final Lock lock = new ReentrantLock();
private int count = 0;
// 增加共享资源的方法
public void increment() {
// 获取锁
lock.lock();
try {
count++;
System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
} finally {
// 确保锁在最终被释放
lock.unlock();
}
}
public static void main(String[] args) {
LockExample example = new LockExample();
// 创建多个线程执行increment方法
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
example.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
解析:
lock.lock()
:获取锁,只有获取了锁的线程才能继续执行,其他线程需要等待。try-finally
结构:在try
块中执行线程的核心逻辑。finally
中一定要释放锁,即使发生异常,也能保证锁的释放,避免死锁。- 可重入:
ReentrantLock
是可重入的,线程可以多次获取自己已经持有的锁,而不会发生死锁。
ReentrantLock
公平与非公平模式:
ReentrantLock
支持公平锁和非公平锁。公平锁按照线程的等待顺序分配锁,而非公平锁则允许线程插队,可能提升系统的吞吐量,但可能导致某些线程长时间得不到锁。
什么是公平锁和非公平锁:
- 公平锁:当多个线程等待锁时,公平锁会按照线程进入等待队列的顺序来分配锁,这样可以避免线程“饥饿”。
- 非公平锁:非公平锁有可能让一个新请求的线程抢到锁,而不一定按照等待时间顺序分配锁,可能导致某些线程长时间等待,但有时系统的吞吐量会更高。
代码示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
// true 表示公平锁,false 表示非公平锁
private final Lock lock = new ReentrantLock(true);
public void accessResource() {
lock.lock();
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");
}
}
public static void main(String[] args) {
FairLockExample example = new FairLockExample();
Runnable task = () -> {
for (int i = 0; i < 3; i++) {
example.accessResource();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
使用场景:
- 当只有一个线程可以访问某个共享资源时,适合使用
ReentrantLock
。 - 需要重入锁的功能,允许一个线程重复获取它已经持有的锁。
- 需要使用非阻塞锁或支持锁的超时获取时(通过
tryLock()
)。 - 在需要精确控制锁的获取和释放时,比如需要在多个代码块中进行锁的管理。
(2)ReadWriteLock:
ReadWriteLock
允许多个线程同时读,但写线程独占锁。适合读操作远多于写操作的场景。
ReadWriteLock
提供了两种锁,一种是用于读操作的共享锁 (readLock
),另一种是用于写操作的独占锁 (writeLock
)。它的主要设计目标是提升读操作的并发性,适用于读多写少的场景。
特点:
- 读写分离:允许多个线程同时读,但只允许一个线程写,且在写操作时,所有的读写操作都被阻塞。
- 共享读锁:多个线程可以同时持有读锁,只要没有线程持有写锁。
- 独占写锁:写锁是独占的,只有一个线程可以持有写锁,且持有写锁时,所有的读操作都会被阻塞。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int sharedData = 0;
// 读操作
public void readData() {
lock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " read: " + sharedData);
} finally {
lock.readLock().unlock(); // 释放读锁
}
}
// 写操作
public void writeData(int data) {
lock.writeLock().lock(); // 获取写锁
try {
sharedData = data;
System.out.println(Thread.currentThread().getName() + " wrote: " + sharedData);
} finally {
lock.writeLock().unlock(); // 释放写锁
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 读线程
Runnable readTask = example::readData;
Thread reader1 = new Thread(readTask, "Reader-1");
Thread reader2 = new Thread(readTask, "Reader-2");
// 写线程
Runnable writeTask = () -> example.writeData(42);
Thread writer = new Thread(writeTask, "Writer");
reader1.start();
reader2.start();
writer.start();
}
}
解析:
- 读写锁的优势:在读多写少的场景下,多个线程可以同时读取而不会阻塞,只有在写入时才会阻塞其他线程,提升了并发性能。
- 读锁:当多个线程同时获取读锁时,不会互相阻塞。
- 写锁:写锁是独占的,任何一个线程在写入时,其他线程(包括读线程和写线程)都无法获取锁。
使用场景:
- 适用于 读多写少 的场景,如缓存、配置文件的读取等。
- 当多个线程需要同时读取共享资源,但不希望多个线程同时进行写操作时,
ReadWriteLock
能够有效提升读操作的并发性能。- 例如:数据库的读取操作、内存数据的统计和报告生成等场景。
区别总结:
特性 | ReentrantLock | ReadWriteLock |
---|---|---|
锁类型 | 独占锁 | 读锁(共享锁)和写锁(独占锁) |
同时访问的线程数 | 只有一个线程可以持有锁 | 允许多个线程同时持有读锁,写锁独占 |
使用场景 | 适合任意场景,但并发读时性能一般 | 适合读多写少的场景,提高并发读取效率 |
公平性支持 | 支持公平锁和非公平锁的选择 | 默认是非公平的,但可以通过实现定制公平锁 |
锁的可中断性 | 可中断等待锁(lockInterruptibly() ) |
读锁和写锁都可以中断 |
代码复杂度 | 相对简单,只涉及一把锁 | 复杂一些,涉及读锁和写锁的区分 |
锁的用途 | 精细化控制锁,支持多种高级功能 | 提高读操作的并发性,避免读写冲突 |
三.Lock与synchronized
的比较:
- 锁的控制:
Lock
需要显式地获取和释放锁,可以手动控制,而synchronized
是隐式的,基于块或方法的结束来自动释放锁。 - 公平锁与非公平锁:
ReentrantLock
可以选择公平锁,而synchronized
无法做到这一点。 - 中断响应:
Lock
可以响应线程的中断,而synchronized
不支持中断。 - 性能:在低竞争的情况下,
synchronized
的性能接近甚至优于Lock
,但在高竞争时,Lock
提供的机制更加灵活且性能更好。
四.什么是阻塞线程?
阻塞线程指的是线程由于某种原因(如等待资源、锁或I/O操作)进入无法继续执行的状态,直到某个条件被满足或事件完成。阻塞线程不会占用 CPU 时间,但是它会一直处于等待状态,无法继续执行其他操作,直到被唤醒或解除阻塞。
阻塞线程的常见场景:
1. I/O 操作阻塞
- 原因:线程在执行文件读写、网络请求等 I/O 操作时,需要等待数据返回或写入完成,这会导致线程阻塞。
- 场景:读取大文件、从远程服务器获取数据、向数据库发送请求等。
解决方法:
- 使用 异步 I/O(如 NIO)来处理 I/O 操作,避免线程被阻塞。
- 采用 线程池 来管理 I/O 操作,使得 I/O 密集型任务不会阻塞主线程。
2. 等待锁(同步阻塞)
- 原因:当多个线程尝试访问共享资源时,线程需要等待锁的释放。如果锁被其他线程持有,当前线程会阻塞。
- 场景:使用
synchronized
或ReentrantLock
进行同步访问时,未获得锁的线程会阻塞。
解决方法:
- 尽量缩小锁的粒度,减少锁的持有时间,避免长时间的阻塞。
- 使用 非阻塞锁,如
tryLock()
,在锁不可用时不阻塞线程,而是返回失败,让线程可以去执行其他任务。 - 使用 读写锁(
ReadWriteLock
),允许多个线程同时进行读操作,但只有一个线程可以进行写操作。
3. 线程睡眠(Thread.sleep()
)
- 原因:调用
Thread.sleep()
方法会让线程进入休眠状态,直到指定时间过去为止。 - 场景:定时任务、等待某个事件的发生时,使用
Thread.sleep()
让线程暂时休眠。
解决方法:
- 避免使用
Thread.sleep()
进行时间控制,采用 定时器 或 异步调度 来处理延迟任务。 - 如果非要使用
sleep()
,尽量确保睡眠时间准确,不会影响整体性能。
4. 线程等待(wait()/notify()
)
- 原因:线程调用
wait()
方法后,会进入等待状态,直到另一个线程调用notify()
或notifyAll()
将其唤醒。 - 场景:生产者-消费者模式中,消费者在没有数据可消费时调用
wait()
阻塞,生产者生成数据后通过notify()
唤醒消费者。
解决方法:
- 使用 更高级的线程通信机制,如
CountDownLatch
、Semaphore
、Condition
来替代wait/notify
,提高可读性和可靠性。 - 采用 并发集合(如
BlockingQueue
)实现生产者-消费者模式,自动处理线程阻塞问题。
5. 网络连接阻塞
- 原因:网络操作(如 HTTP 请求、数据库连接等)通常是耗时操作,线程需要等待响应数据而阻塞。
- 场景:在处理大量网络请求时,线程可能因为等待服务器响应而阻塞。
解决方法:
- 采用 异步网络框架(如 Netty、Spring WebFlux)进行非阻塞式网络操作。
- 使用 线程池 来管理网络请求的并发,避免单线程阻塞。
6. 等待其他线程执行完成(join()
)
- 原因:调用
join()
方法的线程会阻塞,直到另一个线程执行完成。 - 场景:多个线程之间有依赖关系,某些线程必须等到其他线程完成后才能继续执行。
解决方法:
- 避免线程间的强依赖关系,尽量减少
join()
的使用。 - 可以通过 Future 或 CompletableFuture 来处理线程间的依赖关系,使线程可以异步获取结果,而不完全阻塞。
7. 死锁
- 原因:多个线程互相等待对方持有的锁,导致所有线程都无法继续执行,出现阻塞状态。
- 场景:线程A持有锁L1等待锁L2,而线程B持有锁L2等待锁L1,导致死锁。
解决方法:
- 避免嵌套锁定,减少锁的复杂性,确保锁的获取顺序一致,避免死锁。
- 使用 死锁检测算法 或 超时锁,如
tryLock()
,如果长时间未获取到锁,则放弃锁的等待。
总结:
Lock
提供了一种更加灵活的同步机制,在复杂的并发编程中发挥了关键作用。开发者可以根据场景选择不同的锁机制,如 ReentrantLock
用于可重入锁,ReadWriteLock
用于读多写少场景。通过合理使用这些锁机制,可以有效避免死锁问题并提高应用的并发性能。