经典 OOM 问题|pthread_create

1、背景

近期版本上线后收到很多用户反馈(大可能是华为用户)崩溃,日志上整体表现为 pthread_create (1040KB stack) failed: XXX。html

java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Out of memory 1 java.lang.Thread.nativeCreate(Native Method) 2 java.lang.Thread.start(Thread.java:743) 3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941) 4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009) 5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151) 6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607) 7 java.lang.Thread.run(Thread.java:774) 复制代码
java.lang.OutOfMemoryError pthread_create (1040KB stack) failed: Try again 1 java.lang.Thread.nativeCreate(Native Method) 2 java.lang.Thread.start(Thread.java:733) 3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975) 4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043) 5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1185) 6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 7 java.lang.Thread.run(Thread.java:764) 复制代码

2、问题分析

2.1 初步推断

Android的内存管理策略

OOM 并不等于 RAM 不足,这和 Android 的内存管理策略有关。java

咱们知道,内存分为虚拟地址和物理地址。经过 malloc 或 new 分配的内存都是虚拟地址空间的内存。虚拟地址空间比物理的地址空间要大的多。在较多进程同时运行时,物理地址空间有可能不够,这该怎么办?android

Linux 采用的是 “进程内存最大化” 的分配策略,用 Swap 机制来保证物理内存不被消耗殆尽,把最近最少使用的空间腾到外部存储空间上,伪装仍是存储在 RAM 里。c++

虽然 Android 基于 Linux,可是在内存策略上有本身的套路 —— 没有交换区。git

Android 的进程分配策略是每一个进程都有一个内存占用限制,这个具体大小由手机具体配置决定。目的就是为了让更多的进程都保留在 RAM 中,这样每一个进程被唤起的时候能够避免外部存储到内部存储的数据读写的消耗,加快更多的 App 恢复的响应速度,也避免了流氓 App 抢占全部内存。随之而然,Android 采用了本身的 LowMemoryKill 策略来控制RAM中的进程。若是 RAM 真的不足,MemoryKiller 就会杀死一些优先级比较低的进程来释放物理内存。github

因此触发OOM,只多是使用的虚拟内存地址空间超过度配的阈值。shell

那 Android 为每一个应用分配多少内存呢?这个因手机而异,以手头的测试机举例,系统正常分配的内存最多为 192 M;当设置 largeHeap 时,最多可申请 512M。编程

2.2 代码分析

那这个溢出是怎么被系统抛出的?经过 Android 源码能够看到,是由 runtime/thread.cc内抛出的异常。c#

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) 复制代码

线程建立有如下两个关键的步骤:数组

  • 第一列中的建立线程私有的结构体JNIENV(JNI执行环境,用于C层调用Java层代码)
  • 第二列中的调用posix C库的函数pthread_create进行线程建立工做

而这两步均有可能抛出OOM,基本定位 —— 建立线程致使了OOM

Android 建立线程源码与OOM分析 该文分析了建立线程的原理,其实就是调用mmap分配栈内存(虚拟内存),再经过 Linux 的 mmap 调用映射到用户态虚拟内存地址空间。建立线程过程当中发生OOM是由于进程内的虚拟内存地址空间耗尽了。

那何时会虚拟内存地址空间不足呢 ?

方向一:fd 过多

Linux 系统中一切皆文件,网络是文件,打开文件、新建 tcp 链接也是文件,都会占用 fd。fd是一种资源,是资源就会有限制。每一个进程最大打开文件的数目有一个上限。

而fd的增长的时机有:

  • 建立socket网络链接
  • 打开文件
  • 建立HandlerThread
  • 建立NIO的Channel(读写各占用一个fd)
  • 经过命令:ls -l /proc//fd/ 来查看某个进程打开了哪些文件
  • cat /proc/<pid>/limits 命令查看进程的fd限制,或其它限制 如Max open files
  • lsof -p <pid> |wc -l 查看进程全部的fd总数

