likes
comments
collection
share

并发编程:Kotlin Coroutines vs Java concurrency

作者站长头像
站长
· 阅读数 9

引言

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

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

源码

[[github.com/ThirdPrince…]

转载自:https://juejin.cn/post/7291936473079119907
评论
请登录