美文网首页
并发编程:Kotlin Coroutines vs Java c

并发编程:Kotlin Coroutines vs Java c

作者: BlueSocks | 来源:发表于2023-11-20 18:00 被阅读0次

    引言

    实际开发过程中我们经常需要处理并发操作,以提高性能和资源利用率。并发编程不仅可以加快应用程序的响应速度,还可以充分利用多核处理器的性能。在这篇文章中,我们将深入探讨并比较两种不同的方式来处理并发编程: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的关键特点:

    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 则需要更多手动的线程管理和同步,代码可能会更复杂。选择使用哪种方式取决于您的偏好、项目需求和已有代码基础。

    相关文章

      网友评论

          本文标题:并发编程:Kotlin Coroutines vs Java c

          本文链接:https://www.haomeiwen.com/subject/bsbvwdtx.html