继续来,同我一起撸Kotlin Channel 深水区

一起撸个简单粗暴的Tv应用主界面的网格布局控件(下) 上一篇中我们已经一起学了怎么简单粗暴的撸个支持动态布局的网格控件出来,但在上一篇的介绍中,并没有学习实现网格控件的滑动效果,所以本篇就来讲讲,要如何让我们的网格控件可以支持自定义滑动策略。 效果 图一是Tv应用:当贝市场的主页 图二是咱自己撸的简单粗暴的 Tv 应用主界面网格控件:TvGridLayout 的示例,每个 Tab 下,每一屏的卡位大小、位置都是动态计算出来的。 实现 第一步:定义... 阅读详情

前言

本篇文章将着重分析协程间的通信方式。通过本篇文章,你将了解到:

  1. Channel的引入及简单使用
  2. Channel的原理
  3. Channel四种类型深入解析
  4. produce/actor的使用与原理

1. Channel的引入及简单使用

初级版协程间通信

先来看一个简单的通信Demo:

    fun testChannel() {
        //协程1
        var deferred= GlobalScope.async {
            //假装在加工数据
            Thread.sleep(2000)
            "Hello fishforest"
        }
        //协程2
        GlobalScope.launch {
            var result = deferred.await()
            println("get result from coroutine1: $result")
        }
    }

如上,协程2拿到了协程1的值,这就是一次简单的协程间通信过程。
现在需求变了,协程1一直在生产数据,协程2也需要不断地从中取数据,此时靠async/await 配合无能为力了。当然,我们很容易想到的方案是:

共享一个变量,这个变量可以是个队列。

于是Demo改造如下:

    fun testChannel2() {
        //阻塞队列
        var queue = ArrayBlockingQueue<String>(5)
        //协程1
        GlobalScope.launch {
            var count = 0
            while (true) {
                //假装在加工数据
                Thread.sleep(1000)
                queue.put("fish ${count++}")
            }
        }

        //协程2
        GlobalScope.launch {
            while (true) {
                Thread.sleep(1000)
                println("get result from coroutine1:${queue.take()}")
            }
        }
    }

通过阻塞队列,当协程2取数据时,如果队列是空,那么等待协程1往队列里放数据;当协程1放数据时,如果队列满了,那么等待协程2从队列里取出数据。如此,就是简单的协程通信。
看似美好,实际上此处有个很大的漏洞:

队列满/队列空 时,此时等待动作阻塞的是线程,而我们知道协程的挂起并不阻塞线程,因此此种方式并没有利用到协程的优势。

我们期望协程发现队列满/空时将自己挂起等待,此时就引入了Channel。

升级版协程间通信-Channel

同样的需求,我们用Channel 实现:

    fun testChannel3() {
        //定义Channel
        var channel = Channel<String>()
        //协程1
        GlobalScope.launch {
            var count = 0
            while (true) {
                //假装在加工数据
                Thread.sleep(1000)
                var sendStr = "fish ${count++}"
                println("send $sendStr")
                channel.send("$sendStr")
            }
        }

        //协程2
        GlobalScope.launch {
            while (true) {
                Thread.sleep(1000)
                println("receive:${channel.receive()}")
            }
        }
    }

与之前的实现方案相比,仅仅只是将队列换成了Channel,可以看出,Channel 和队列比较类似,而Channel的send/recevie 函数并没有阻塞线程,仅仅只是挂起了协程。
查看打印结果:

你可能发现了端倪:发送者和接收者是成对出现的,难道Channel的内部实现不是队列?
要想解开这个谜题,最好的方法是从源码入手深究其原理。

2. Channel的原理

Channel的构造

先从Channel 构造开始:

#Channel.kt
public fun <E> Channel(
    //Channel 容量/叫做Channel类型更合理一些
    capacity: Int = Channel.RENDEZVOUS,
    //缓冲区满后,发送端的处理方式
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    //信息没有传递出去时的回调
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
    when (capacity) {
        //默认是约会模式
        Channel.RENDEZVOUS -> {
            //默认挂起
            if (onBufferOverflow == BufferOverflow.SUSPEND)
                RendezvousChannel(onUndeliveredElement) // an efficient implementation of rendezvous channel
            else
                ArrayChannel(1, onBufferOverflow, onUndeliveredElement) // support buffer overflow with buffered channel
        }
        //...
    }

此处的Channel() 并不是构造函数,而是顶层函数,Kotlin里有很多伪装为构造函数的顶层函数。该顶层函数默认构造并返回RendezvousChannel类型的Channel。
RendezvousChannel 类本身很简单,就重写了一些属性,它继承自AbstractChannel。

重点在AbstractChannel/AbstractSendChannel及其子类里。

Channel的队列结构

AbstractSendChannel 里有个很重要的成员变量:

    protected val queue = LockFreeLinkedListHead()

LockFreeLinkedListHead 继承自LockFreeLinkedListNode,而这个Node 我们在分析Kotlin 协程之取消与异常处理探索之旅(上) 有提及过,此处再拎出来说说。
先看其定义:

#LockFreeLinkedList.kt
public actual open class LockFreeLinkedListNode {
    //后驱指针
    private val _next = atomic<Any>(this) // Node | Removed | OpDescriptor
    //前驱指针
    private val _prev = atomic(this) 
    //...
}

很典型的一个链表结构,并且是无锁链表,意思是它的插入/删除是无需上锁的,核心是使用了CAS。 回到Channel里的成员变量queue,初始的链表结构如下:

可以看出,当前节点的_next、_prev分别指向自己。

当往链表里面添加Node时,形成如下结构:

链表头为固定节点,通过它构造了双向循环链表。
AbstractSendChannel 里的queue 就是个链表头,通过它我们可以快速找到链表里的第1个节点(_next 指向的节点),也可以快速找到链表的最后一个节点(_prev指向的节点)。
于是形成了一个队列结构,每次往队列里放入数据,就放到链表的尾部,每次从队列里取数据,就从链表头后的第一个节点取。

Channel的send/receive

send 分析

#AbstractChannel.kt
    public final override suspend fun send(element: E) {
        //快速判断是否可以放入queue 队列
        //若能成功,则直接返回
        if (offerInternal(element) === kotlinx.coroutines.channels.OFFER_SUCCESS) return
        //不能退出,则挂起协程
        return sendSuspend(element)
    }

    protected open fun offerInternal(element: E): Any {
        while (true) {
            //先找到队列第一个Node节点,如果存在并且是Receive 类型,说明有接收者在等待
            val receive = takeFirstReceiveOrPeekClosed() ?: return OFFER_FAILED
            //给接收者协程赋值
            val token = receive.tryResumeReceive(element, null)
            if (token != null) {
                kotlinx.coroutines.assert { token === RESUME_TOKEN }
                //重新恢复接收者协程
                receive.completeResumeReceive(element)
                //返回插入的结果
                return receive.offerResult
            }
        }
    }

    private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont ->
        //suspendCancellableCoroutineReusable 里有挂起协程的逻辑
        loop@ while (true) {
            if (isFullImpl) {
                //构造SendElement,它是Node类型 
                val send = if (onUndeliveredElement == null)
                    //SendElement 有两个成员变量:1是具体的值,2是当前协程的封装体cont
                    SendElement(element, cont) else
                    SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement)
                //将Element 加入到队列尾部
                val enqueueResult = enqueueSend(send)
                //插入成功,则返回
            }
        }
    }

用图表示以上流程:

接收者协程被恢复后,重新调度执行协程,而传入的值即为send发送的值,最终recevie返回的即是send过来的值。
对协程的挂起有疑惑请移步:讲真,Kotlin 协程的挂起没那么神秘(原理篇)

receive 分析
与send流程类似,就不贴代码了,仅用图表示:

可以看出,send/receive 通过判断queue的状态来决定是否挂起当前协程,而queue里的Node 又分为三种类型:

综合以上得出:

在RENDEZVOUS类型(默认类型)下,发送者协程需要等待接收者就位了(到队列里等待)才会继续往下走。同样的,接收者协程需要等待发送者就位了(到队列里等待)才会继续往下走。因此,形成的现象是发送者/接收者成对出现。