如上图,Max open files表示每一个进程最大打开文件的数目,进程每打开一个文件就会产生一个文件描述符fd(记录在/proc/pid/fd中)

验证也很简单,经过触发大量的网络链接或者文件打开,每打开一个 socket 都会增长一个 fd。

private Runnable increaseFDRunnable = new Runnable() {
      @Override
      public void run() {
          try {
              for (int i = 0; i < 1000; i++) {
                  new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/status"));
              }
              Thread.sleep(Long.MAX_VALUE);
          } catch (InterruptedException e) {
              //
          } catch (FileNotFoundException e) {
              //
          }
      }
  };
复制代码
方向二:线程过多

已用逻辑空间地址能够查看 /proc//status 中的 VmPeak / VmSize

无非两个缘由: 一、进程的栈内存超过了虚拟机的最大内存数; 二、线程数达到了系统最大限制数;

排查工具

  • profilter CPU 查看当前全部线程列表

  • 使用CPU分析器监视CPU使用状况和线程活动使用过重了?没法分类统计?可使用 adb shell ps -T -p ,还可使用 | grep xxx 过滤,使用wc -l来统计线程数量。

  • 直接dump进程内存,来查看内存状况:

    adb shell dumpsys meminfo [pacakgename]
    复制代码
  • 也能够查看线程等汇总数据:

    adb shell
    cat /proc/19468/status
    复制代码

Linux在 /proc/sys/kernel/threads-max 中有描述线程限制,能够经过命令cat /proc/sys/kernel/threads-max 查看,华为在线程限制上很是严苛,在 7.0+ 手机上已将最大线程数修改为了 500。

那么是哪里代码致使了线程爆发呢?咱们使用 watch1s打印一下当前的线程数再经过页面交互来定位问题,观察看看哪类的线程名字在增多。

watch -n 1 -d 'adb shell ps -T | grep XXX | wc -l'
复制代码

观察后发现,线程总数在进入直播间时,垂手可得就达到了 290多,并且有大量 RxCachedThreadSchedule 线程(也就是 Rx 的Scheduler.io调度器)被建立,IO线程数暴涨到 46。停留在直播间一段时间,线程数只增不减,并不会过时清理。

2.3 验证推断,定位缘由

写个demo来验证,用 Kotlin 协程和 RxJava IO 调度器,模拟密集并发IO的环境

for (i in 0..100) {
            GlobalScope.launch(Dispatchers.IO) {
                delay(100)
                Log.e("IOExecute", "协程 - 当前线程:"
                      + Thread.currentThread().name)
            }
        }
复制代码

for (i in 0..100) {
            ThreadExecutor.IO.execute {
                Thread.sleep(100)
                Log.e("IOExecute", "RxJava IO - 当前线程:"
                      + Thread.currentThread().name)
            }
        }
复制代码

看起来 IO 线程没有复用,有点奇怪,咱们知道 Rx 的调度器其实就是封装的线程池,咱们也早已对线程池的流程倒背如流。以下图:

难道是工做队列满了?难道是线程无上限?难道是饱和策略有问题?

疑点

  1. 初进直播间,密集IO,没有复用,线程突增

  2. 停留超过 keepAliveTime,IO线程没有销毁

源码探寻

那到底哪里出了问题呢?本着挖掘机专业毕业的精神,咱们来看看Scheduler.io的源码定位缘由,看源码前,咱们先提出疑问和设想,带着问题看源码才不容易迷失方向:

疑问

  • 工做队列是怎么管理的,容量多大?
  • 线程池策略是什么?何时新建线程?何时销毁?

咱们先来看看 RxJava 线程模型图,理清楚类之间的关系:Scheduler 是 RxJava 的线程任务调度器,Worker 是线程任务的具体执行者。不一样的Scheduler类会有不一样的Worker实现,由于Scheduler类最终是交到Worker中去执行调度的。

