线程和异步
前言
用gpt的话解释:
在Java中,线程和异步是两个相关的概念,但它们有不同的含义和用途。下面我将尝试解释这两个概念:
### 线程(Thread)
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。在Java中,`Thread`类是线程控制的基石,它提供了线程的启动、终止、挂起、恢复等方法。
- 线程的创建和运行:在Java中,你可以通过继承`Thread`类或者实现`Runnable`接口来创建一个线程。然后,你可以调用线程的`start()`方法来启动它,这会导致Java虚拟机启动一个新的线程,并执行`run()`方法中的代码。
- 线程的生命周期:线程有几种状态,包括新建、就绪、运行、阻塞、等待、超时等待和终止。
- 线程同步:由于线程可能会并发访问共享资源,因此需要使用同步机制(如`synchronized`关键字、`ReentrantLock`等)来避免竞态条件和不一致的状态。
- 线程通信:线程之间可以通过`wait()`、`notify()`和`notifyAll()`方法进行通信,这些方法用于协调多个线程的工作。
### 异步(Asynchronous)
异步是一种编程模型,它允许某些操作在其他操作执行时继续进行,而不需要等待这些操作完成。在Java中,异步编程通常涉及到`Future`、`CompletableFuture`、`Callable`等类,以及使用`ExecutorService`来管理线程池。
- 异步执行:当你启动一个异步操作时,你通常会得到一个`Future`对象,它表示一个可能还没有完成的操作的结果。你可以使用`Future`对象的`get()`方法来等待操作完成并获取结果,或者使用`isDone()`来检查操作是否完成。
- 回调函数:在异步编程中,回调函数是一种常见的模式,它允许你在操作完成时执行一些代码。在Java中,`CompletableFuture`类提供了丰富的API来支持回调函数和链式调用。
- 非阻塞:异步编程的一个主要优点是它允许程序在等待I/O操作或长时间运行的任务完成时继续执行其他工作,从而提高程序的响应性和效率。
### 线程与异步的关系
线程是实现异步编程的一种方式,但它们并不完全相同。线程是操作系统层面的概念,而异步是一种编程模式。在异步编程中,你通常会在一个线程上发起一个操作,然后该操作可能在另一个线程上完成,这样发起操作的线程就可以继续执行其他任务。
在实际应用中,线程和异步往往是结合使用的。例如,你可能会使用`ExecutorService`来管理一个线程池,并使用`CompletableFuture`来执行异步任务,这样可以在少量的线程上高效地处理多个并发任务。
总结来说,线程是并发执行的单元,而异步是一种编程模式,它允许程序在等待某些操作完成时继续执行其他任务。理解这两个概念有助于编写高效、响应性好的Java程序。
下面是一个例子来说明异步和多线程的区别:
假设有一个程序需要从网络上下载一份文件。如果使用同步方式,程序需要等待下载完成后才能继续执行其他操作。如果使用异步方式,程序可以在下载过程中继续执行其他操作,等到下载完成后再处理下载的结果。如果使用多线程方式,程序可以同时启动多个线程来下载文件,从而提高下载速度和并发性。
大白话解释:
多线程就是实现异步的一个方式。异步更注重结果,多线程更注重处理过程,算是两个层级的概念。参考文章:[传送门]
正文
了解了两种功能的区别概念后,这里就分别进行介绍和使用以及一些注意事项,也还算比较全面
多线程
一: 几种常见的实现方式
1. 继承Thread类:创建一个继承自Thread类的子类,并重写其run()方法。然后创建该子类的对象,并调用start()方法启动线程
2. 实现Runnable接口:创建一个实现了Runnable接口的类,并实现其run()方法。然后创建该类的对象,并将其作为参数传递给Thread类的构造方法,最后调用start()方法启动线程
3. 使用Callable和Future:创建一个实现了Callable接口的类,并实现其call()方法。然后创建一个ExecutorService线程池,调用submit()方法提交Callable任务,并返回一个Future对象,通过调用Future对象的get()方法可以获取任务的返回结果
4. 使用线程池:创建一个ExecutorService线程池,通过调用execute()方法提交Runnable任务,或者通过调用submit()方法提交Callable任务。线程池会自动管理和调度线程的执行
这是使用类继承实现,而且这些实现的接口类都只有一个方法,所以直接方法中使用lambda表达式就可以了,同时,也建议配合线程池一起,提高效率,下面是几个参考案例:
@RequestMapping("thread")
void demo2() throws ExecutionException, InterruptedException {
//第一种
new Thread(() -> {
System.out.println("第一个程序");
}).start();
//第二种
final ExecutorService executorService = Executors.newFixedThreadPool(10);
final Future<?> submit = executorService.submit(new Runnable() {
@Override
public void run() {
System.err.println("hello");
}
},"结束123");//第二个参数可以不填,就不需要自定义返回结果
//返回结果
final Object o = submit.get();//输出结束123
executorService.shutdown();
//第三种
final ExecutorService executorService2 = Executors.newFixedThreadPool(10);
final Future<?> submit2 = executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
return "开始";
}
});
//返回结果
final Object o2 = submit2.get();//输出开始
executorService.shutdown();
}
二 : 线程产生的问题
说到多线程,那就不得不提到线程安全的问题了,Java中的锁机制主要用于确保多线程环境下的数据一致性和线程安全。Java提供了多种锁机制,包括内置锁(synchronized)、显式锁(ReentrantLock)以及读写锁(ReadWriteLock)等
在介绍这些锁之前, 需要先了解几个关于锁的概念(注意, 这里不是专门指的某一种锁, 而是锁具备的特性) :
- 可重入和不可重入[参考文章]
可重入锁:
允许同一个线程多次获取同一个锁。当一个线程获取锁后,可以再次获取该锁的状态,而不会被阻塞。原理是,每次获取锁后,计数器会递增+1,每次释放锁后,计数器递减-1。只有所有的锁获取都被释放了,其他线程才能获取该锁。java.util.concurrent.locks.ReentrantLock类实现了可重入锁。可重入锁提供了比内置锁(synchronized关键字)更加灵活的锁机制,可以实现更复杂的同步需求。可重入锁还提供了公平性和非公平性的选择,以及可中断的锁获取、定时锁获取等功能
不可重入锁:
不允许同一个线程多次获取同一个锁。当一个线程已经持有该锁时,如果尝试再次获取该锁,会被阻塞,直到该锁被释放。不可重入锁不维护计数器,因此每次获取锁和释放锁都是一对一的操作
通俗来讲:
可重入锁就是一张通一卡通,只需一张卡就可以通过所有相同关卡
不可重入锁就是:即使每个关卡相同,你也得再拿一个一摸一样的卡来
- 自旋锁
自旋锁(Spin Lock):
是一种特殊的锁机制,它在尝试获取锁时不会立即阻塞线程,而是通过循环不断尝试获取锁,直到成功获取锁或者达到最大尝试次数
自旋锁的目的是为了避免线程切换的开销,因为线程切换需要保存和恢复线程的上下文,开销较大。在多核处理器上,当一个线程在获取锁时,如果发现锁已经被其他线程持有,它会不断尝试获取锁,而不是立即被阻塞,等待其他线程释放锁。这种循环尝试的过程称为自旋
自旋锁适用于锁竞争不激烈、临界区执行时间短的情况。如果锁竞争激烈,多个线程频繁地竞争同一个锁,自旋锁的效果可能不如其他的锁机制
需要注意的是,自旋锁需要占用CPU资源进行循环尝试,如果自旋的时间过长,会导致CPU资源的浪费。因此,在使用自旋锁时需要根据实际情况进行调优,设置合适的自旋次数或者自旋时间
在Java中,自旋锁可以使用synchronized关键字或者java.util.concurrent包中的Lock接口的实现类来实现。**在使用synchronized关键字时,JVM会自动进行自旋锁的优化,尝试获取锁时会进行短暂的自旋,如果获取不到锁则会转为阻塞状态**
ps:我看到网上文章有很多说不可重入锁就等同于自旋锁,个人觉得对这两个还是有点差入的,当然,自己没有研究过源码,大部分也是网上获取到的信息,这个就自己权衡了。
自旋锁是一种基于忙等待的锁机制。当一个线程尝试获取自旋锁时,如果锁被其他线程持有,则该线程会一直循环尝试获取锁,而不是阻塞等待。自旋锁主要是可以避免线程上下文切换的开销,从而提高性能
不可重入锁是一种锁机制,在同一个线程中多次获取同一个锁会导致线程阻塞。在线程获取不可重入锁之后,如果尝试再次获取该锁,就会被阻塞,直到之前获取的锁被释放。不可重入锁主要用于特定场景,需要确保同一个线程在某个临界区内只能执行一次
总结:自旋锁主要是为了减少上下文切换的开销,而不可重入锁主要是为了控制同一个线程对于某个锁的多次获取行为
- 公平锁, 非公平锁
公平锁:
是指多个线程按照请求的顺序依次获取锁。当多个线程等待同一个锁时,公平锁会根据线程的请求顺序进行排队,然后依次将锁分配给等待的线程。这种方式确保了锁的获取是按照先来先得的顺序进行的,公平锁遵循了"先到先得"的原则。公平锁能够避免饥饿情况的发生,即一个线程一直无法获取锁的情况
非公平锁:
是指多个线程在争夺同一个锁时,允许新申请的线程插队抢占锁。在非公平锁的情况下,当锁释放时,新申请的线程有可能在竞争之前就获得锁,而排在等待队列中的线程需要等待更长的时间才能获取到锁。非公平锁的优势在于可以减少竞争,提高整体的吞吐量
Java中的ReentrantLock默认使用的是非公平锁,而公平锁可以通过ReentrantLock的构造函数显式设置为公平锁。在某些场景下,公平锁会导致性能下降,因为需要维护等待队列的顺序和更频繁的上下文切换。非公平锁因为允许插队,可以更高效地保持较高的吞吐量
需要注意的是,即使使用公平锁,也不能保证绝对的公平性。由于线程调度和竞争的不确定性,实际上某些线程可能会插队或者发生优先级反转的情况
- 偏向锁
偏向锁:
Java中的一种优化技术,用于提高多线程并发执行时的性能。它是一种轻量级的同步机制,用于减少线程竞争和同步开销
当一个线程访问一个同步代码块时,偏向锁会将对象头中的标记设置为该线程的Thread ID,表示该线程获取了偏向锁。之后,当同一个线程再次进入同步代码块时,无需重新竞争锁,而是直接进入
偏向锁的优势在于减少了多线程竞争的情况,提高了单线程执行同步代码块的性能。但是,如果多个线程竞争同一个锁,偏向锁就会升级为轻量级锁或重量级锁,增加了竞争和同步的开销
需要注意的是,偏向锁在对象创建时是不会立即启用的,而是需要经过一定的延迟才会启用。这是为了避免在对象刚创建时就启用偏向锁,从而浪费了额外的资源
偏向锁适用于以下情况:
大部分情况下,锁只会被一个线程持有,即存在无竞争的情况
锁的竞争情况很少发生,即多个线程很少同时竞争同一个锁
总的来说,偏向锁是Java中用于优化同步操作的一种机制,可以提高单线程执行同步代码块的性能,但在多线程竞争同一个锁时会升级为其他类型的锁
- 轻量级锁(CAS),乐观锁,悲观锁
CAS(Compare and Swap):
是一种并发编程的技术和算法,用于实现多线程环境下的无锁同步操作
CAS操作基于对比和交换的原理。它包含三个参数:内存地址(或变量)、旧的预期值和新的更新值。CAS操作会先比较内存地址中的值与旧的预期值是否相等,如果相等,则将内存地址中的值更新为新的更新值,否则不做任何操作,对共享变量的操作是线程安全的,避免了使用锁的开销
在Java中,java.util.concurrent.atomic包提供了一系列的原子操作类,如AtomicInteger、AtomicLong等,可以使用CAS算法来实现对这些变量的原子操作
CAS操作是一种乐观锁的机制,相比于传统的基于锁的同步机制,它具有更低的开销和更好的可伸缩性。但需要注意的是,CAS操作可能存在ABA问题,即在CAS操作过程中,旧的预期值可能被修改为其他值,然后再修改回原来的预期值。为了解决ABA问题,可以使用版本号、时间戳等方式来实现更加安全的CAS操作
总之,CAS是一种无锁同步的机制,在Java中通过java.util.concurrent.atomic包的原子操作类来实现,可以保证变量的原子性操作和线程安全性
轻量级锁(Lightweight Lock):
是一种用于提高多线程并发性能的锁机制,主要是Java虚拟机(JVM)中对于synchronized关键字的一种优化实现。
为了减少线程阻塞和唤醒的开销,JVM引入了轻量级锁的概念。当一个线程访问一个同步块时,JVM会尝试将对象头中的标记位设置为“轻量级锁”(Lightweight Lock),表示该对象已经被当前线程锁定。如果其他线程也尝试获取该对象的锁,JVM会使用CAS(Compare and Swap)操作来判断是否能够获取锁,而不是直接阻塞线程。如果CAS操作成功,表示获取到了锁,可以继续执行同步块;如果CAS操作失败,表示其他线程已经获取到了锁,当前线程会进入自旋等待状态,不会被阻塞
轻量级锁适用于短时间内只有一个线程访问同步块的情况,避免了线程阻塞和唤醒的开销,提高了并发性能。但是如果同步块的竞争激烈,多个线程频繁地竞争同一个锁,轻量级锁的效果可能不如其他的锁机制
悲观锁:
悲观锁的思想是默认认为会有并发冲突发生,因此在访问共享资源之前会先获取锁,并且如果获取锁失败,线程会被阻塞等待锁的释放,介绍的这几种锁都是悲观锁,只不过各自有各自的其他优点与优化
> 下面就开始介绍锁的使用了
1. 内置锁/隐式锁(synchronized)
内置锁是Java最基本的同步机制,Synchronized是依赖于JVM实现, 它使用synchronized关键字实现。synchronized可以修饰方法或者作为代码块的一部分。当一个线程获得一个对象的内置锁时,其他线程无法访问该对象的所有synchronized方法或代码块,直到锁被释放
当一个线程获取到synchronized锁时,它将持有这个锁,并开始执行相关的方法或代码块。在执行完之后,锁会被自动释放,其他线程可以继续获取锁并执行。当线程在synchronized代码块中抛出异常时,锁也会被释放
在Java 6及以前的版本中,synchronized关键字在内部实现中会使用重量级锁(即操作系统线程的同步原语)。然而,从Java 6开始,synchronized关键字引入了锁的升级机制,即偏向锁、轻量级锁和重量级锁结合的优化策略。具体锁的选择和升级过程是由JVM动态决定的,开发者无需过多关注,现在对性能而言,synchronized已经不比其他锁弱了,只是区别于对业务功能的需求
①. 对象级别的锁:synchronized可以用于实例方法或代码块中,它提供了对象级别的锁,也称为内部锁或监视器锁。当一个线程进入synchronized修饰的方法或代码块时,它会尝试获取该对象的锁。如果锁已被其他线程持有,则当前线程将被阻塞,直到锁被释放
示例:
public synchronized void synchronizedMethod() {
// 该方法为实例方法,使用synchronized修饰
// 同一时间只能有一个线程能够执行该方法
// ...
}
public void synchronizedBlock() {
synchronized(this) {
// 代码块使用synchronized(this)锁定当前实例对象
// 只有一个线程能够执行这段代码块
// ...
}
}
②. 类级别的锁:除了对象级别的锁,synchronized还可以应用于静态方法或代码块,提供类级别的锁。当一个线程进入synchronized修饰的静态方法或代码块时,它会尝试获取该类的锁(类的Class对象)。其他线程必须等待锁被释放才能执行相应的静态方法或代码块
示例:
public static synchronized void synchronizedStaticMethod() {
// 该方法为静态方法,使用synchronized修饰
// 同一时间只能有一个线程能够执行该静态方法
// ...
}
public void synchronizedStaticBlock() {
synchronized(MyClass.class) {
// 代码块使用synchronized(MyClass.class)锁定当前类的Class对象
// 只有一个线程能够执行这段代码块
// ...
}
}
③. 锁的获取和释放:当一个线程获取到synchronized锁时,它将持有这个锁,并开始执行相关的方法或代码块。在执行完之后,锁会被自动释放,其他线程可以继续获取锁并执行。当线程在synchronized代码块中抛出异常时,锁也会被释放
④. 锁的可重入性:synchronized支持锁的可重入性,也就是说同一个线程在持有锁的情况下,可以再次进入被同一个锁保护的方法或代码块,而不会发生死锁
⑤. 内存的可见性:synchronized不仅提供了互斥的功能,还保证了内存的可见性。当一个线程在释放锁之前,所有对共享变量的修改都将立即对其他线程可见,保证了数据的一致性
2. 显式锁(ReentrantLock)
①. 可重入性:与synchronized一样,ReentrantLock支持锁的可重入性。一个线程可以多次获取同一个ReentrantLock锁,而不会发生死锁
②. 公平性:ReentrantLock可以选择公平锁或非公平锁。公平锁会按照线程的请求顺序来获取锁,而非公平锁则没有这样的保证。可以使用ReentrantLock(true)设置为公平锁,默认情况下,ReentrantLock是非公平锁
③. 锁的获取和释放:使用ReentrantLock,可以通过lock()方法来获取锁,在使用完锁后,需要通过unlock()方法手动释放锁。通常情况下,应该将unlock()放在finally块中,以确保锁的释放
示例:
// 创建一个非公平的ReentrantLock
ReentrantLock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
// 临界区,访问共享资源
// ...
} finally {
// 释放锁
lock.unlock();
}
④. 条件变量:ReentrantLock提供了Condition接口来支持条件变量。一个ReentrantLock对象可以有多个关联的Condition对象,用于线程间的等待和通知。线程可以通过await()方法进入等待状态,而其他线程可以通过signal()方法来唤醒等待的线程
示例:
// 创建一个ReentrantLock和对应的Condition对象
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待条件满足
lock.lock();
try {
while (!条件) {
condition.await();
}
// 条件满足后的处理
// ...
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// 唤醒等待的线程
lock.lock();
try {
// 修改条件
condition.signalAll();
} finally {
lock.unlock();
}
⑤. 中断响应:ReentrantLock提供了对中断的响应机制。当一个线程在等待锁的过程中被中断时,它可以选择如何响应中断。可以通过lockInterruptibly()方法来获取锁,如果线程在等待锁时被中断了,它将被立即唤醒并抛出InterruptedException异常。
示例:
ReentrantLock lock = new ReentrantLock();
public void doSomething() {
try {
lock.lockInterruptibly(); // 尝试获取锁,响应中断
// 临界区,访问共享资源
// ...
} catch (InterruptedException e) {
// 当前线程在等待锁的过程中被中断
// 处理中断的逻辑
// ...
} finally {
lock.unlock(); // 释放锁
}
}
需要注意的是,相对于synchronized关键字,ReentrantLock在使用上更加灵活,提供了更多功能,例如公平性、条件变量和中断响应。但它也复杂一些,需要手动管理锁的获取和释放,因此需要注意避免死锁和使用try-finally块来确保锁的释放。在选择使用ReentrantLock时,应根据具体场景和需求来进行评估和选择。
总结来说,ReentrantLock是Java中提供的可重入锁实现,它提供了灵活的锁定机制和丰富的功能,例如可重入性、公平性、条件变
3. 读写锁(ReadWriteLock)
ReadWriteLock是一个接口,它的实现类ReentrantReadWriteLock提供了一种读写锁机制。读写锁允许多个线程同时读取共享资源,但在写入时只允许一个线程访问。这种锁机制在读操作远多于写操作的场景下可以提高性能。
使用ReadWriteLock的基本步骤如下:
- 创建一个ReentrantReadWriteLock实例。
- 使用readLock()方法获取读锁,或使用writeLock()方法获取写锁。
- 将需要同步的代码放在try块中。
- 在finally块中使用unlock()方法释放锁。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Example {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void readMethod() {
rwLock.readLock().lock();
try {
// 读取共享资源
} finally {
rwLock.readLock().unlock();
}
}
public void writeMethod() {
rwLock.writeLock().lock();
try {
// 修改共享资源
} finally {
rwLock.writeLock().unlock();
}
}
}
4. StampedLock
StampedLock是Java 8中新增的一个读写锁(ReadWriteLock)的改进版本,它引入了乐观读锁的概念,并提供了更高级别的锁优化。StampedLock相比于ReentrantReadWriteLock在读多写少的场景下,具有更好的性能。
StampedLock的主要特点如下:
1. 三种锁模式:StampedLock支持三种锁模式:读锁(共享锁),写锁(独占锁)和乐观读锁。读锁和写锁的获取方式与ReentrantReadWriteLock类似,而乐观读锁则是一种无锁的模式,乐观读锁不会阻塞其他线程对共享资源的访问。
2. 乐观读锁:乐观读锁是StampedLock的独有特性。在乐观读锁模式下,不会加任何锁,直接读取数据。然后,通过返回一个标记(Stamp)来判断读操作期间是否有写操作发生。如果数据没有被修改,读操作就可以继续。如果有写操作发生,则需要重新获取读锁或者使用其他方式处理。乐观读锁的好处是减少了锁的开销,提高了并发性能。
3. 锁降级:StampedLock支持锁的升级和降级。锁降级指的是将写锁降级为读锁,即在持有写锁的情况下获取读锁。StampedLock提供了tryConvertToReadLock()方法来实现锁的降级,这种方式能够减少线程切换的开销。
4. 无法重入:与ReentrantReadWriteLock不同,StampedLock是无法重入的。同一个线程获取写锁两次将会导致死锁。这是StampedLock的一个限制,需要在使用时特别注意。
使用StampedLock的示例代码如下:
import java.util.concurrent.locks.StampedLock;
public class StampedLockDemo {
private double x, y;
private final StampedLock lock = new StampedLock();
public void write(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp); // 释放写锁
}
}
public double read() {
long stamp = lock.tryOptimisticRead(); // 尝试获取乐观读锁
double currentX = x, currentY = y;
if (!lock.validate(stamp)) { // 判断期间是否有写操作
stamp = lock.readLock(); // 获取悲观读锁
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX currentX + currentY currentY);
}
}
以上示例中,write()方法获取写锁,更新共享变量x和y的值。read()方法首先尝试获取乐观读锁,如果没有写操作发生,则直接读取x和y的值。如果有写操作,则需要升级为悲观读锁,并重新读取x和y的值。
总体来说,StampedLock在某些特定的场景下可以比传统的读写锁提供更好的性能,尤其是读操作远远超过写操作的情况下。然而,使用StampedLock需要注意其无法重入以及有锁降级的特性,避免出现死锁等问题。
> 除了用锁保证业务流程数据的安全之外,也可以用一些java提供的具有线程安全属性的现有对象, 比如 ConcurrentHashMap , Atomic系列(AtomicInteger, AtomicInteger, AtomicBoolean, AtomicIntegerArray, AtomicReferenceArray) 等等
三 : 线程控制
线程模型是抢占式的多线程模型, 也就是说, 每个线程都有一定的时间片来执行自己的任务, 然后让出CPU的使用权给其他线程, 当你想控制有限资源的并发访问控制时, 可使用java提供的方法, 这也是锁的一种, 不过有更多的扩展功能
CountDownLatch
CountDownLatch是Java中的一个同步工具类,用于控制线程的执行顺序。它可以让一个或多个线程等待其他线程完成某个操作后再继续执行。
CountDownLatch内部维护了一个计数器,该计数器初始值为指定的数目。当一个线程完成了一部分操作后,可以调用CountDownLatch的countDown()方法将计数器减1。其他线程可以调用CountDownLatch的await()方法来等待计数器变为0,一旦计数器变为0,等待的线程就会被唤醒,可以继续执行。
使用CountDownLatch的典型场景是一个任务需要等待其他多个任务都完成后才能继续执行。例如,一个主线程需要等待多个子线程都完成某个操作后才能继续执行,可以使用CountDownLatch来实现。
下面是一个使用CountDownLatch的示例代码:
import java.util.concurrent.CountDownLatch;
public class Main {
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
// 线程执行一些操作
System.out.println("Thread " + Thread.currentThread().getId() + " is running");
// 操作完成后调用countDown()方法,将计数器减1
latch.countDown();
});
thread.start();
}
// 主线程调用await()方法等待计数器变为0
latch.await();
// 所有线程都完成操作后,主线程继续执行
System.out.println("All threads have completed");
}
}
在上面的示例中,主线程创建了5个子线程,并启动它们。每个子线程执行一些操作后调用countDown()方法,将计数器减1。主线程调用await()方法等待计数器变为0,当所有子线程都调用了countDown()方法后,计数器变为0,主线程被唤醒,继续执行后面的代码。
需要注意的是,CountDownLatch的计数器只能减少不能增加,一旦计数器变为0后,再调用countDown()方法不会有任何效果。因此,CountDownLatch通常用于一次性的任务,无法重复使用。如果需要重复使用计数器,可以考虑使用CyclicBarrier或Semaphore。
CyclicBarrier和Semaphore
CyclicBarrier和Semaphore都是Java并发包中的同步工具类,用于控制多个线程的执行顺序和并发访问共享资源。
1. CyclicBarrier:
CyclicBarrier是一个同步辅助类,它允许一组线程相互等待,直到达到一个共同的屏障点。CyclicBarrier内部维护了一个计数器和一个屏障点。当线程到达屏障点时,会调用await()方法等待其他线程,当所有线程都到达屏障点后,屏障点被打开,所有线程可以继续执行。CyclicBarrier的计数器可以重复使用,当计数器减到0时,可以重置计数器并再次使用。
下面是一个使用CyclicBarrier的示例代码:
import java.util.concurrent.CyclicBarrier;
public class Main {
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
// 所有线程都到达屏障点后执行的操作
System.out.println("All threads have reached the barrier");
});
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
// 线程执行一些操作
System.out.println("Thread " + Thread.currentThread().getId() + " is running");
try {
// 线程到达屏障点,调用await()方法等待其他线程
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
在上面的示例中,主线程创建了5个子线程,并启动它们。每个子线程执行一些操作后调用await()方法等待其他线程。当所有子线程都到达屏障点后,屏障点被打开,所有线程可以继续执行。
2. Semaphore:
Semaphore是一个计数信号量,用于控制同时访问某个资源的线程数。Semaphore内部维护了一个计数器和一组许可证。线程可以通过acquire()方法获取许可证,如果计数器大于0,则线程可以继续执行;如果计数器等于0,则线程会被阻塞,直到有其他线程释放许可证。线程可以通过release()方法释放许可证,将计数器加1。
下面是一个使用Semaphore的示例代码:
import java.util.concurrent.Semaphore;
public class Main {
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
try {
// 线程尝试获取许可证
semaphore.acquire();
// 线程获取到许可证后可以执行一些操作
System.out.println("Thread " + Thread.currentThread().getId() + " is running");
// 线程执行完成后释放许可证
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
在上面的示例中,主线程创建了5个子线程,并启动它们。每个子线程尝试获取许可证,如果有许可证可用,则线程可以继续执行;如果没有许可证可用,则线程会被阻塞,直到有其他线程释放许可证。线程执行完成后释放许可证,将计数器加1。通过Semaphore的控制,最多会有2个线程同时执行任务,其他线程会等待许可的释放
需要注意的是,Semaphore并不保证获取许可证的顺序,只保证同时获取许可证的线程数不超过指定的数目。如果需要保证获取许可证的顺序,可以考虑使用Lock和Condition。
Lock和Condition
这两个是Java并发包中的同步工具类,用于实现线程之间的协作和控制并发访问共享资源的顺序。
1. Lock:
Lock是一个接口,定义了锁的基本操作。与synchronized关键字相比,Lock提供了更灵活的锁定机制。Lock的常用实现类是ReentrantLock,它支持可重入的互斥锁。
使用Lock的基本步骤如下:
- 创建一个Lock对象:Lock lock = new ReentrantLock();
- 在需要加锁的代码块前调用lock()方法获取锁:lock.lock();
- 在代码块执行完成后调用unlock()方法释放锁:lock.unlock();
下面是一个使用Lock的示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private static int count = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
// 加锁
lock.lock();
try {
// 访问共享资源
count++;
System.out.println("Thread " + Thread.currentThread().getId() + " is running, count = " + count);
} finally {
// 释放锁
lock.unlock();
}
});
thread.start();
}
}
}
在上面的示例中,主线程创建了5个子线程,并启动它们。每个子线程在访问共享资源之前调用lock()方法获取锁,在访问完成后调用unlock()方法释放锁。
2. Condition:
Condition是与Lock相关联的条件对象,用于实现线程之间的协作。一个Lock对象可以有多个关联的Condition对象,每个Condition对象可以控制一组线程的执行顺序。
使用Condition的基本步骤如下:
- 创建一个Lock对象:Lock lock = new ReentrantLock();
- 使用Lock对象创建一个Condition对象:Condition condition = lock.newCondition();
- 在需要等待的代码块中调用await()方法等待条件满足:condition.await();
- 在条件满足时调用signal()或signalAll()方法唤醒等待的线程:condition.signal();
下面是一个使用Condition的示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private static int count = 0;
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
int threadCount = 5;
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
// 加锁
lock.lock();
try {
// 等待条件满足
while (count < 5) {
condition.await();
}
// 条件满足后执行操作
System.out.println("Thread " + Thread.currentThread().getId() + " is running");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
});
thread.start();
}
Thread.sleep(1000);
// 修改共享变量,满足条件
lock.lock();
try {
count = 5;
// 唤醒等待的线程
condition.signalAll();
} finally {
lock.unlock();
}
}
}
在上面的示例中,主线程创建了5个子线程,并启动它们。每个子线程在等待条件满足时调用await()方法等待,条件满足后被唤醒并执行操作。主线程在一定时间后修改共享变量,满足条件后调用signalAll()方法唤醒等待的线程
异步
1. @EnableAsync
在 Java 11 中,使用 @EnableAsync
注解启用异步方法的支持与之前的版本没有太大差别。`@EnableAsync` 注解是 Spring Framework 中的一个注解,用于启用异步方法的支持。
要使用 @EnableAsync
注解,需要遵循以下步骤:
①. 创建一个配置类,并在该类上添加 @EnableAsync
注解,以启用异步方法的支持。
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AppConfig {
// 配置其他相关的 Bean
}
在上述示例中,我们创建了一个名为 AppConfig
的配置类,并在类上添加了 @EnableAsync
注解,以启用异步方法的支持。你可以根据实际需求在该类中添加其他配置和 Bean 定义。
②. 在需要异步处理的方法上添加 @Async
注解,以将该方法标记为异步方法。
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Async
public void asyncMethod() {
// 异步方法的实现
}
}
在上述示例中,我们在 MyService
类的 asyncMethod()
方法上添加了 @Async
注解,将该方法标记为异步方法。注意,被 @Async
注解标记的方法将在一个单独的线程中执行。
③. 配置任务执行器(可选)
如果你希望使用自定义的任务执行器,可以创建一个 Executor
Bean,并在配置类中进行配置。可以通过实现 AsyncConfigurer
接口来自定义线程池等相关配置。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AppConfig implements AsyncConfigurer {
@Override
@Bean
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);//设置核心线程池大小为10,即线程池中始终保持的线程数量;
executor.setMaxPoolSize(20);//设置最大线程池大小为20,即线程池中能容纳的最大线程数量;
executor.setQueueCapacity(30);//设置任务队列的容量为30,当线程池中的线程数量达到核心线程池大小后,多余的任务会被放入任务队列,直到队列达到最大容量;
executor.initialize();
return executor;
}
}
在上述示例中,我们实现了 AsyncConfigurer
接口,并重写了 getAsyncExecutor()
方法来配置线程池的参数。这样就可以自定义任务执行器的设置
需要注意的是,在使用 @Async
注解时,要确保异步方法的调用是通过 Spring 容器中的代理来完成的,而不是通过类的内部调用。因为只有通过代理类的方法调用,Spring 才会生效并启动异步处理
2. CompletableFuture
CompletableFuture 是 Java 8 引入的一个类,可以用于实现异步操作。它提供了丰富的方法来处理异步任务的结果,包括处理完成时的操作、组合多个 CompletableFuture、处理异常等
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 异步执行的任务
return "result";
});
future.thenAccept(result -> {
// 异步任务完成时的处理
System.out.println("Result: " + result);
});
CompletableFuture 的其余功能如下:
1. 异步任务执行:CompletableFuture 可以通过 supplyAsync()
或 runAsync()
方法执行异步任务。 supplyAsync()
用于执行有返回值的任务, runAsync()
用于执行无返回值的任务。
示例:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 异步任务逻辑
return "Hello";
});
2. 结果处理:CompletableFuture 提供了一系列方法来处理异步任务的结果。 thenApply()
方法用于对任务结果进行处理并返回一个新的 CompletableFuture 对象。
示例:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 异步任务逻辑
return "Hello";
}).thenApply(result -> result + " World");
3. 异常处理:CompletableFuture 支持异常处理。 exceptionally()
方法用于处理任务发生异常的情况,并返回一个默认值。
示例:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 异步任务逻辑
throw new RuntimeException("Error");
}).exceptionally(ex -> "Default Value");
4. 组合多个 CompletableFuture:CompletableFuture 提供了多个方法来组合多个 CompletableFuture 对象。 thenCombine()
方法用于将两个 CompletableFuture 对象的结果进行组合处理。
示例:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2);
5. 等待多个 CompletableFuture 完成:CompletableFuture 提供了 allOf()
和 anyOf()
方法来等待多个 CompletableFuture 对象的完成。 allOf()
方法等待所有 CompletableFuture 对象完成, anyOf()
方法等待任意一个 CompletableFuture 对象完成。
示例:
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);
6. 超时处理:CompletableFuture 支持设置超时时间。 completeOnTimeout()
方法在指定时间内等待任务完成,超时后执行指定操作。
示例:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 异步任务逻辑
return "Hello";
}).completeOnTimeout("Default Value", 1, TimeUnit.SECONDS);
CompletableFuture 提供了丰富的功能,可以简化异步编程的复杂性,提高代码的可读性和可维护性。它在并发编程、网络编程、异步任务处理等场景中都有广泛的应用。