成对出现的场景,我们称RENDEZVOUS 为约会类型。

3. Channel四种类型深入解析

CONFLATED 类型

前面的分析是基于约会类型,实际上Channel还有其它类型,通过其构造过程可看出:

#AbstractChannel.kt
public fun <E> Channel(
    capacity: Int = Channel.RENDEZVOUS,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,
    onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =
    when (capacity) {
        //约会类型
        Channel.RENDEZVOUS -> {
            if (onBufferOverflow == BufferOverflow.SUSPEND)
                RendezvousChannel(onUndeliveredElement) // an efficient implementation of rendezvous channel
            else
                //转为缓冲类型
                ArrayChannel(1, onBufferOverflow, onUndeliveredElement) // support buffer overflow with buffered channel
        }
        //混合类型
        Channel.CONFLATED -> {
            //此种类型下必须是挂起模式
            require(onBufferOverflow == BufferOverflow.SUSPEND) {
                "CONFLATED capacity cannot be used with non-default onBufferOverflow"
            }
            ConflatedChannel(onUndeliveredElement)
        }
        //无限制类型
        Channel.UNLIMITED -> LinkedListChannel(onUndeliveredElement) // ignores onBufferOverflow: it has buffer, but it never overflows
        //缓冲类型
        Channel.BUFFERED -> ArrayChannel( // uses default capacity with SUSPEND
            if (onBufferOverflow == BufferOverflow.SUSPEND) Channel.CHANNEL_DEFAULT_CAPACITY else 1,
            onBufferOverflow, onUndeliveredElement
        )
        //没有指定具体类型是以上4种内的组合
    }

先看CONFLATED(混合)类型。
ConflatedChannel 继承自AbstractChannel,有个成员变量:value。
重点来看其重写的函数:offerInternal与pollInternal,分别对应send与receive的逻辑。

send 分析

#ConflatedChannel.kt
protected override fun offerInternal(element: E): Any {
    var receive: ReceiveOrClosed<E>? = null
    //先上锁
    lock.withLock {
        //如果value 为空,也就是之前没有发送过,说明可能有接收者在等待。
        if (value === kotlinx.coroutines.channels.EMPTY) {
            loop@ while(true) {
                //尝试取出接收者
                receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued
                if (receive is Closed) {
                    //是关闭Node,直接返回
                    return receive!!
                }
                //赋值给接收者协程
                val token = receive!!.tryResumeReceive(element, null)
                if (token != null) {
                    //跳出锁
                    return@withLock
                }
            }
        }
        //更新发送值到value里
        updateValueLocked(element)?.let { throw it }
        //成功插入
        return OFFER_SUCCESS
    }
    //如果找到接收者,则恢复接收者协程
    receive!!.completeResumeReceive(element)
    return receive!!.offerResult
}

receive 分析

#ConflatedChannel.kt
protected override fun pollInternal(): Any? {
    var result: Any? = null
    //上锁
    lock.withLock {
        //如果value 为空,说明没数据,取数据失败
        if (value === kotlinx.coroutines.channels.EMPTY) return closedForSend ?: POLL_FAILED
        //从value 里取数据
        result = value
        //恢复到无数据状态
        value = EMPTY
    }
    return result
}

由此可见:

在 CONFLATED类型下,发送者无需等待接收者就位,它可以一直更新数据。

BUFFERED 类型

此为缓冲类型,与其它类型最大的不同之处在于它内部有数据缓冲区。
ArrayChannel 继承自AbstractChannel,其成员变量:

//数据缓冲区
    private var buffer: Array<Any?> = arrayOfNulls<Any?>(min(capacity, 8)).apply { fill(EMPTY) }

重点函数还是offerInternal与pollInternal。
send 分析

#ArrayChannel.kt
protected override fun offerInternal(element: E): Any {
    var receive: ReceiveOrClosed<E>? = null
    lock.withLock {
        //size 为buffer 当前的实际存储数据的个数
        val size = this.size.value
        //更新size,此处根据发送策略,有可能会直接退出
        updateBufferSize(size)?.let { return it }
        if (size == 0) {
            //当前缓冲区没有数据
            loop@ while (true) {
                //查看是否有接收者等待
                receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued
                //给接收者协程赋值
                val token = receive!!.tryResumeReceive(element, null)
                if (token != null) {
                    //缓冲区数量不变
                    this.size.value = size
                    return@withLock
                }
            }
        }
        //加入到缓冲队列
        enqueueElement(size, element)
        //插入成功
        return OFFER_SUCCESS
    }
    //恢复接收者协程
    receive!!.completeResumeReceive(element)
    return receive!!.offerResult
}

private fun updateBufferSize(currentSize: Int): Symbol? {
    if (currentSize < capacity) {
        //还可以继续存放数据
        size.value = currentSize + 1 // tentatively put it into the buffer
        return null // proceed
    }
    //缓冲区满
    return when (onBufferOverflow) {
        //协程需要挂起
        BufferOverflow.SUSPEND -> OFFER_FAILED
        //舍弃最新数据,相当于发送永远是成功的
        BufferOverflow.DROP_LATEST -> OFFER_SUCCESS
        //舍弃旧的数据,发送继续走下面的流程
        BufferOverflow.DROP_OLDEST -> null // proceed, will drop oldest in enqueueElement
    }
}

receive 分析
receive 过程与send类似,就不贴源码了,直接上图对比:

可以看出,对于发送者来说:

先将数据放入数据缓冲队列,当缓冲区满后才会考虑是否需要挂起发送者协程。同样的,对于接收者来说,先从缓冲队列取数据,当缓冲区没数据时才会挂起自身。

UNLIMITED 类型

此类型为无限制类型,网上一些文章将此与BUFFERED类型类比,并归为“无限缓冲类型”,该说法是否正确,接下来一步步印证。
同样的,LinkedListChannel继承自AbstractChannel。
重点函数还是offerInternal与pollInternal。
send 分析

#LinkedListChannel.kt
protected override fun offerInternal(element: E): Any {
    while (true) {
        //快速查找是否有接收者等待
        val result = super.offerInternal(element)
        when {
            //找到接收者,插入算是成功
            result === OFFER_SUCCESS -> return OFFER_SUCCESS
            //没找到
            result === OFFER_FAILED -> {
                //加入到协程缓冲队列
                when (val sendResult = sendBuffered(element)) {
                    null -> return OFFER_SUCCESS
                    is Closed<*> -> return sendResult
                }
            }
        }
    }
}

protected fun sendBuffered(element: E): ReceiveOrClosed<*>? {
    //将SendBuffered 加入到queue里(队尾)
    queue.addLastIfPrev(AbstractSendChannel.SendBuffered(element)) { prev ->
        if (prev is ReceiveOrClosed<*>) return@sendBuffered prev
        true
    }
    //添加成功
    return null
}

receive 分析 receive 过程完全依靠父类AbstractChannel完成,此处就不再赘述,用图表示:

可以看出:

此类型下,发送者不会挂起,会一直往队列里存放数据,理论上是可以无限制存放的。与BUFFERED类型不同的是,UNLIMITED 缓冲数据使用的是queue,它是链表。而BUFFERED 缓冲数据使用的是数组。

Channel 四种类型比对

对于接收者来说,只有一种逻辑:

有数据则消费数据,没数据则挂起等待。

4. produce/actor的使用与原理

使用

通过上面的分析,我们知道接收者有可能会阻塞,怎样才能让接收者知道数据已经发送完毕了呢?
答案是:Channel.close()。
当调用该函数时,会往queue里加入Closed节点,当send/receive 取出该节点时就知道Channel关闭了。
你说,能不能不手动调用该函数呢?刚好,produce可以解决该问题:

    fun testProduce() {
        //返回接收者
        var receiveChannel = GlobalScope.produce<String> {
            //
            for (x in 1..5) {
                var sendStr = "fish $x"
                println("send $sendStr")
                send("$sendStr")
            }
        }
        //接收数据
        GlobalScope.launch {
            while (true) {
                println("job2 receive:${receiveChannel.receive()}")
            }
            println("job2 end")
        }
        GlobalScope.launch {
            while (true) {
                println("job3 receive:${receiveChannel.receive()}")
            }
            println("job3 end")
        }
    }

produce 函数返回Channel,在produce的协程体里可以发送数据,而通过返回的Channel,其它协程可以接收数据。当produce协程执行完毕后,将会主动调用close关闭Channel,其它Receive的Channel就会有感知,从而退出挂起状态。
这是一个典型的单生产者–多消费者的模型。
反之单消费者–多生产者的模型如下:

    fun testActor() {
        //返回发送者
        var sendChannel = GlobalScope.actor<String> {
            //
            for (x in 1..5) {
                println("job1 receive:${receive()}")
            }
            println("actor end")
        }
        //发送者
        GlobalScope.launch {
            sendChannel.send("send from job2")
        }
        GlobalScope.launch {
            sendChannel.send("send from job3")
        }
    }

原理

produce和actor内部创建了RENDEZVOUS 类型的Channel,它们返回的Channel以及协程体里的Channel都是委托这个内部的Channel来完成功能的,并且Channel绑定了协程的生命周期,当协程取消时将会取消Channel。(由于篇幅原因,就不展开细说了,有兴趣可以自行阅读源码或是留言)。

下篇将着重分析Flow以及与LiveData的PK。

本文基于Kotlin 1.5.3,文中完整Demo请点击

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

作者:小鱼人爱编程
链接:https://juejin.cn/post/7139468247119691807
更多Android学习笔记+视频资料可点击下方优快云官方认证卡片👇

Allegro17.2 出gerber文件详细流程 - 干货 阅读详情

相关推荐

Springboot信息泄露以及heapdump的利用

努力是为了不平庸~安全有些时候是枯燥的,这一次,让我们先人一步,趣学渗透!

LiBai'S BLOG 3万+

【深入浅出Node.js系列九】一起撸Node.js

为什么80%的码农都做不了架构师?>>> ...

weixin_34241036的博客 585

三维重建之条纹投影结构光(三)——相位展开优化与多频外差实践

本文深入探讨了三维重建中条纹投影结构光技术的核心环节——相位展开优化。针对传统路径跟踪法存在的误差传播与路径依赖问题,重点解析了多频外差法的原理与优势,并通过详细的C++代码实践,展示了如何利用三频外差法实现鲁棒、高精度的相位展开,从而获得连续的绝对相位图,为后续三维点云生成奠定坚实基础。

weixin_29074295的博客 529

面试题分享网站在程序员行业,找师傅跟找对象差不多,难道Python真的凉了

拿个最简单的例子,我带徒弟的时候,我们就喜欢一起撸点小串、喝点小酒,吹吹牛逼,真正的良师也是你最好的朋友,取决于三观合不合得来,有项目一起会带着徒弟一起,感兴趣的领域我先领头,咱们一起钻研,一起进步。有很多小伙伴都喜欢找行业内的大佬当老师或者当师傅,这里我不得不提醒一下这种小伙伴,大佬其实都是反社交的,真正厉害的程序员,生活都是简单、有规律的,因为他们需要更专注、更有效率,所以你去找大佬当师傅是没有多大意义的。一个好的师傅会决定你至少三到五年的幸福指数,所以在找师傅的时候,还是有很多点需要你额外注意的,

789

记录思科cisco交换机升级iso步骤

Cisco Nexus 93180交换机版本升级

a635161812的博客 4376

【实用运维脚本】Windows 批量 TCP 端口检测(tcping 一键巡检)

摘要:本文介绍一款高效的Windows批量TCP端口检测脚本,适配tcpingv0.39工具,实现多服务器一键巡检。脚本具有进度可视化、双日志输出、自动容错等优势,支持注释行过滤和UTF-8编码。 使用仅需三步: 1)下载tcping工具 2)编辑目标列表 3)运行脚本即可自动检测并生成带编号的检测报告。 该方案解决了手动检测效率低、易遗漏的问题,特别适合日常运维中的端口连通性检查,实测稳定可靠,开箱即用。