能够看到,Schedulers.io()中使用了静态内部类的方式来建立出了一个单例IoScheduler对象出来,这个IoScheduler是继承自Scheduler的。

@NonNull
static final Scheduler IO;

@NonNull
public static Scheduler io() {
    //1.直接返回一个名为IO的Scheduler对象
    return RxJavaPlugins.onIoScheduler(IO);
}

static {
    //省略无关代码
    
    //2.IO对象是在静态代码块中实例化的,这里会建立按一个IOTask()
    IO = RxJavaPlugins.initIoScheduler(new IOTask());
}

static final class IOTask implements Callable<Scheduler> {
    @Override
    public Scheduler call() throws Exception {
        //3.IOTask中会返回一个IoHolder对象
        return IoHolder.DEFAULT;
    }
}

static final class IoHolder {
    //4.IoHolder中会就是new一个IoScheduler对象出来
    static final Scheduler DEFAULT = new IoScheduler();
}
复制代码

IoScheduler 的父类 Scheduler 在 scheduleDirect()、schedulePeriodicallyDirect() 方法中建立了 Worker,而后会分别调用 worker 的 schedule()、schedulePeriodically() 来执行任务。

public abstract class Scheduler {
    
    //检索或建立一个表明操做串行执行的新{@link Scheduler.Worker}。工做完成后,应使用{@link Scheduler.Worker#dispose()}取消订阅。 return一个Worker,它表明要执行的一系列动做。
    @NonNull
    public abstract Worker createWorker();

    @NonNull
    public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) {
        final Worker w = createWorker();

        final Runnable decoratedRun = RxJavaPlugins.onSchedule(run);

        DisposeTask task = new DisposeTask(decoratedRun, w);

        w.schedule(task, delay, unit);

        return task;
    }

    @NonNull
    public Disposable schedulePeriodicallyDirect(@NonNull Runnable run, long initialDelay, long period, @NonNull TimeUnit unit) {
        final Worker w = createWorker();
        //省略无关代码
        Disposable d = w.schedulePeriodically(periodicTask, initialDelay, period, unit);
        //省略无关代码
    }
}

复制代码

前面咱们说到,不一样的Scheduler类会有不一样的Worker实现,咱们看看 IoScheduler 这个实现类对应的 Worker 是什么:

final AtomicReference<CachedWorkerPool> pool;

public Worker createWorker() {
    //就是new一个EventLoopWorker,传一个 CachedWorkerPool 对象(Worker缓存池)
    return new EventLoopWorker(pool.get());
}

static final class EventLoopWorker extends Scheduler.Worker {
    private final CompositeDisposable tasks;
    private final CachedWorkerPool pool;
    private final ThreadWorker threadWorker;

    final AtomicBoolean once = new AtomicBoolean();
    
    //构造方法
    EventLoopWorker(CachedWorkerPool pool) {
        this.pool = pool;
        this.tasks = new CompositeDisposable();
        //从缓存Worker池中取一个Worker出来
        this.threadWorker = pool.get();
    }

    @NonNull
    @Override
    public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) {
        //省略无关代码
        
        //Runnable交给threadWorker去执行
        return threadWorker.scheduleActual(action, delayTime, unit, tasks);
    }
}
复制代码

接下来是Worker缓存池的操做:

CachedWorkerPool的get()
static final class CachedWorkerPool implements Runnable {
    ThreadWorker get() {
        if (allWorkers.isDisposed()) {
            return SHUTDOWN_THREAD_WORKER;
        }
        while (!expiringWorkerQueue.isEmpty()) {
            //若是缓冲池不为空,就从缓冲池中取threadWorker
            ThreadWorker threadWorker = expiringWorkerQueue.poll();
            if (threadWorker != null) {
                return threadWorker;
            }
        }

        //若是缓冲池中为空,就建立一个并返回。
        ThreadWorker w = new ThreadWorker(threadFactory);
        allWorkers.add(w);
        return w;
    }
}
复制代码

