ava Concurrency API中的Lock接口是用于实现线程同步
的一种机制。它提供了比传统的 synchronized块更灵活的方式来控制多个线程之间的访问共享资源的方式。
Lock接口的主要方法是lock()
和unlock()
, 它们分别用于获取锁
和释放锁
。
它的优势有:
选择是否支持公平性
。公平锁是指等待时间最长的线程将获得锁的访问权。而 synchronized 关键字默认是非公平的,没有提供选择的机会。等待锁的过程中被中断
。而使用synchronized关键字时,线程一旦进入等待状态,只能等待锁的释放,不能被中断。它实现了 Lock 接口。与传统的 synchronized 关键字相比,ReentrantLock 提供了更多的灵活性和功能。
ReentrantLock 的可重入性体现在同一个线程可以多次获得同一个锁,而不会发生死锁。这是通过内部维护一个持有锁的线程计数器来实现的。synchronized关键字也同样具备可重入性
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockReentrancyExample {
private final Lock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 第一次获得锁
try {
innerMethod(); // 在同一线程中调用另一个方法,再次获得锁
} finally {
lock.unlock(); // 第一次释放锁
}
}
public void innerMethod() {
lock.lock(); // 第二次获得锁
try {
// 执行需要同步的代码块
System.out.println("线程 " + Thread.currentThread().getName() + " 获得锁");
} finally {
lock.unlock(); // 第二次释放锁
System.out.println("线程 " + Thread.currentThread().getName() + " 释放锁");
}
}
public static void main(String[] args) {
ReentrantLockReentrancyExample example = new ReentrantLockReentrancyExample();
// 启动一个线程调用 outerMethod 方法
new Thread(() -> {
example.outerMethod();
}).start();
}
}
outerMethod 方法中调用了 innerMethod 方法,而这两个方法都在同一个线程中执行。当线程首次进入 outerMethod 时,它成功获得了锁,并在 innerMethod 中再次成功获得了同一个锁。在 innerMethod 中释放锁时,锁的计数器减为零,才真正释放了锁。这种方式确保了同一线程可以在持有锁的情况下多次进入同一个锁保护的代码块。
我们看下执行流程:
什么场景下,会使用到锁的可重入性
public class RecursiveExample {
private final Object lock = new Object();
public void recursiveMethod(int count) {
synchronized (lock) {
if (count > 0) {
System.out.println("Count: " + count);
recursiveMethod(count - 1); // 递归调用,再次获取相同锁
}
}
}
public static void main(String[] args) {
RecursiveExample example = new RecursiveExample();
example.recursiveMethod(3);
}
}
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class NestedCallExample {
private final Lock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 第一次获得锁
try {
System.out.println("执行 outerMethod");
innerMethod(); // 在同一线程中再次获取相同锁
} finally {
lock.unlock(); // 第一次释放锁
}
}
public void innerMethod() {
lock.lock(); // 第二次获得锁
try {
System.out.println("执行 innerMethod");
} finally {
lock.unlock(); // 第二次释放锁
}
}
public static void main(String[] args) {
NestedCallExample example = new NestedCallExample();
example.outerMethod();
}
}
ReentrantLock可以选择是公平锁(fair lock)还是非公平锁(non-fair lock),而 synchronized关键字默认是非公平的。
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
需要注意的是,虽然公平锁确保了锁的公平性,但在高并发环境下,公平锁的性能可能相对较低,因为每次都要考虑等待队列中线程的顺序。非公平锁可能会在性能上有一些优势,但可能导致某些线程长时间无法获得锁。
在实际应用中,一般情况下会使用非公平锁,因为在高并发的情况下,公平锁可能会导致线程频繁切换,影响性能。非公平锁虽然在一些情况下可能会引入不公平的竞争,但能够更好地提高并发性能。选择使用公平锁还是非公平锁需要根据具体的业务场景和性能需求来权衡。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairnessExample {
private static final int THREAD_COUNT = 5;
private static final Lock fairLock = new ReentrantLock(true); // 公平锁
private static final Lock unfairLock = new ReentrantLock(false); // 非公平锁
private static void performTask(Lock lock, String lockType) {
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println("Thread " + Thread.currentThread().getName() +
" acquired " + lockType + " lock, counter: " + i);
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
// 使用公平锁
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> performTask(fairLock, "fair")).start();
}
// 使用非公平锁
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> performTask(unfairLock, "unfair")).start();
}
}
}
在上述示例中,创建了两个 ReentrantLock,一个是公平锁,一个是非公平锁。在 performTask 方法中,线程通过 lock() 方法获取锁,执行一些操作,然后通过 unlock() 方法释放锁。运行这个程序,你会观察到使用公平锁时,线程按照请求锁的顺序获取锁,而使用非公平锁时,线程可能会插队成功,不按照请求的顺序获取锁。这是通过 ReentrantLock 的公平性来体现的。
可中断性是指在一个线程等待获取锁的过程中,如果其他线程对该等待线程进行中断(调用 interrupt() 方法),那么等待线程能够感知到中断,并有机会响应中断而不是一直等待下去。在这种情况下,等待线程会收到 InterruptedException 异常。
在使用ReentrantLock的lockInterruptibly()
方法时,线程可以响应中断,即在等待锁的过程中,如果线程被其他线程中断,它会立即抛出InterruptedException 异常,而不是一直等待。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockInterruptExample {
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可中断获取锁
try {
System.out.println("Thread 1 acquired the lock");
Thread.sleep(2000); // 模拟持有锁的一些操作
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Thread 1 interrupted while waiting for the lock");
}
});
Thread thread2 = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可中断获取锁
try {
System.out.println("Thread 2 acquired the lock");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Thread 2 interrupted while waiting for the lock");
}
});
// 启动第一个线程,并让它持有锁
thread1.start();
// 等待一段时间,确保第一个线程先获取到锁
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 启动第二个线程,但在获取锁之前中断它
thread2.start();
thread2.interrupt();
}
}
结果输出:
Thread 1 acquired the lock
Thread 2 interrupted while waiting for the lock
第二个线程在等待锁的过程中, 感知到被中断, 并抛出InterruptedException, 从而可以在中断的处理代码块中执行相应的逻辑。
使用synchronized关键字
进行同步的代码块或方法在等待锁的过程中是无法响应中断
的。
"等待锁时响应中断"和"中断线程的执行"是两个不同的概念
虽然都是通过调用interrupt()来中断线程, 但是两者有实质区别
ReentrantLock提供了一种超时获取锁的方式,即通过tryLock(long time, TimeUnit unit)
方法,线程在一定的时间范围内尝试获取锁,如果在指定的时间内获取到锁,则返回 true,否则返回 false。这样可以避免线程一直阻塞等待锁,而是在一定时间内尝试获取,如果获取不到则可以执行其他逻辑或放弃锁的获取。
方法的参数 time 表示等待时间的数量,unit 表示等待时间的单位。如果在指定的时间内获取到锁,方法返回 true,否则返回 false。
// 线程一: 获取锁, 并休眠3秒, 注意在finally中调用unlock
Thread thread1 = new Thread(() -> {
try {
System.out.println("线程一获取锁");
lock.lock();
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
System.out.println("线程一等待锁时响应中断");
} finally {
lock.unlock();
}
});
// 线程二: 超时获取锁, 如果2秒内未获取锁, 则
Thread thread2 = new Thread(() -> {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println("线程二获取锁");
} finally {
lock.unlock();
}
} else {
System.out.println("线程二放弃获取锁");
}
} catch (InterruptedException e) {
System.out.println("线程二等待锁时响应中断");
}
});
thread1.start();
// 休眠, 保证线程一先获取锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException();
}
thread2.start();
// thread2.interrupt();
因为线程一先获取了锁, 并休眠3秒, 而休眠期间, 线程二等待锁的释放, 并愿意等待2秒. 但2秒之后, 线程一并没有释放锁, 所以tryLock(2, TimeUnit.SECONDS)
返回false, 线程二放弃获取锁
释放锁的时机
很多人使用tryLock时, 会犯一个错误, 会在最外层的try中通过finally来释放锁, 这样是错误的. 因为tryLock返回true, 才能有释放锁(unlock)的操作, 因为你持有了锁, 才能释放锁. 如果放弃获取锁, 还释放锁, 会导致其他线程持有的锁被释放
tryLock等待锁时, 线程被中止会怎么样
在使用 tryLock() 方法尝试获取锁的过程中,如果线程在等待锁的过程中被中断,tryLock() 方法会响应中断,即会抛出 InterruptedException 异常。
ReentrantLock提供了与锁关联的条件变量(Condition),条件变量允许线程以灵活的方式进行等待和通知,以实现更复杂的线程协作
。条件变量提供了一种在某些条件不满足时线程等待的机制,并在条件满足时通知其他线程的方式。
public class ThreadTest {
private static final Lock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static boolean isConditionMet = false;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 等待条件满足
while (!isConditionMet) {
condition.await();
}
// 执行条件满足后的操作
System.out.println("condition is met!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 改变变量
isConditionMet = true;
// 通知等待的线程条件已满足
condition.signal();
System.out.println("通知等待的线程条件已满足");
} finally {
// 解锁
lock.unlock();
}
});
thread1.start();
TimeUnit.SECONDS.sleep(1);
thread2.start();
}
}
上面的代码额外使用了isConditionMet来判断等待条件是否满足, 这是为了防止虚假唤醒
condition.await()会阻塞线程运行吗?
是的,condition.await()方法会阻塞当前线程的执行。
在await()被调用时,它会释放当前线程持有的锁,使其他线程有机会获取这个锁。当条件满足时,通过调用signal()或signalAll()来唤醒一个或所有等待的线程。被唤醒的线程会尝试重新获得锁,然后继续执行。
isConditionMet并没有使用voliate, 在线程中是怎么感知它的值变成true
虽然确实没有使用volatile关键字来声明isConditionMet变量,但是它是在同一个锁的保护下被读取和修改的。
在Java中,当一个线程在获取锁的时候,会从主内存中读取共享变量的最新值,并在执行过程中将其缓存在线程的本地内存中。其他线程在获取同一把锁时,会从主内存中重新读取共享变量的值。因为 ReentrantLock 是一个可重入锁,同一个线程在获取锁的时候,不会真正释放锁,因此它的本地内存中的共享变量值是可见的。
在你的例子中,awaitCondition 方法中的 while (!isConditionMet) 中的读取操作和 signalCondition 方法中的写入操作都在同一个 ReentrantLock 的保护下,这确保了线程在获取锁的时候能够看到最新的共享变量值。
虽然没有使用 volatile,但由于所有对isConditionMet的读取和写入都在同一把锁的保护下,因此在这个特定的上下文中,是可以保证可见性的。要注意的是,如果isConditionMet不是在同一个锁的保护下进行读写,或者在其他地方可能会涉及到多线程并发访问,那么使用 volatile 或其他同步手段可能是更安全的选择。
条件变量与多线程编程中wait()和notify()机制的异同
相同点:
线程等待
条件变量的await()方法和Object的wait()方法都允许线程等待某个条件的发生.
线程通知
条件变量的signal()方法和notify()方法都用于通知等待线程条件的变化,以便它们有机会再次检查条件是否满足。
区别:
public class ThreadTest {
private static final Lock lock = new ReentrantLock();
private static final Condition condition1 = lock.newCondition();
private static final Condition condition2 = lock.newCondition();
private static boolean isConditionMet1 = false;
private static boolean isConditionMet2 = false;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 等待条件满足
while (!(isConditionMet1 && isConditionMet2)) {
condition1.await();
System.out.println("条件一已满足");
condition2.await();
System.out.println("条件二已满足");
}
// 执行条件满足后的操作
System.out.println("condition is met!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 改变变量
isConditionMet1 = true;
// 通知等待的线程条件已满足
condition1.signal();
System.out.println("通知等待的线程条件已满足");
} finally {
// 解锁
lock.unlock();
}
});
Thread thread3 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 改变变量
isConditionMet2 = true;
// 通知等待的线程条件已满足
condition2.signal();
System.out.println("通知等待的线程条件已满足");
} finally {
// 解锁
lock.unlock();
}
});
thread1.start();
TimeUnit.SECONDS.sleep(1);
thread2.start();
thread3.start();
}
}
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
long timeout = 1000; // 等待1秒
while (condition不满足) {
timeout = condition.await(timeout, TimeUnit.MILLISECONDS);
if (timeout <= 0) {
// 超时处理
break;
}
}
// 执行相应操作
} finally {
lock.unlock();
}
public class ThreadTest {
private static final Lock lock = new ReentrantLock();
private static final Condition condition1 = lock.newCondition();
private static final Condition condition2 = lock.newCondition();
private static boolean isConditionMet1 = false;
private static boolean isConditionMet2 = false;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 等待条件满足
while (!(isConditionMet1 && isConditionMet2)) {
condition1.await();
System.out.println("条件一已满足");
condition2.await();
System.out.println("条件二已满足");
}
// 执行条件满足后的操作
System.out.println("condition is met!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 改变变量
isConditionMet1 = true;
// 通知等待的线程条件已满足
condition1.signal();
System.out.println("通知等待的线程条件已满足");
} finally {
// 解锁
lock.unlock();
}
});
Thread thread3 = new Thread(() -> {
// 上锁
lock.lock();
try {
// 改变变量
isConditionMet2 = true;
// 通知等待的线程条件已满足
condition2.signal();
System.out.println("通知等待的线程条件已满足");
} finally {
// 解锁
lock.unlock();
}
});
thread1.start();
TimeUnit.SECONDS.sleep(1);
thread2.start();
thread3.start();
}
}
乐观锁和悲观锁都是并发控制的机制
, 用于在多个事务同时访问共享资源时, 保护数据的一致性
悲观锁: 总是设想最坏的情况, 每次拿数据的时候都认为别人会修改, 所以在拿数据之前会上锁, 这样别人如果想来操作这个数据, 就需要等我先释放了锁
悲观锁的基本思想是: 在操作共享资源前, 先获取锁, 确保其他事务无法同时修改此资源
, 从而避免数据冲突和不一致的问题
悲观锁的实现有哪些?
悲观锁: 总是乐观, 每次拿数据的时候都认为别人不会修改, 所以不会上锁, 但是在更新的时候会判断一下在此期间有没有人更新过这个数据
乐观锁的基本思想是: 假设事务冲突很少发生, 因为在操作前不会对数据上锁, 而是在提交事务时, 校验数据是否发生了冲突, 如果发生了冲突, 系统会拒绝提交当前事务, 并通知用户解决冲突
乐观锁的实现有哪些?
版本号
或时间戳
字段, 每个更新数据时, 版本号递增或者时间戳更新, 在提交事务时, 检查版本号或时间戳, 如果不一致, 说明数据已经被其他事务修改过乐观锁 | 悲观锁 | |
---|---|---|
基本思想 | 假设冲突较少, 不立即加锁, 而是在提交事务时检查是否发生冲突 | 假设会发生冲突, 因此在访问共享资源之前会先获取锁, 确保资源独占 |
加锁时机 | 在事务提交时才会检查冲突, 事务执行过程中不进行加锁 | 假设会发生冲突, 因此在访问共享资源之前会先获取锁, 确保资源独占 |
性能影响 | 可以提高并发性能,因为事务执行时不加锁,只在提交时检查冲突,减少了锁竞争 | 可能导致性能下降,因为在事务执行期间资源被锁定,其他事务需要等待= |
冲突处理 | 冲突发生时需要回滚事务,重新尝试或通知用户处理冲突 | 冲突发生时直接阻塞或等待其他事务释放锁 |
选择标准:
CAS即Compare And Swap的缩写, 即比较并交换
CAS操作包含三个参数
期望值
期望值
赋值, 或者其他复杂的操作, 得到新值
期望值
与当前内存地址的实际值
是否相等, 如果相等, 说明在读取和执行操作的过程中, 没有其他线程修改了这个值新值
写回到内存位置, 如果在这个过程中,其他线程修改了内存位置的值,CAS会失败。重新尝试
整个CAS过程, 直到CAS操作成功, 或达到某个预定的重试次数AQS(AbstractQueuedSynchronizer)是一个用于构建各种同步器的基础框架,提供了一些基本的同步操作的模板方法。AQS 的具体实现方式取决于具体的同步器需求,以下是一些常见的基于AQS实现的同步器:
这些同步器都使用了AQS提供的模板方法,如acquire
和release
,并通过扩展 AbstractQueuedSynchronizer类
来实现特定的同步逻辑。通过 AQS,开发者可以相对容易地构建出各种灵活、可扩展的同步工具。实际上,除了上述提到的同步器之外,还可以根据具体需求实现自定义的同步器。
AQS的工作原理基于等待队列(waiting queue)
和整数状态(state)
当线程请求获取共享资源时, AQS会根据状态判断是否可以获取访问权限, 如果不能就将线程加入等待队列中, 一旦资源可用或者满足特定条件, AQS会通过状态的改变唤醒等待队列中的线程来实现多线程协助和同步
AQS支持独享锁
和共享锁
两种不同类型的同步机制, 两者的区别在于它们允许同时被多个线程持有的方式
独占锁 | 共享锁 | |
---|---|---|
特点 | 独享锁是一种排它锁,即一次只能有一个线程持有该锁。当一个线程持有独享锁时,其他线程无法同时获取相同的锁,必须等待当前持有锁的线程释放 | 共享锁是一种允许多个线程同时持有的锁。当一个线程持有共享锁时,其他线程仍然可以获取相同的锁,只要它们也是请求获取共享锁的。但是,当一个线程持有共享锁时,其他线程无法获取独享锁。 |
示例 | ReentrantLock 是 AQS 的独享锁的实现。它允许同一个线程多次获取锁,即支持重入,但要求每一次获取锁都必须对应一次释放锁 | ReentrantReadWriteLock 是 AQS 的共享锁的实现。它区分了读锁和写锁,允许多个线程同时持有读锁,但在写锁被持有时,其他线程无法获取读锁或写锁。 |
ReentrantReadWriteLock也被称为读写锁, 它的内部维护了一对相关的锁, 一个用于只读操作, 称为读锁
; 一个用于写入操作, 称为写锁
读写锁: 顾名思义, 包含两种锁, 一个是读锁
、一个是写锁
. 其中读锁是共享锁、写锁是排它锁. 读锁允许多个线程同时获取锁进行读操作, 而写锁在同一时间只允许一个线程获取锁, 进行写操作
读锁(共享锁)
写锁(独占锁)
public static void main(String[] args) {
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Thread read1 = new Thread(() -> {
try {
rwl.readLock().lock();
System.out.println("读操作执行 时间:" + System.currentTimeMillis());
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
rwl.readLock().unlock();
}
});
Thread read2 = new Thread(() -> {
try {
rwl.readLock().lock();
System.out.println("读操作执行 时间:" + System.currentTimeMillis());
} finally {
rwl.readLock().unlock();
}
});
Thread write = new Thread(() -> {
try {
rwl.writeLock().lock();
System.out.println("写操作执行 时间:" + System.currentTimeMillis());
} finally {
rwl.writeLock().unlock();
}
});
read1.start();
// 休眠一会儿
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
read2.start();
write.start();
}
线程进入读锁的前提条件
线程进入写锁的前提条件
只要没有Writer线程,读锁可以由多个Reader线程同时持有。也就是说,写锁是独占的,读锁是共享的
有这么一些场景
1.优点如下
线程池通过提供一种有效的线程管理
和调度机制
,帮助提高应用程序的性能和可维护性,尤其在处理大量并发任务时,线程池是一种强大而有效的工具。
2.缺点如下
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
当我们核心线程数已经到达最大值、阻塞队列也已经放满了所有的任务、而且我们工作线程个数已经达到最大线程数, 此时如果还有新任务, 就只能走拒绝策略了
作用有以下两点
在线程池中, BlockingQueue主要通过以下两个参数进行配置:
corePoolSize
和maximumPoolSize
这个两个参数来指定线程池的基本大小和最大大小
通过选择不同的BlockingQueue实现,可以实现不同的任务调度策略。
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个LinkedBlockingQueue作为任务队列
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(10);
// 创建一个ThreadPoolExecutor,使用LinkedBlockingQueue作为任务队列
ExecutorService executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
1, // keepAliveTime
TimeUnit.SECONDS,
queue);
// 提交任务到线程池
for (int i = 0; i < 15; i++) {
executor.submit(() -> {
try {
Thread.sleep(1000);
System.out.println("Task completed by: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}
打印结果:
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-3
Task completed by: pool-1-thread-2
Task completed by: pool-1-thread-4
Task completed by: pool-1-thread-5
Task completed by: pool-1-thread-1
Task completed by: pool-1-thread-3
在这个例子中,LinkedBlockingQueue作为任务队列,可以存储最多 10 个等待执行的任务。线程池的核心线程数为5,最大线程数为10,因此在任务队列未满时,新任务将放入队列等待。如果队列已满,新任务将创建新线程执行,但不会超过最大线程数。
因为开启了15个线程, 而核心线程数+阻塞队列容量正好为15个, 所以不会创建新的线程
1.ArrayBlockingQueue
基于数组实现的有界队列
。固定容量,一旦创建就不能更改。需要指定容量,适用于任务数量固定的情况。
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
使用有界任务队列, 若有新的任务需要执行时, 线程池会创建新的线程, 直到创建的线程数量达到corePoolSize时, 则会将新的任务加入到等待队列中. 若等待队列已满, 即超过 ArrayBlockingQueue初始化的容量, 则继续创建线程, 直到线程数量达到maximumPoolSize设置的最大线程数量, 若大于 maximumPoolSize, 则执行拒绝策略.
在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态, 线程数将一直维持在 corePoolSize 以下, 反之当任务队列已满时, 则会以maximumPoolSize为最大线程数上限
2.LinkedBlockingQueue
基于链表实现的有界或无界队列
。可以选择是否指定容量,如果不指定容量则默认是 Integer.MAX_VALUE,适用于任务数量不固定的情况
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize 后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
3.SynchronousQueue
一个不存储元素的队列
, 每个插入操作必须等待另一个线程的对应移除操作, 反之亦然.
主要用于直接传递任务的场景, 一个线程产生任务, 另一个线程消费任务
SynchronousQueue常用于以下场景:
4.PriorityBlockingQueue
支持优先级的无界队列。它可以确保按照元素的优先级顺序进行处理,优先级较高的元素会被优先处理。
5.DelayedWorkQueue
一个支持延时获取元素的无界队列,用于实现定时任务。元素需要实现Delayed接口
通常用于ScheduledThreadPoolExecutor中。
ThreadFactory是一个接口, 用于创建新线程的工厂, 它允许你自定义线程的创建过程, 例如设置线程的名称、优先级、守护状态等…
import java.util.concurrent.*;
public class CustomThreadFactoryExample {
public static void main(String[] args) {
// 创建一个自定义的ThreadFactory
ThreadFactory customThreadFactory = new CustomThreadFactory("CustomThread");
// 使用自定义的ThreadFactory创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5, customThreadFactory);
// 提交一些任务
for (int i = 0; i < 10; i++) {
executorService.submit(() -> System.out.println(Thread.currentThread().getName()));
}
// 关闭线程池
executorService.shutdown();
}
}
// 自定义的ThreadFactory实现
class CustomThreadFactory implements ThreadFactory {
private final String threadNamePrefix;
public CustomThreadFactory(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
@Override
public Thread newThread(Runnable r) {
// 创建新线程并设置线程名称
Thread thread = new Thread(r, threadNamePrefix + "-" + System.nanoTime());
// 设置为后台线程(可选)
thread.setDaemon(false);
// 设置线程优先级(可选)
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}
1.AbortPolicy
这是默认的拒绝策略,当队列满时直接抛出RejectedExecutionException异常,阻止系统继续运行。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
2.CallerRunsPolicy
新任务会被直接在提交任务的线程中运行。这样做可以避免任务被拒绝,但会影响任务提交的线程的性能。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
3.DiscardPolicy
新任务被直接丢弃,不做任何处理。
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
4.DiscardOldestPolicy
尝试将最旧的未处理任务从队列中删除,然后重新尝试执行任务
ExecutorService executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
new LinkedBlockingQueue<>(capacity),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
1.execute
用于提交普通的可运行任务(Runnable)
, 没有办法获取任务执行的结果和异常
public void execute(Runnable command)
异常处理: execute方法提交任务后,异常会被线程池中的线程捕获并处理,但是这个异常处理是在线程内部进行的,不会传递到主线程中。
public static void main(String[] args) {
try {
System.out.println("主线程开始");
// 创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 执行线程方法
executor.execute(()->{
System.out.println("子线程运行开始");
int i = 1 / 0;
System.out.println("子线程运行结束");
});
executor.shutdown();
System.out.println("主线程结束");
} catch (Exception e) {
System.out.println("异常信息:" + e.getMessage());
}
}
在上面的代码中, 异常无法被捕获的原因是因为异常发生在子线程中,而主线程并不直接捕获这个异常。
execute方法提交任务后,异常会被线程池中的线程捕获并处理,但是这个异常处理是在线程内部进行的,不会传递到主线程中。
2.submit
提交可调度任务(Callable)
, 返回一个Future对象, 通过这个对象可以判断任务的执行状态和获取执行结果
public Future<?> submit(Runnable task)
public <T> Future<T> submit(Callable<T> task)
public <T> Future<T> submit(Runnable task, T result)
异常处理: submit方法可以通过Future对象的get方法来获取任务执行过程中抛出的异常。如果任务执行过程中发生异常,get方法会抛出ExecutionException。
try {
System.out.println("主线程开始");
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
System.out.println("运行开始");
int i = 1 / 0;
System.out.println("运行结束");
});
executor.shutdown();
// 获取任务执行的结果,这里会阻塞直到任务完成
future.get();
System.out.println("主线程结束");
} catch (Exception e) {
System.out.println("捕获到异常:" + e.getMessage());
}
submit适用于提交既可以是Runnable也可以是Callable 的任务,可以获取任务执行结果,更灵活。
3.schedule
ScheduledThreadPoolExecutor类的方法, 用于提交定时任务, 它支持在将来的某个时间点执行任务, 以及以一定时间间隔执行重复任务
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
4.invokeAll
提交一组可调用任务,返回一个包含所有Future对象的列表。在所有任务都完成后,调用线程将得到一个包含各个任务执行结果的列表。
注意: 在所有任务完成之前, invokeAll方法会一直阻塞
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue);
List<Callable<String>> tasks = Arrays.asList(new MyCallable(), new AnotherCallable());
List<Future<String>> futures = executor.invokeAll(tasks);
5.invokeAny
提交一组可调用任务,并返回其中一个成功执行的任务的结果。如果其中一个任务成功执行,其他任务将被取消。
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue);
List<Callable<String>> tasks = Arrays.asList(new MyCallable(), new AnotherCallable());
String result = executor.invokeAny(tasks);
submit和execute都可以提交任务, 两者有一些关键的区别
Callable的返回值类型是在实现接口时指定的, 例如下面这个例子
public class GetStrService implements Callable<String> {
private int i;
public GetStrService(int i) {
this.i = i;
}
@Override
public String call() throws Exception {
int t=(int) (Math.random()*(10-1)+1);
System.out.println("第"+i+"个任务开始啦:"+Thread.currentThread().getName()+"准备延时"+t+"秒");
Thread.sleep(t*1000);
return "第"+i+"个GetStrService任务使用的线程:"+Thread.currentThread().getName();
}
}
自定义ThreadPoolExecutor作为线程池
// 自定义线程池
class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
System.out.println("捕获到异常。异常信息为:" + t.getMessage());
System.out.println("异常栈信息为:");
t.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("主线程开始");
// 创建线程池
ExecutorService executor = new MyThreadPoolExecutor(5,50, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(20));
// 执行线程方法
executor.execute(()->{
System.out.println("子线程运行开始");
int i = 1 / 0;
System.out.println("子线程运行结束");
});
executor.shutdown();
System.out.println("主线程结束");
}
FixedThreadPool | SingleThreadExecutor | ScheduledThreadPool | CachedThreadPool | |
---|---|---|---|---|
名称 | 固定大小线程池 | 单线程线程池 | 定时任务线程池 | 缓存线程池 |
特点 | 固定线程数量的线程池,适用于负载较重的服务器 | 只有一个工作线程的线程池,确保所有任务按顺序执行 | 支持定时及周期性任务执行的线程池 | 线程数量根据需求动态调整,线程空闲一定时间后被回收 |
1.FixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
public class NewFixedThreadPoolTest {
public static void main(String[] args) {
System.out.println("主线程启动");
// 1.创建1个有2个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
}
};
// 2.线程池执行任务(添加4个任务,每次执行2个任务,得执行两次)
threadPool.submit(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
System.out.println("主线程结束");
}
}
上述代码:创建了一个有2个线程的线程池,但一次给它分配了4个任务,每次只能执行2个任务,所以,得执行两次。
该线程池重用固定数量的线程在共享的无界队列中运行。 在任何时候,最多 nThreads 线程将是活动的处理任务。如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待,直到有线程可用。 所以,它会一次执行 2 个任务(2 个活跃的线程),另外 2 个任务在工作队列中等待着。
submit() 方法和 execute() 方法都是执行任务的方法。它们的区别是:submit() 方法有返回值,而 execute() 方法没有返回值。
2.CachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
适用场景:快速处理大量耗时较短的任务,如 Netty 的 NIO 接受请求时,可使用 CachedThreadPool。
public class NewCachedThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
3.SingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序。
4.SingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池。
public class SingleThreadScheduledExecutorTest {
public static void main(String[] args) {
ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
System.out.println("添加任务,时间:" + new Date());
threadPool.schedule(() -> {
System.out.println("任务被执行,时间:" + new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
}, 2, TimeUnit.SECONDS);
}
}
关闭线程池的正确方式是调用其shutdown方法。这个方法会平滑地关闭线程池,不再接受新的任务,但会让已经在队列中的任务执行完毕。随后,线程池会进入TERMINATED状态。