蕲士|D365 & 服务器运维,心有东坡,行稳致远。 797

手撸一个一起看电影应用-4-Python脚本后端信息收发

一起看电影应用-4-Python脚本后端信息收发

Samle的博客 7320

实战指南:如何用DCCRN模型提升语音增强效果(附代码示例)

本文提供了基于DCCRN模型进行语音增强的实战指南。通过深入解析复数网络如何协同处理语音的幅度与相位信息,并附有完整的PyTorch代码实现,帮助开发者从数据准备、模型搭建到训练调优,构建高效的实时语音增强系统,有效提升嘈杂环境下的语音清晰度。

weixin_29054245的博客 478

当,Kotlin Flow与Channel相逢

之前的文章已经分析了Flow的相关原理与简单使用,Flow之所以用起来香,Flow便捷的操作符功不可没,而想要熟练使用更复杂的操作符,那么需要厘清Flow和Channel的关系。

840

BGP路由聚合:自动聚合和手动聚合

路由聚合是指将多条具体的路由合并为一条聚合路由,以减少路由表的条目数。这种技术通过只向对等体发送聚合后的路由,而不发送所有的具体路由,来减小路由表的规模。同时,被聚合的路由如果发生路由振荡,也不再对网络造成影响,从而提高了网络的稳定性。