ThreadWorker到底作了什么呢?追进去父类NewThreadWorker

NewThreadWorker 的构造函数
public class NewThreadWorker extends Scheduler.Worker implements Disposable {
    private final ScheduledExecutorService executor;
		volatile boolean disposed;

		public NewThreadWorker(ThreadFactory threadFactory) {
    		//构造方法中建立一个ScheduledExecutorService对象,能够经过ScheduledExecutorService来使用线程池
    		executor = SchedulerPoolFactory.create(threadFactory);
		}
    }
复制代码
SchedulerPoolFactory.create
public final class SchedulerPoolFactory {
		/** * Creates a ScheduledExecutorService with the given factory. * @param factory the thread factory * @return the ScheduledExecutorService */
    public static ScheduledExecutorService create(ThreadFactory factory) {
        // 此处建立了线程!!
        final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1, factory);
        if (PURGE_ENABLED && exec instanceof ScheduledThreadPoolExecutor) {
            ScheduledThreadPoolExecutor e = (ScheduledThreadPoolExecutor) exec;
            POOLS.put(e, exec);
        }
        return exec;
    }
}
复制代码

因此,IoScheduler 使用 CachedWorkerPool 做为线程池,其内部维护了一个阻塞队列,用于记录全部可用线程,当有新的任务需求时,线程池会查询阻塞队列中是否有可用线程,没有的话就新建一个。

咱们想要知道为何线程突增没有复用,就要看看全部使用过的那些空闲线程什么时机会被回收到阻塞队列中去。

CachedWorkerPool 的 release()
void release(ThreadWorker threadWorker) {
            // Refresh expire time before putting worker back in pool
  					// 刷新线程的到期时间 将执行完毕的 Worker 放入缓存池中
            threadWorker.setExpirationTime(now() + keepAliveTime);
            expiringWorkerQueue.offer(threadWorker);
        }
复制代码

调用此处代码只有一处:

@Override
        public void dispose() {
            if (once.compareAndSet(false, true)) {
                tasks.dispose();
                pool.release(threadWorker);
            }
        }
复制代码

对于这一处的调用,能够简单理解为线程内部维护了一个状态列表,当线程内的任务完成以后,会调用 dispose 来解除订阅,释放线程的占用。

那何时销毁呢?能够看到 CachedWorkerPool 构造函数中建立了清理定时任务:

static final class CachedWorkerPool implements Runnable {
        CachedWorkerPool(long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory) {
            //...
          	// 建立一个线程,该线程默认会每60s执行一次,来清除已到期的线程
                evictor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY);
           // 设置定时任务
                task = evictor.scheduleWithFixedDelay(this, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS);
            //...
        }

        @Override
        public void run() {
            evictExpiredWorkers();
        }
     }
复制代码
CachedWorkerPool 的 evictExpiredWorkers()
void evictExpiredWorkers() {
            if (!expiringWorkerQueue.isEmpty()) {
                long currentTimestamp = now();

                for (ThreadWorker threadWorker : expiringWorkerQueue) {
                    if (threadWorker.getExpirationTime() <= currentTimestamp) {
                        if (expiringWorkerQueue.remove(threadWorker)) {
                            allWorkers.remove(threadWorker);
                        }
                    } else {
                        // 队列是根据失效时间排序的,因此一旦当咱们找到未失效的Worker就能够中止清理了
                        break;
                    }
                }
            }
        }
复制代码

这个IO调度器不像计算调度器,计算调度器用一个数组来保存一组线程,而后根据索引将任务分配给每一个线程,多余的任务放在队列中等待执行,因此每一个线程后面任务的执行须要等待前面的任务执行完毕。

而IO调度器里的线程池是一个能够自增、无上限的线程池,且60s 保活。也就是说:若是在 60s 内密集请求 IO 调度,超过了复用阈值,调度器不会约束线程数且会不断开新线程

