并发编程:Kotlin Coroutines vs Java concurrency

引言

实际开发过程中我们经常需要处理并发操作,以提高性能和资源利用率。并发编程不仅可以加快应用程序的响应速度,还可以充分利用多核处理器的性能。在这篇文章中,我们将深入探讨并比较两种不同的方式来处理并发编程:Kotlin Coroutines和Java Concurrency。这两种技术在不同的编程语境和需求下都有它们的优点和适用场景。通过了解它们的特点,您将能够更明智地选择合适的并发工具,以满足您的项目需求。

从需求出发:异步获取User和Avatar

已有两个异步接口,模拟如下:

java 复制代码
/**
* 模拟网络请求
* 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 异步回调

最简单直接的方式,在异步回调里直接调用异步回调

typescript 复制代码
/**
 * 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 工具让异步接口变成同步,如下:

scss 复制代码
    /**
     * 加锁
     */
    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];

    }
}

业务直接调用即可,如下所示:

ini 复制代码
long startTime = System.currentTimeMillis();
User user = getUser();
user = getUserAvatar(user);
LogKt.log(user.toString());
LogKt.log("costTime -->"+(System.currentTimeMillis() - startTime));

业务调用方看起来简单多了,但是异步转同步的过程需要加锁,这部分容易出错。

Kotlin 协程实现

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实现异步代码同步化:

kotlin 复制代码
/**
 * 异步同步化
 */
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)
    }
}

这里我们暂时先不关心取消与异常。业务调用方如下:

scss 复制代码
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:

ini 复制代码
/**
 * 并发下载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 协程实现

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的关键特点:

  1. 线程和执行器框架:Java Concurrency提供了多线程和执行器框架,允许您创建和管理线程,以在多核处理器上执行并发任务。
  2. 同步和锁 :Java Concurrency支持传统的同步和锁机制,如synchronized关键字和ReentrantLock,用于确保多线程环境下的数据同步和安全性。
  3. 线程池:Java Concurrency提供了线程池来管理线程的生命周期,减少了线程的创建和销毁开销。
  4. 并发集合 :Java Concurrency提供了并发集合类,如ConcurrentHashMapConcurrentLinkedQueue,用于在多线程环境下安全地操作数据结构。
  5. 手动的线程管理:您需要明确地创建和管理线程池中的线程。 灵活的任务提交:您可以将任务作为 Runnable 或 Callable 对象提交给执行器。 线程同步:使用 CountDownLatch 和 AtomicInteger 等同步机制进行协调。 更多的控制:Java Executor 提供了更细粒度的线程池大小和任务提交控制。

Kotlin Coroutines

Kotlin Coroutines是一种异步编程框架,它在Kotlin语言中引入了挂起函数的概念,使得异步代码更加直观和容易理解。以下是Kotlin Coroutines的关键优点:

  1. 简洁性和可读性 :Kotlin Coroutines使用suspend关键字,使异步代码看起来像是同步代码,提高了代码的可读性。
  2. 取消和超时处理:Coroutines内置了取消和超时处理机制,使得处理任务取消或在超时后进行处理变得简单。
  3. 协程作用域:Kotlin Coroutines允许您创建协程作用域,以管理协程的生命周期,防止资源泄漏。
  4. 并发组合器 :Coroutines提供了各种方便的并发组合器,例如async/awaitlaunch,使并发编程更加容易。

总结

Kotlin 协程中也有等待的过程,但与传统的 Java 多线程方式相比,有一些关键的不同之处。以下是 Kotlin 协程和 Java 多线程之间的一些主要不同之处:

  1. 挂起与阻塞:
  • Kotlin 协程使用挂起来代替阻塞。当协程中的操作需要等待时,它会被挂起,让出线程,然后允许其他协程在同一个线程中执行。这样可以更高效地利用线程,而不会阻塞整个线程。
  • Java 多线程使用阻塞来等待,即线程会在某个操作上阻塞,直到操作完成或等待超时。
  1. 无需显式锁:
  • Kotlin 协程通过挂起和恢复来避免了显式的锁机制。协程之间的数据共享是更安全的,因为它们不会直接在不同线程中执行,从而避免了多线程竞争的问题。
  • Java 多线程通常需要使用锁来保护共享资源,以防止多个线程之间的竞争条件和数据不一致性。
  1. 代码简洁性:
  • Kotlin 协程使用顺序的代码结构,更易于理解和编写。协程代码通常比传统的多线程代码更简洁,因为它们隐藏了大部分线程管理细节。
  • Java 多线程代码可能需要处理更多的线程管理和同步细节,导致代码变得复杂。
  1. 异常处理:
  • Kotlin 协程通过异常传播和处理提供了更直观的方式来处理异常。异常在协程之间传播,可以使用 try-catch 块捕获异常。
  • Java 多线程代码中的异常处理可能需要更多的手动操作,有时可能较为繁琐。
  1. 线程切换:
  • Kotlin 协程内部管理线程切换,使得在协程之间进行切换更为高效。
  • Java 多线程通常需要手动进行线程切换,可能需要使用 ExecutorServiceFuture 来管理线程。

高效和轻量,都不是 Kotlin 协程的核心竞争力。 Kotlin 协程的核心竞争力在于:它能简化异步并发任务。 Kotlin 协程提供了更高级、更简洁、更易读的方式来处理异步任务和并发操作。它的语法和语义更贴近顺序执行,而底层细节由协程库自动管理。Java Executor 则需要更多手动的线程管理和同步,代码可能会更复杂。选择使用哪种方式取决于您的偏好、项目需求和已有代码基础。

源码

[[github.com/ThirdPrince...]

相关推荐
ggdpzhk1 小时前
idea 编辑竖列:alt +shift+insert
java·ide·intellij-idea
hikktn2 小时前
Java 兼容读取WPS和Office图片,结合EasyExcel读取单元格信息
java·开发语言·wps
迪迦不喝可乐2 小时前
软考 高级 架构师 第十一章 面向对象分析 设计模式
java·设计模式
檀越剑指大厂2 小时前
【Java基础】使用Apache POI和Spring Boot实现Excel文件上传和解析功能
java·spring boot·apache
苹果酱05672 小时前
Golang的网络流量分配策略
java·spring boot·毕业设计·layui·课程设计
孑么3 小时前
GDPU Android移动应用 重点习题集
android·xml·java·okhttp·kotlin·android studio·webview
未命名冀3 小时前
微服务面试相关
java·微服务·面试
Heavydrink4 小时前
ajax与json
java·ajax·json
阿智智4 小时前
纯手工(不基于maven的pom.xml、Web容器)连接MySQL数据库的详细过程(Java Web学习笔记)
java·mysql数据库·纯手工连接
fangxiang20084 小时前
spring boot 集成 knife4j
java·spring boot