网络技术联盟站 1219

医学图像处理实战:利用OpenSlide解析病理SVS切片的完整解决方案

本文介绍了使用openslide处理病理SVS切片的完整流程,从基础安装到科研应用。主要内容包括:1)SVS切片特点及openslide工具简介;2)环境配置与软件安装;3)基础操作:读取切片、查看信息、可视化不同层级图像;4)进阶操作:坐标裁剪与批量处理;5)科研方向拓展:结合AI模型进行病理分割分类、量化分析及可视化报告。教程提供了完整的代码示例,帮助毕业设计学生快速掌握病理图像分析的核心技能。

m0_71589036的博客 308

骚年~来一起撸一撸,这些面试必问的知识

长按上方小姐姐。

芋道源码 6万+

Android Camera MTK框架

Camera简介: 虽然前文说道Android在5.0之后就推出了Camera2 API,但是各个厂家的基本未适配,导致目前市场上大部分机型使用的仍然是Camera1 API。PS:Android 9.0 Google强制要求各个平台厂商(高通/MTK/华为等)支持Camera2,不再兼容Camera1。 ...

weixin_49303682的博客 5506

撸一撸Spring Framework-IoC-BeanDefinition

BeanFactory会从配置元信息中加载beanDefinition,并将其注册到BeanDefinitionRegistry中,之后创建、管理bean的工作,都要依赖于beanDefinition。容器启动过程中加载beanDefinition,注册beanDefinition,从容器获取bean时(getBean),容器如何使用beanDefinition完成bean的创建工作