这样子就解释了疑点 1 为何进直播间时线程暴涨,是由于没有任务队列,直接来一个任务,能复用就复用 Worker,不能就新建。

那疑点 2 呢?为何停留超过了 60s 突涨的线程没有被回收?

咱们推测:

  • 清理线程是否在正常工做?

  • 有没有可能存在订阅泄露?有的地方 Observable 没有及时结束,因此一直占用着线程呢?

斋看源码没法模拟真实生产环境,那

如何在没法改动源码状况下作动态观察?

3种方式:

  1. 动态hook
  2. 静态插桩
  3. 非阻塞式断点,打 log

观察点整理

  1. 任务的入口 —— 到底有多频繁,谁在提交任务?
  2. 工做的逻辑 —— 这个任务被分配了哪一个线程?
  3. 复用的逻辑 —— 满的时候触发 new Thread,此时复用的状况怎么样,为何会新建这么多?
  4. 释放的逻辑 —— 解除订阅和订阅数量的比对,是否存在订阅泄漏?
  5. 过时清除的逻辑 —— 清理线程是否在正常工做?每一个线程都在作什么,为何停留在直播间没有被销毁掉

好,具体观察结果的log就不贴了,由于斋看日志体验不好,我画了一张图总结下整个流程:

看出什么问题了吗?

在直播间内一直停留,超过 keepAliveTime,之因此没有清理线程,是由于线程都没过时,没错,前面的 46 个 IO 线程都没有过时,IoScheduler 使用 ConcurrentLinkedQueue 维护使用完毕的Worker,按插入顺序(也就是释放顺序)排序,因此会优先使用最先过时的 Worker 提供给新任务。

咱们来算一下,算 2s 一个轮询,直播间只有 1 个轮询协议(实际上不止), 那 60s 已经足够让 30 个 Work 更新一遍过时时间了,n 个轮询能够更新 60 / 2 * n 个 worker 的过时时间。

果真源码面前,了无秘密

结论

  1. 直播间这种业务场景的特色:进房时,短时间内大量任务要并行;存在多轮询

  2. RxJava 的 IO 调度策略,并不适合用于并发多 IO + 轮询的状况,没有任务排队队列、线程可自增、无上限、优先使用快过时的线程;

  3. 另外,业务中存在 Rx 不合理使用(前面咱们拦截了入口,因此能够直观看到哪里在使用 IO 调度),如 timer 、打点、jsBridge 都使用了 IO 调度,嵌套调度(重复 new 了 Worker 任务),没有跟随生命周期取消订阅等等等等。

3、解决

找到问题的根源,问题便已经解决了一半,基本 3 个解决方向:

  1. 优化不合理的调度器建立释放

  2. 线程收敛,不是阻塞就必定要用 IO 调度

    其实 IO 不必使用多线程,改成 IO 多路复用或协程更合理

  3. 减小并发 IO,分块加载

4、思考

4.1 如何快速分类并定位线程?如何拿到NativeThread?

用前面的方式去分析,有几个缺点:

  1. 由于前面咱们拿到堆栈是用了断点 log 的方式,因此咱们拿不到没有经过指定方法建立的任务信息;
  2. 咱们无法约束开发小伙伴和三方 SDK 为每一个线程起自定义名称,没法快速分类线程,例如 thread-1,咱们就很难定位到是哪一个类发起的调用;
  3. 咱们只能拿到 Java 层与其对接的 native 层 thread 总数,拿不到没有 attach 到 java 层的 native thread,也就是直接在 native 层建立的线程,好比 Futter engine 中的native thread。

有什么骚操做呢?

1. ASM字节码修改

思路很简单,你既然要建立线程,就确定是经过如下几种方式:

  • Thread 及其子类
  • TheadPoolExecutor 及其子类、ExecutorsThreadFactory 实现类
  • AsyncTask
  • Timer 及其子类

