引言
实际开发过程中我们经常需要处理并发操作,以提高性能和资源利用率。并发编程不仅可以加快应用程序的响应速度,还可以充分利用多核处理器的性能。在这篇文章中,我们将深入探讨并比较两种不同的方式来处理并发编程:Kotlin Coroutines和Java Concurrency。这两种技术在不同的编程语境和需求下都有它们的优点和适用场景。通过了解它们的特点,您将能够更明智地选择合适的并发工具,以满足您的项目需求。
从需求出发:异步获取User和Avatar
已有两个异步接口,模拟如下:
/**
* 模拟网络请求
* Java 版本
*/
public class ClientUtils {
static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
/**
* getUser
*
* @param userId
* @param userCallback
*/
public static void getUser(int userId, UserCallback userCallback) {
executorService.execute(() -> {
long sleepTime = new Random().nextInt(500);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
userCallback.onCallback(new User(userId, sleepTime + "", "avatar", ""));
});
}
/**
* getAvatar
*
* @param user
* @param userCallback
* @throws InterruptedException
*/
public static void getUserAvatar(User user, UserCallback userCallback) {
executorService.execute(() -> {
int sleepTime = new Random().nextInt(1000);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
user.setFile(sleepTime + ".png");
userCallback.onCallback(user);
});
}
}
interface UserCallback {
void onCallback(User user);
}
需求分析
我们的需求是获取用户信息(User)和用户头像(Avatar)。这两个操作是相互独立的,但必须按顺序执行:首先获取用户信息,然后使用该信息获取用户头像。这种情况下,异步操作是必不可少的,因为网络请求通常需要时间来完成。
java 异步回调
最简单直接的方式,在异步回调里直接调用异步回调
/**
* getUser callBack
*/
public class GetUser {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
ClientUtils.getUser(1, new UserCallback() {
@Override
public void onCallback(User user) {
LogKt.log(user.toString());
ClientUtils.getUserAvatar(user, new UserCallback() {
@Override
public void onCallback(User user) {
LogKt.log(user.toString());
LogKt.log("costTime -->"+(System.currentTimeMillis() - startTime));
}
});
}
});
}
}
这种实现方式简单,但是缺点也很明显,接口多了容易形成回调地狱,代码难以维护且调用流程脱离了主流程。
java 异步加锁变为同步
使用JUC包下的CountDownLatch 工具让异步接口变成同步,如下:
/**
* 加锁
*/
public static User getUser() {
CountDownLatch countDown = new CountDownLatch(1);
User[] result = new User[1];
ClientUtils.getUser(1, new UserCallback() {
@Override
public void onCallback(User user) {
result[0] = user;
countDown.countDown();
}
});
try {
countDown.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return result[0];
}
/**
* getAvater
*
* @param user
* @return
*/
public static User getUserAvatar(User user) {
CountDownLatch countDown = new CountDownLatch(1);
User[] result = new User[1];
ClientUtils.getUserAvatar(user, new UserCallback() {
@Override
public void onCallback(User user) {
result[0] = user;
countDown.countDown();
//result = user;
}
});
try {
countDown.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return result[0];
}
}
业务直接调用即可,如下所示:
long startTime = System.currentTimeMillis();
User user = getUser();
user = getUserAvatar(user);
LogKt.log(user.toString());
LogKt.log("costTime -->"+(System.currentTimeMillis() - startTime));
业务调用方看起来简单多了,但是异步转同步的过程需要加锁,这部分容易出错。
Kotlin 协程实现
Kotlin协程可以优雅的实现异步同步化,让业务方只关心业务,而无需关心线程切换的细节。
先把Kotlin版本的异步回调:
/**
* 模拟客户端请求
*/
object ClientManager {
var executor: Executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
val customDispatchers = executor.asCoroutineDispatcher()
/**
* getUser
*/
fun getUser(userId: Int, callback: (User) -> Unit) {
executor.execute {
val sleepTime = Random().nextInt(500)
Thread.sleep(sleepTime.toLong())
callback(User(userId, sleepTime.toString(), "avatar", ""))
}
}
/**
* getAvatar
*/
fun getUserAvatar(user: User, callback: (User) -> Unit) {
executor.execute {
val sleepTime = Random().nextInt(1000)
try {
Thread.sleep(sleepTime.toLong())
} catch (e: InterruptedException) {
e.printStackTrace()
}
user.file = "$sleepTime.png"
callback(user)
}
}
}
使用 suspendCoroutine实现异步代码同步化:
/**
* 异步同步化
*/
suspend fun getUserAsync2(userId: Int): User = suspendCoroutine { continuation ->
ClientManager.getUser(userId) {
continuation.resume(it)
}
}
/**
* 异步同步化
*/
suspend fun getUserAvatarAsync2(user: User): User = suspendCoroutine { continuation ->
ClientManager.getUserAvatar(user) {
continuation.resume(it)
}
}
这里我们暂时先不关心取消与异常。业务调用方如下:
val costTime = measureTimeMillis {
val user = getUserAsync(1);
val userAvatar = getUserAvatarAsync2(user)
log(userAvatar.toString())
}
log("cost -->$costTime")
是不是看起来比Java版本简洁许多,这还不够。
需求变更:首先并发访问100个User,然后在并发访问100个Avatar
java实现 同样适用JUC下的CountDownLatch:
/**
* 并发下载100个User
* 然后并发下载100个头像
* Java
*/
public class UserDownload {
public static void main(String[] args) throws InterruptedException {
long startTime = System.currentTimeMillis();
List<Integer> userId = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
userId.add(i);
}
Map<Integer,User> map = new ConcurrentHashMap<>();
log("开始下载user");
AtomicInteger atomicInteger = new AtomicInteger(userId.size());
CountDownLatch countDownLatch = new CountDownLatch(userId.size());
for (Integer id : userId) {
ClientUtils.getUser(id, user -> {
log("atomicInteger-->"+ atomicInteger.decrementAndGet());
map.put(id,user);
countDownLatch.countDown();
});
}
countDownLatch.await();
log("atomicInteger-->"+atomicInteger.get());
log("开始下载头像");
AtomicInteger atomicIntegerAvatar = new AtomicInteger(userId.size());
CountDownLatch countDownLatchDownload= new CountDownLatch(userId.size());
log("map size-->"+map.size());
for (User user : map.values()){
ClientUtils.getUserAvatar(user, new UserCallback() {
@Override
public void onCallback(User user) {
log("atomicIntegerAvatar-->"+ atomicIntegerAvatar.decrementAndGet());
map.put(user.getUserId(),user);
countDownLatchDownload.countDown();
}
});
}
countDownLatchDownload.await();
long costTime = (System.currentTimeMillis() -startTime)/1000;
log("costTime -->"+costTime);
}
}
这里并发访问条件下,需要记录User与下载次数,我使用了并发map与AtomicInteger。
Kotlin 协程实现
/**
* 并发下载100个User
* 然后并发下载100个头像
* Kotlin
*/
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val userIds: MutableList<Int> = ArrayList()
for (i in 1..100) {
userIds.add(i)
}
var count = userIds.size
val map: MutableMap<Int, User> = HashMap()
val deferredResults = userIds.map { userId ->
async {
val user = getUserAsync2(userId)
log("userId-->$userId :::: user ---> $user")
map[userId] = user
map
}
}
// 获取每个 async 任务的结果
val results = deferredResults.map { deferred ->
count--
log("count $count")
deferred.await()
}
log("map -->${map.size}")
val deferredAvatar = map.map { map ->
async {
getUserAvatarAsync2(map.value)
}
}
var countAvatar = results.size
val resultAvatar = deferredAvatar.map { deferred ->
countAvatar--
log("countAvatar $countAvatar")
deferred.await()
}
val costTime = (System.currentTimeMillis() - startTime) / 1000
log("costTime-->$costTime")
log("user -> $resultAvatar")
}
在协程的加持下,代码又变得简洁起来,没有锁,没有异步回调,没有并发容器(单线程调度器是线程安全的)。
Kotlin Coroutines vs Java concurrency
Java Concurrency
Java Concurrency API是Java平台上用于处理并发任务的传统工具。以下是Java Concurrency的关键特点:
- 线程和执行器框架:Java Concurrency提供了多线程和执行器框架,允许您创建和管理线程,以在多核处理器上执行并发任务。
-
同步和锁:Java Concurrency支持传统的同步和锁机制,如
synchronized
关键字和ReentrantLock
,用于确保多线程环境下的数据同步和安全性。 - 线程池:Java Concurrency提供了线程池来管理线程的生命周期,减少了线程的创建和销毁开销。
-
并发集合:Java Concurrency提供了并发集合类,如
ConcurrentHashMap
和ConcurrentLinkedQueue
,用于在多线程环境下安全地操作数据结构。 - 手动的线程管理:您需要明确地创建和管理线程池中的线程。 灵活的任务提交:您可以将任务作为 Runnable 或 Callable 对象提交给执行器。 线程同步:使用 CountDownLatch 和 AtomicInteger 等同步机制进行协调。 更多的控制:Java Executor 提供了更细粒度的线程池大小和任务提交控制。
Kotlin Coroutines
Kotlin Coroutines是一种异步编程框架,它在Kotlin语言中引入了挂起函数的概念,使得异步代码更加直观和容易理解。以下是Kotlin Coroutines的关键优点:
-
简洁性和可读性:Kotlin Coroutines使用
suspend
关键字,使异步代码看起来像是同步代码,提高了代码的可读性。 - 取消和超时处理:Coroutines内置了取消和超时处理机制,使得处理任务取消或在超时后进行处理变得简单。
- 协程作用域:Kotlin Coroutines允许您创建协程作用域,以管理协程的生命周期,防止资源泄漏。
-
并发组合器:Coroutines提供了各种方便的并发组合器,例如
async/await
和launch
,使并发编程更加容易。
总结
Kotlin 协程中也有等待的过程,但与传统的 Java 多线程方式相比,有一些关键的不同之处。以下是 Kotlin 协程和 Java 多线程之间的一些主要不同之处:
- 挂起与阻塞:
- Kotlin 协程使用挂起来代替阻塞。当协程中的操作需要等待时,它会被挂起,让出线程,然后允许其他协程在同一个线程中执行。这样可以更高效地利用线程,而不会阻塞整个线程。
- Java 多线程使用阻塞来等待,即线程会在某个操作上阻塞,直到操作完成或等待超时。
- 无需显式锁:
- Kotlin 协程通过挂起和恢复来避免了显式的锁机制。协程之间的数据共享是更安全的,因为它们不会直接在不同线程中执行,从而避免了多线程竞争的问题。
- Java 多线程通常需要使用锁来保护共享资源,以防止多个线程之间的竞争条件和数据不一致性。
- 代码简洁性:
- Kotlin 协程使用顺序的代码结构,更易于理解和编写。协程代码通常比传统的多线程代码更简洁,因为它们隐藏了大部分线程管理细节。
- Java 多线程代码可能需要处理更多的线程管理和同步细节,导致代码变得复杂。
- 异常处理:
- Kotlin 协程通过异常传播和处理提供了更直观的方式来处理异常。异常在协程之间传播,可以使用
try
-catch
块捕获异常。 - Java 多线程代码中的异常处理可能需要更多的手动操作,有时可能较为繁琐。
- 线程切换:
- Kotlin 协程内部管理线程切换,使得在协程之间进行切换更为高效。
- Java 多线程通常需要手动进行线程切换,可能需要使用
ExecutorService
或Future
来管理线程。
高效和轻量,都不是 Kotlin 协程的核心竞争力。 Kotlin 协程的核心竞争力在于:它能简化异步并发任务。 Kotlin 协程提供了更高级、更简洁、更易读的方式来处理异步任务和并发操作。它的语法和语义更贴近顺序执行,而底层细节由协程库自动管理。Java Executor 则需要更多手动的线程管理和同步,代码可能会更复杂。选择使用哪种方式取决于您的偏好、项目需求和已有代码基础。