wb_snail的博客 4万+

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

手撸一个一起看电影应用-3-Vue前端信息收发

一起看电影应用-3-Vue前端信息收发

Samle的博客 3128

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

手撸一个一起看电影应用-1-设计思路

movie together,设计一个可以远程一起看电影的应用

Samle的博客 2730

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

撸一撸Spring Framework-IoC-概述

Spring Framework中最核心的模块就是IoC容器(Inversion of Control,控制反转),其他模块如aop、aspectjs、Data Access等都在此基础上展开,我们就从IoC开始旅程 IoC也称为依赖注入(DependeccyInjection,DI),注意,这二者是替代关系,并不是网上广泛传播的、极具迷惑性的"思想和实现"的关系

wb_snail的博客 7万+

一起撸Dart语言基础

语法基础: 使用 var 声明一个变量 使用 final 声明一个只能赋值一次的变量 使用 const 声明一个常量 var bb=123; final s=123; const ss=123123; 一、Dart中的数据类型 1. 数值型-Number 分为2种类型 1.1 可以是任意类型:num 因为num是int和double的父类 1.2 整型 :int 1.3 浮点型: d...

One X的博客 2087

撸一撸多线程的来龙去脉你就知道,为什么面试官折磨喜欢问这种问题了!

撸一撸多线程的来龙去脉你就知道,为什么面试官折磨喜欢问这种问题了! 有一天张三去面试,被面试官问了一个问题: ​ 什么叫多线程? 张三:多线程就是多个线程在cpu上执行,抢占Cpu资源,提高CPU执行效率… 面试官:你在说什么?你懂什么?你到底学了什么?回去吧你!!! 张三很懊恼,出来的时候在路上直接一把抢了大妈的**《java从入门到放弃》**,一回去就把java多线程复习了一遍。 一、认识多线程 1.多任务 2.多线程 3.程序、进程、线程 程序:指令和数据的有序集合,其本身没有任何运行含义,是