滴滴团队开源库booster就是这么个思路 —— 利用ASM对字节码修改,将全部建立线程的指令在编译期间替换成自定义的方法调用,为线程名加上调用者的类名前缀,实现了追踪线程建立来源。

除了支持线程重命名,还能够把Executors 的方法调用替换成 ShadowExecutors 中对应的优化方法,达到全局暴力收敛的效果。

备注:若是采用 booster ,尽可能多作一些测试和降级方案。例如:ShadowExecutors.newOptimizedFixedThreadPool方法中使用了 LinkedBlockingQueue 队列,没有指定队列大小,默认为 Integer.MAX_VALUE,无界的LinkedBlockingQueue 做为阻塞队列,当任务耗时较长时可能会致使大量新任务在队列中堆积, CPU 和内存飙升,最终致使 OOM。

2. NativeHook

那问题来了,ASM字节码修改,只 hook 到了 Java 层与其对接的 native 层 thread ,怎么拿到直接在 native 层建立的线程呢?诶,咱们前面不是看了线程建立的 C++ 代码吗?基本思路就是找到 pthread_create 相关的函数,拦截它。

第一步:寻找Hook点

这须要对线程的启动流程有必定的了解,能够参考这篇文章Android线程的建立过程

java_lang_Thread.cc:Thread_nativeCreate

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size, jboolean daemon) {
  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}
复制代码

thread.cc 中的CreateNativeThread函数

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
    ...
    pthread_create_result = pthread_create(&new_pthread,
                                             &attr,
                                             Thread::CreateCallback,
                                             child_thread);
    ...
}
复制代码
第二步:查找Hook的So

上面Thread_nativeCreate、CreateNativeThread和pthread_create函数分别编译在哪一个 library 中呢?

很简单,咱们看看编译脚本Android.bp就知道了。

art_cc_library {
   name: "libart",
   defaults: ["libart_defaults"],
}

cc_defaults {
   name: "libart_defaults",
   defaults: ["art_defaults"],
   host_supported: true,
   srcs: [
    thread.cc",
   ]
}
复制代码

能够看到是在"libart.so"中。

第三步:查找Hook函数的符号

C++ 的函数名会 Name Mangling,咱们须要看看导出符号。

readelf -a libart.so
复制代码

pthread_create函数的确是在libc.so中,并且由于c编译的不须要deMangling

001048a0  0007fc16 R_ARM_JUMP_SLOT   00000000   pthread_create@LIBC
复制代码
第四步:实现

考虑到性能问题,咱们只 hook 指定的so。

hook_plt_method("libart.so", "pthread_create", (hook_func) &pthread_create_hook);
复制代码

若是你想监控其余so库的 pthread_create,能够本身加上。Facebook 的 profilo 中有一种作法是把目前已经加载的全部so都统一hook了。

至于 pthread_create 的参数直接查看pthread.h就能够了。

int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
复制代码

获取堆栈就是在 native 反射 Java 的方法

jstring java_stack = static_cast<jstring>(jniEnv->CallStaticObjectMethod(kJavaClass, kMethodGetStack));
复制代码

Profilo :Facebook 的性能分析工具,黑科技不少

epic:该库已经支持拦截 Thread 类以及 Thread 类全部子类的 run方法,更进一步,咱们能够结合 Systrace 等工具,来生成整个过程的执行流程图。

注:对于 app 上的 hook,不要再像之前那样去依赖反射、动态代理了,关注下 lancet、epic,真的是随心所欲。

4.2 排查痛点

很是惋惜的是没能保留一手现场,只能靠猜,靠复现。

卡顿、崩溃都须要“现场信息”。由于 bug 产生也是依赖不少因素,好比用户的系统版本、CPU 负载、网络环境、应用数据、线程数、利用率、崩溃发生时全部的线程栈,而不仅是崩溃的线程栈……

脱离这个现场,咱们本地难以复现,也就很难去解决问题。那咱们应该如何去监控线上,而且保留足够多的现场信息协助咱们排查解决问题呢?