二叉树的博客 7513

线程池没你想的那么简单(续)

前言 前段时间写过一篇《线程池没你想的那么简单》,和大家一起撸了一个基本的线程池,具备: 线程池基本调度功能。 线程池自动扩容缩容。 队列缓存线程。 关闭线程池。 这些功能,最后也留下了三个待实现的 features 。 执行带有返回值的线程。 异常处理怎么办? 所有任务执行完怎么通知我? 这次就实现这三个特性来看看 j.u.c 中的线程池是如何实现这些需求的。 再看本文之前,强烈建议先...

weixin_34176694的博客 246

04.视频播放器通用架构实践

04.视频播放器通用架构实践 目录介绍 01.视频播放器的痛点 02.业务需求的目标 03.该播放器框架特点 04.播放器内核封装 05.播放器UI层封装 06.如何简单使用 07.如何自定义播放器 08.该案例的拓展性分享 09.关于视频缓存方案 10.如何监控视频埋点 11.待实现的需求分析 12.一些细节上优化 13.参考案例和博客记录 00.视频播放器通用框架 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer

杨充 2376

撸一撸Spring Framework-IoC-ApplicationContext

从UML关系上看,ApplicationContext与BeanFactory既有继承关系、又有关联关系。很多ApplicationContext的实现中,都引用了一个DefaultListableBeanFactory对象,并将bean管理操作都委托给它。以下是从GenericApplicationContext及其父类中摘录的一段代码,主要是为了让你感受下它与DefaultListableBeanFactory的关系

wb_snail的博客 1万+

撸大师SDK V1.0 iOSAPP永久后台运行

今天2019年1月已经更新了SDK,新的SDK修复已知问题,并包括常驻后台运行和杀死进程后自动复活的功能,请到这里下载 https://blog.youkuaiyun.com/wuhengsi620/article/details/73297300   撸大师SDK V1.0 功能:        突破苹果限制,令iOS APP进入后台或锁屏的情况下仍然能永久后台运行(事务、代码),从而让APP能够实...

撸大师的博客 88万+

使用Beego撸了一个社区

直接放链接 霓虹灯下社区Github

chen15920142402的博客 4万+

一起撸个朋友圈吧 图片浏览(上)【图片点击前景色】

项目地址:github.com/razerdp/Fri… (能弱弱的求个star或者fork么QAQ) 上篇链接:一起撸个朋友圈吧 (Step6)- 评论对齐(点击评论对齐)【下】 下篇链接:一起撸个朋友圈吧 - 图片浏览(中)【图片浏览器】 前言 终于,这个系列的文章进入了全新的篇章,咱们的朋友圈系列进入研磨阶段,目前我们可以成功展示数据,可以进行点赞或者取消点赞,可以进行评...

weixin_33889665的博客 2387

保险公司还有哪些业务,一起撸一下?

本文深入探讨了保险公司的人身保险与财产保险两大核心业务,通过具体场景、案例及业务逻辑分析,展示了保险公司在风险管理与资金配置中的重要作用。技术研发人员可借此深入理解保险业务,以更好地将技术应用于保险领域。

技术铺掌柜 1181
上一篇: 五年 Java一朝转为 Kotlin,这份Kotlin协程入门指南太详细了
下一篇: 2022最新【Android Framework开发解密】腾讯技术团队出品,必属精品
Agent学习路线
博客等级 码龄6年 1万+粉丝 1902原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值