这里要么咱们能够本身研发一套崩溃收集系统,要么能够接入现有的方案

4.3 异步的本质?协程、NIO、fiber、loom 解决了什么?

回到本质上,思考下,为何咱们须要多线程?多线程真的有必要吗?

由于顺序代码结构是阻塞式的,每一行代码的执行都会使线程阻塞在那里,也就决定了全部的耗时操做不能在主线程中执行,因此就须要多线程来执行。

因此目的是非阻塞,方式是异步。

但许多异步库被引入,根本缘由是当前线程实现的不足,而并不是说明异步的代码更好。咱们不要理所固然以为,异步就是正常不过的事情。其实是 Java 的设计问题,让咱们一直默默忍受到如今,异步带来的一系列问题:回调地狱、不方便调试分析……

长期以来,Java 的线程是与操做系统的线程一一对应的,这种模式直接限制了 Java 平台并发能力的提高:任务阻塞意味着线程阻塞,线程状态切换又带来开销,阻塞线程对系统资源的浪费…… 从 Quasar 项目、Alibaba JDK 的协程特性,到 Kotlin 协程和 OpenJDK 的 Project Loom, Java 社区已经愈来愈多地认识到:目前 Java 的线程模型愈来愈难以知足整个行业对高并发应用开发的需求。

解决方式不少,其中一个流派 —— 语言层:

其中的表明 —— 协程,虽然在不一样语言中,协程的实现方法各有不一样,但本质是一致的,是一种任务封装的思想:调度任务代替了调度线程,从而减小线程阻塞,用尽可能少的线程执行尽可能多的任务。

线程调度二级模型

好比 Kotlin 协程,由于 Kotlin 的运行依赖于 JVM,所以没办法在底层支持协程。同时,Kotlin 是一门编程语言,须要在语言层面支持协程,而不是像框架那样在语言层面之上支持。所以,Kotlin-JVM 协程最核心的部分是在编译器中,基于各类 Callback 技术达到看起来像同步代码的效果,本质上仍是异步,调用各类阻塞 API 仍是无解,好比 synchronized、 Native 方法中线程挂起,该阻塞线程仍是会阻塞。

为了使 Java 并发能力在更大范围上获得提高,从底层进行改进即是必然。这就是 Project Loom 项目发起的缘由,也就是另一个流派 —— JVM 层。

[译]loom项目提案

表明项目是 Project LoomAJDK(Alibaba JDK),从 Erlang 和 Go 语言中获得了启发,从 JVM 层面着手,把以前阻塞线程的,通通改成阻塞“纤程(fiber)”、“轻量级线程”或者“虚拟线程”,这么作的优势是更能完全解决问题,不须要靠 async / await 这种语法糖了,在JVM 和类库层面作支持,能使整个 JVM 生态上的其余语言都收益

但缺点或者说难点一样明显,那就是如何与现有的代码兼容,这个改造,意味着不少 native 方法也要改,大概要等到 JDK20 才能出预览版本吧,再看看咱们 Android 如今仅支持到 Java8,emmm,仍是早日使用 Kotlin-JVM 协程吧。

若是后面 JVM 开发团队完成这个项目,看看 Kotlin coroutine 对此会有什么反应,须要怎么调整,在最好的状况下,Kotlin coroutine 未来只是简单映射到 “纤程” 上。其实蛮有意思的,社区上关于异步的讨论,还有对Java、JVM的设计反思。感兴趣的小伙伴能够去研究研究。

参考:


我是 FeelsChaotic,一个写得了代码 p 得了图,剪得了视频画得了画的程序媛,致力于追求代码优雅架构设计T 型成长

欢迎关注 FeelsChaotic 的简书掘金若是个人文章对你哪怕有一点点帮助,欢迎 ❤️! 你的鼓励是我写做的最大动力!

最最重要的,请给出你的建议或意见,有错误请多多指正!

相关文章
相关标签/搜索