关于线程:深入讲解并发中最致命的死锁

本文首发自[慕课网](imooc.com) ,想理解更多IT干货内容,程序员圈内热闻,欢送关注"慕课网"及“慕课网公众号”!作者:王军伟Tech|慕课网讲师 1. 前言本节内容次要是对死锁进行深刻的解说,具体内容点如下: 了解线程的上下文切换,这是本节的辅助根底内容,从概念层面进行了解即可; 理解什么是线程死锁,在并发编程中,线程死锁是一个致命的谬误,死锁的概念是本节的重点之一; 理解线程死锁的必备 4 因素,这是防止死锁的前提,理解死锁的必备因素,能力找到防止死锁的形式; 把握死锁的实现,通过代码实例,进行死锁的实现,深刻领会什么是死锁,这是本节的重难点之一; 把握如何防止线程死锁,咱们可能实现死锁,也能够防止死锁,这是本节内容的外围。 2. 了解线程的上下文切换概述: 在多线程编程中,线程个数个别都大于 CPU 个数,而每个 CPU 同一时-刻只能被一个线程应用,为了让用户感觉多个线程是在同时执行的, CPU 资源的调配采纳了工夫片轮转的策略,也就是给每个线程调配一个工夫片,线程在工夫片内占用 CPU 执行工作。 定义: 以后线程应用完工夫片后,就会处于就绪状态并让出 CPU,让其余线程占用,这就是上下文切换,从以后线程的上下文切换到了其余线程。 问题点解析: 那么就有一个问题,让出 CPU 的线程等下次轮到本人占有 CPU 时如何晓得本人之前运行到哪里了?所以在切换线程上下文时须要保留以后线程的执行现场, 当再次执行时依据保留的执行现场信息复原执行现场。 线程上下文切换机会: 以后线程的 CPU 工夫片应用完或者是以后线程被其余线程中断时,以后线程就会开释执行权。那么此时执行权就会被切换给其余的线程进行工作的执行,一个线程开释,另外一个线程获取,就是咱们所说的上下文切换机会。 3. 什么是线程死锁定义:死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的相互期待的景象,在无外力作用的状况下,这些线程会始终互相期待而无奈持续运行上来。 如上图所示死锁状态,线程 A 己经持有了资源 2,它同时还想申请资源 1,可是此时线程 B 曾经持有了资源 1 ,线程 A 只能期待。 反观线程 B 持有了资源 1 ,它同时还想申请资源 2,然而资源 2 曾经被线程 A 持有,线程 B 只能期待。所以线程 A 和线程 B 就因为互相期待对方曾经持有的资源,而进入了死锁状态。 4. 线程死锁的必备因素 互斥条件:过程要求对所调配的资源进行排他性管制,即在一段时间内某资源仅为一个过程所占有。此时若有其余过程申请该资源,则申请过程只能期待;不可剥夺条件:过程所取得的资源在未应用结束之前,不能被其余过程强行夺走,即只能由取得该资源的过程本人来开释(只能是被动开释,如 yield 开释 CPU 执行权);申请与放弃条件:过程曾经放弃了至多一个资源,但又提出了新的资源申请,而该资源已被其余过程占有,此时申请过程被阻塞,但对本人已取得的资源放弃不放;循环期待条件:指在产生死锁时,必然存在一个线程申请资源的环形链,即线程汇合 {T0,T1,T2,…Tn}中的 T0 正在期待一个 T1 占用的资源,T1 正在期待 T2 占用的资源,以此类推,Tn 正在期待己被 T0 占用的资源。 ...

May 31, 2023 · 3 min · jiezi

关于线程:创建线程时实现hello-thread-具体用什么代码表示呀

实现hello thread 具体用代码示意为 public class HelloThread extends Thread!public void run()( Systen.cut.printin("Hello from a thread!"): }public statie vold main(string argsl)( (new HelloThreadi)).start(;) I残缺内容请点击下方链接查看:https://developer.aliyun.com/ask/463629?groupCode=learning 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

April 24, 2023 · 1 min · jiezi

关于线程:容易忽视的细节Log4j-配置导致的零点接口严重超时

作者:vivo 互联网服务器团队- Jiang Ye本文具体的记录了一次0点接口重大超时的问题排查经验。本文以作者本身视角极具代入感的描述了从问题定位到具体的问题排查过程,并通过根因剖析并最终解决问题。整个过程须要清晰的问题排查思路和丰盛的问题解决教训,也离不开公司弱小的调用链、和全方位的系统监控等基础设施。 一、问题发现我所负责的商城流动零碎用于承接公司线上官网商城的营销流动,最近忽然收到凌晨0点的服务超时告警。 营销流动类的零碎有如下特点: 营销流动个别会0点开始,如红包雨、大额优惠券抢券等。日常营销流动的机会刷新,如每日工作,每日签到,每日抽奖机会的刷新等。营销流动的利益刺激会吸引大量实在用户及黑产前来参加流动,所以流量在0点会迎来一波小顶峰,也正因如此线上偶现的服务超时告警起初并未引起我的留神。然而接下来的几天,每天的凌晨0点都会收到服务超时告警,这引起了我的警觉,决定一探到底。 二、问题排查首先通过公司的利用监控零碎查看了0点前后每分钟各接口的P95响应工夫。如下图所示,接口响应工夫在0点时刻最高达到了8s。持续查看锁定耗时最高的接口为商品列表接口,上面就针对这个接口开展具体的排查。 2.1 排查思路正式排查之前,先和大家分享下我对接口超时问题的排查思路。下图是一个简化的申请流程。 用户向利用发动申请应用服务进行逻辑解决应用服务通过RPC调用上游利用以及进行数据库的读写操作 服务超时可能是应用服务本身慢导致,也可能上游依赖响应慢导致。具体排查思路如下: 2.1.1 上游依赖慢服务排查(1)通过调用链技术定位上游依赖中的慢服务 调用链技术是实现零碎可观测性的重要保障,常见的开源计划有ziplin、pinpoint等。残缺的调用链能够按工夫正序记录每一次上游依赖调用的耗时,如rpc服务调用、sql执行耗时、redis拜访耗时等。因而应用调用链技术能够迅速定位到上游依赖的慢服务,如dubbo接口拜访超时、慢SQL等。但现实很饱满,事实很骨感。因为调用链路信息的数量过大,想要收集全量的链路信息须要较高的存储老本和计算资源。因而在技术落地时,通常都会采纳抽样的策略来收集链路信息。抽样带来的问题是申请链路信息的失落或不残缺。 (2)无调用链时的慢服务排查 如果调用链失落或不残缺,咱们就要再联合其它伎俩进行综合定位了。 上游RPC服务响应超时:如果是用Dubbo框架,在provider响应超时时会打印timeout相干日志;如果公司提供利用监控,还能够查看上游服务P95响应工夫综合判断。 慢SQL:MySQL反对设置慢SQL阈值,超过该阈值会记录下慢SQL;像咱们罕用的数据库连接池Druid也能够通过配置打印慢SQL日志。如果确认申请链路中存在慢SQL能够进一步剖析该SQL的执行打算,如果执行打算也没有问题,再去确认在慢SQL产生时mysql主机的零碎负载。 上游依赖蕴含Redis、ES、Mongo等存储服务时,慢服务的排查思路和慢SQL排查类似,在此不再赘述。 2.1.2 利用本身问题排查(1)应用逻辑耗时多 应用逻辑耗时多比拟常见,比方大量对象的序列化和反序列化,大量的反射利用等。这类问题的排查通常要从剖析源码动手,编码时应该尽量避免。 (2)垃圾回收导致的进展(stop the world) 垃圾回收会导致利用的进展,特地是产生Old GC或Full GC时,进展显著。不过也要看利用选配的垃圾回收器和垃圾回收相干的配合,像CMS垃圾回收器通常能够保障较短的工夫进展,而Parallel Scavenge垃圾回收器则是谋求更高的吞吐量,进展工夫会绝对长一些。 通过JVM启动参数-XX:+PrintGCDetails,咱们能够打印具体的GC日志,借此能够察看到GC的类型、频次和耗时。 (3)线程同步阻塞 线程同步,如果以后持有锁的线程长时间持有锁,排队的线程将始终处于blocked状态,造成服务响应超时。能够通过jstack工具打印线程堆栈,查找是否有处于block状态的线程。当然jstack工具只能采集实时的线程堆栈信息,如果想要查看历史堆栈信息个别须要通过Prometheus进行收集解决。 2.2 排查过程上面依照这个排查思路进行排查。 2.2.1 上游依赖慢服务排查(1)通过调用链查看上游慢服务 首先到公司的利用监控平台上,筛选出0点前后5min的调用链列表,而后依照链路耗时逆序排列,发现最大接口耗时7399ms。查看调用链详情,发现上游依赖耗时都是ms级别。调用链因为是抽样采集,可能存在链路信息失落的状况,因而须要其余伎俩进一步排查上游依赖服务。 (2)通过其余伎俩排查上游慢服务 接着我查看了0点前后的系统日志,并没有发现dubbo调用超时的状况。而后通过公司的利用监控查看上游利用P95响应工夫,如下图,在0点时刻,上游的一些服务响应时长的确会慢一些,最高的达到了1.45s,尽管对上游有肯定影响,但不至于影响这么大。 (3)慢SQL排查 接下来是慢SQL的排查,咱们零碎的连接池应用的是阿里开源的druid,SQL执行超过1s会打印慢SQL日志,查看日志核心也没有发现慢SQL的形迹。 到当初,能够初步排除因上游依赖慢导致服务超时,咱们持续排查利用本身问题。 2.2.2 利用本身问题排查(1)简单耗时逻辑排查 首先查看了接口的源码,整体逻辑比较简单,通过dubbo调用上游商品零碎获取商品信息,本地再进行商品信息的排序等简略的解决。不存在简单耗时逻辑问题。 (2)垃圾回收进展排查 通过公司利用监控查看利用的GC状况,发现0点前后没有产生过full GC,也没有产生过Old GC。垃圾回收进展的因素也被排除。 (3)线程同步阻塞排查 通过公司利用监控查看是否存在同步阻塞线程,如下图: 看到这里,终于有种天不负有心人的感觉了。从00:00:00开始始终到00:02:00,这两分钟里,呈现了较多状态为BLOCKED的线程,超时的接口大概率和这些blocked线程相干。咱们只须要进一步剖析JVM堆栈信息即可水落石出。 咱们随机选取一台比拟有代表性的机器查看block堆栈信息,堆栈采集工夫是2022-08-02 00:00:20。 // 日志打印操作,被线程catalina-exec-408阻塞"catalina-exec-99" Id=506 BLOCKED on org.apache.log4j.spi.RootLogger@15f368fa owned by "catalina-exec-408" Id=55204 at org.apache.log4j.Category.callAppenders(Category.java:204) - blocked on org.apache.log4j.spi.RootLogger@15f368fa at org.apache.log4j.Category.forcedLog$original$mp4HwCYF(Category.java:391) at org.apache.log4j.Category.forcedLog$original$mp4HwCYF$accessor$pRDvBPqB(Category.java) at org.apache.log4j.Category$auxiliary$JhXHxvpc.call(Unknown Source) at com.vivo.internet.trace.agent.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:46) at org.apache.log4j.Category.forcedLog(Category.java) at org.apache.log4j.Category.log(Category.java:856) at org.slf4j.impl.Log4jLoggerAdapter.info(Log4jLoggerAdapter.java:324) ... // 日志打印操作,被线程catalina-exec-408阻塞"catalina-exec-440" Id=55236 BLOCKED on org.apache.log4j.spi.RootLogger@15f368fa owned by "catalina-exec-408" Id=55204 at org.apache.log4j.Category.callAppenders(Category.java:204) - blocked on org.apache.log4j.spi.RootLogger@15f368fa at org.apache.log4j.Category.forcedLog$original$mp4HwCYF(Category.java:391) at org.apache.log4j.Category.forcedLog$original$mp4HwCYF$accessor$pRDvBPqB(Category.java) at org.apache.log4j.Category$auxiliary$JhXHxvpc.call(Unknown Source) at com.vivo.internet.trace.agent.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:46) at org.apache.log4j.Category.forcedLog(Category.java) at org.apache.log4j.Category.log(Category.java:856) at org.slf4j.impl.Log4jLoggerAdapter.warn(Log4jLoggerAdapter.java:444) ... // 日志打印操作,被线程catalina-exec-408阻塞"catalina-exec-416" Id=55212 BLOCKED on org.apache.log4j.spi.RootLogger@15f368fa owned by "catalina-exec-408" Id=55204 at org.apache.log4j.Category.callAppenders(Category.java:204) - blocked on org.apache.log4j.spi.RootLogger@15f368fa at org.apache.log4j.Category.forcedLog$original$mp4HwCYF(Category.java:391) at org.apache.log4j.Category.forcedLog$original$mp4HwCYF$accessor$pRDvBPqB(Category.java) at org.apache.log4j.Category$auxiliary$JhXHxvpc.call(Unknown Source) at com.vivo.internet.trace.agent.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:46) at org.apache.log4j.Category.forcedLog(Category.java) at org.apache.log4j.Category.log(Category.java:856) at org.slf4j.impl.Log4jLoggerAdapter.warn(Log4jLoggerAdapter.java:444) ...通过堆栈信息能够剖析出2点: ...

April 21, 2023 · 2 min · jiezi

关于线程:看完这篇线程线程锁与线程池讲解面试随便问

文:单线程——多线程的开启——线程锁——线程同步工具——手写连接池——连接池工具类。 一、线程1.线程的概念 2.线程与过程的关系 3.定义: 区别:如上!!! 4.wait()和sleep() 5.线程的状态及其他API 二、线程锁线程并发同步时,引入了锁机制。 一般锁机制:synchronized  润饰代码块与volatile  润饰成员变量 Lock!!共同点:都是从里面创立锁类、再把锁传到线程里对变量对象赋值。 (1)重入锁 (2)读写拆散锁 区别: 三、线程同步工具类!!共同点:都是从里面创立工具类、再把工具类的参数传到线程外面执行。 CountDowmLatch闭锁:期待所有线程执行完 CyclicBarrier栅栏:期待所有线程达到后开启 Exchanger交换机:交换数据 信号量(1)概念 (2)利用场景一 (3)利用场景二 四、线程池为什么应用线程池 线程池的外围队列阻塞式队列:只用于线程对象,次要用于引出线程池 手动创立线程池 Executors工具创立线程外围线程:0(长期线程)、1(队列)、N(队列) 定时线程: 五、彩蛋图

November 17, 2022 · 1 min · jiezi

关于线程:死锁产生的4个必要条件

死锁产生的4个必要条件1 . 产生死锁的必要条件:(1)互斥条件:过程要求对所调配的资源进行排它性管制,即在一段时间内某资源仅为一过程所占用。(2)申请和放弃条件:当过程因申请资源而阻塞时,对已取得的资源放弃不放。(3)不剥夺条件:过程已取得的资源在未应用完之前,不能剥夺,只能在应用完时由本人开释。(4)环路期待条件:在产生死锁时,必然存在一个过程–资源的环形链。解决死锁的根本办法 2 . 预防死锁:(1)资源一次性调配:一次性调配所有资源,这样就不会再有申请了:(毁坏申请条件)(2)只有有一个资源得不到调配,也不给这个过程调配其余的资源:(毁坏放弃条件)(3)可剥夺资源:即当某过程取得了局部资源,但得不到其它资源,则开释已占有的资源(毁坏不可剥夺条件)(4)资源有序调配法:零碎给每类资源赋予一个编号,每一个过程按编号递增的程序申请资源,开释则相同(毁坏环路期待条件)

October 18, 2022 · 1 min · jiezi

关于线程:线程基础知识

前言和纲要计算机系统里每个过程(Process)都代表着一个运行着的程序,过程是对运行时程序的封装,零碎进行资源调度和调配的根本单位。一个过程下能够有很多个线程,线程是过程的子工作,是CPU调度和分派的根本单位,用于保障程序的实时性,实现过程外部的并发,线程同时也是操作系统可辨认的最小执行和调度单位。在 Java 里线程是程序执行的载体,咱们写的代码就是由线程运行的。有的时候为了减少程序的执行效率,咱们不得不应用多线程进行编程,尽管多线程能最大化程序利用 CPU 的效率,但也是程序事变多发、程序员脱发的最大诱因。次要是平时咱们的思维默认是单线程的,写多线程的时候得能够切换一下才行,这就要求咱们对线程的基础知识理解的比拟透彻。这篇文章咱们总结一下 Java线程的根底,多把握点,当前就少掉点头发,不光省下植发的钱,工资还能往上涨,这么一想几乎双赢。本文的纲要如下: 好了让咱们开始明天的注释,间接上代码吧。Java 中的线程到目前为止,咱们写的所有 Java 程序代码都是在由JVM给创立的 Main Thread 中单线程里执行的。Java 线程就像一个虚构 CPU,能够在运行的 Java 应用程序中执行 Java 代码。当一个 Java 应用程序启动时,它的入口办法 main() 办法由主线程执行。主线程(Main Thread)是一个由 Java 虚拟机创立的运行你的应用程序的非凡线程。因为 Java 里所有皆对象,所以线程也是用对象示意的,线程是类 java.lang.Thread 类或者其子类的实例。在 Java 应用程序外部, 咱们能够通过 Thread 实例创立和启动更多线程,这些线程能够与主线程并行执行应用程序的代码。创立和启动线程在 Java 中创立一个线程,就是创立一个 Thread 类的实例 Thread thread = new Thread(); 启动线程就是调用线程对象的 start() 办法 thread.start(); 当然,这个例子没有指定线程要执行的代码,所以线程将在启动后立刻进行。指定线程要执行的代码有两种办法能够给线程指定要执行的代码。 第一种是,创立 Thread 的子类,笼罩父类的 run() 办法,在run() 办法中指定线程要执行的代码。第二种是,将实现 Runnable (java.lang.Runnable) 的对象传递给 Thread 构造方法,创立Thread 实例。 其实,还有第三种用法,不过细究下来可归类到第二种的非凡应用形式,上面咱们看看这三种的用法和区别。通过 Thread 子类指定要执行的代码通过继承 Thread 类创立线程的步骤: 定义 Thread 类的子类,并笼罩该类的 run() 办法。run() 办法的办法体就代表了线程要实现的工作,因而把 run 办法称为执行体。创立 Thread 子类的实例,即创立了线程对象。调用线程对象的 start 办法来启动该线程。 ...

July 1, 2022 · 2 min · jiezi

关于线程:专属云资源包计算规格探秘

咱们晓得专属云资源包有三种规格:36 核/321 GB 、44 核/380 GB、72 核/704 GB,很多人对资源包的规格不太了解,上面我带大家一步步进行分析。 首先咱们要晓得一个专属云计算资源包应着是一台物理机,订购一个资源包,就意味着独占一台物理机。三种规格中,36 核/321 GB对应着V3规格服务器 ,44 核/380 GB对应着V4规格服务器、72 核/704 GB对应着V5规格服务器。 既然是一台物理机,那么为何会有36 核/321 GB 、44 核/380 GB、72 核/704 GB这样奇怪的规格?还有,资源包中的核数到底指的是什么?之所以会有这么奇怪的规格,根本原因是当咱们将一台物理机纳入到云计算环境中,会存在虚拟化平台治理的须要,会吃掉一部分CPU和内存的资源,真正能给客户提供的计算资源就要减去这部分被吃掉的资源。 那么这些规格到底是怎么得来的,咱们能够通过给定的计算公式得出。在给出计算公式之前,须要明确资源包里的核其实指的是(超)线程核。 什么叫(超)线程核?一般来说一个CPU外围对应一个物理线程,但出于对多任务处理的须要,英特尔通过自家的超线程技术能够把一个物理线程模拟出两个逻辑线程来用,以充分发挥 CPU 性能,也就是说,单核心的CPU被模仿成了一个相似双核心CPU的性能,对应操作系统或者虚拟化平台来说,会把1个物理线程(外围)模拟出的2个逻辑线程当成2个CPU核看待。 专属云资源包核数(线程数)的计算公式如下:单计算节点可用线程数 = 单计算节点总线程数 – 单计算节点软件开销线程数单计算节点CPU总线程数 = CPU数量×CPU核数×CPU每核的线程数单计算节点软件开销线程数 = (物理服务器超线程数/10)(向上取偶数)+ FusionStorage Block开销  举例,对于V4通用计算类服务器,CPU规格为2路14核,超线程因子数为2,因而:单计算节点软件开销线程数 = 2x14x2/10(向上取偶)+ 6 = 6 + 6 = 12单计算节点可用CPU线程数 = 2x14x2 - 12 = 44哈,计算结果完全一致! 专属云资源包内存的计算公式如下:单计算节点可用内存 = 单计算节点总内存 – 单计算节点的虚拟化软件内存开销其中单计算节点的虚拟化软件内存开销,包含单计算节点的Domain0内存开销为56GB,Hypervisor治理开销为8GB,这样加起来就是64G。 举例,对于V4通用计算类服务器,总内存容量为14 x 32 = 448GB,因而对于单计算节点可用内存 = 448 - 64= 384GB*。*这里计算出的数据有4G的差别,阐明计算公式中的内存的额定开销也不齐全是固定值。通过以上的计算公式咱们能够再算一下采纳V5服务器的72 核/704 GB是怎么得来的。V5服务器的规格:222外围(英特尔至强金牌6161,超线程因子数为2),2432GB DIMM,2600G SAS,SR130,410GE NIC。单计算节点软件开销线程数 = 2x22x2/10(向上取偶)+ 6 = 10 + 6 = 16单计算节点可用CPU线程数 = 2x22x2 - 16 = 72单计算节点可用内存=24*32-64G=704G ...

April 2, 2022 · 1 min · jiezi

关于线程:全局变量和多线程

先抛出问题:有一全局变量,一个工作读,一个工作写,有没有问题?先抛出答案,不倡议这么做。 临界资源:各工作/线程采取互斥的形式,实现共享的资源称作临界资源。属于临界资源的硬件串口打印、显示等,软件有音讯缓冲队列、变量、数组、缓冲区等。多任务/线程间应采取互斥形式,从而实现对这种资源的共享。多任务/多线程状况下在写模块时,只须要封装进爱护机制即可。常见的爱护机制无关中断、信号量、互斥锁等。最近做的几个我的项目遇到的一些非凡场景: 1.校时定义全局变量 typedef struct{ /*应用软件工夫*/ unsigned int year; unsigned char month; unsigned char day; unsigned char hour; /*时 */ unsigned char minute; /*分 */ unsigned char second; /*秒 */ unsigned int millisecond; /*毫秒*/ unsigned int microsecond; /*微秒*/ unsigned int nanosecond; /*纳秒*/ unsigned int u40mic; int delta; /*日历时与本地RTC的差值*/ int delta_rtc; /*同步RTC与本地RTC的差值*/ int delta_year; /*用于操作系统年份保护*/ int timeElapse; /*零碎上电后运行了多少秒*/ char DateValid; /*年月日无效标识,1为无效*/ char TimeValid; /*时分秒无效标记,1为无效*/ char BusVaild; /*总线工夫无效, 1为无效, 用于飞参授时及文件夹创立*/}STRUCT_CORE_TIME;STRUCT_CORE_TIME gst_TimeState;int TIME_Regulate_Time(int year, int month, int date, int hour, int minute, int second){ struct timespec NewSetTime; struct tm DownTime; SYSTEM_TIME_TYPE sys_Time = 0; RETURN_CODE_TYPE retCode; if((year < 2000)||(year > 2255)) return -1; if((month < 1) || (month > 12)) return -2; if((date < 1) || (date>31)) return -3; if((hour > 23) || (minute > 59) || (second > 59)) return -4; /*当超出年限时,获取delta*/ if(year > 2100){ if(((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){ gst_TimeState.delta_year = year - 2096; year = 2096; }else{ gst_TimeState.delta_year = year - 2100; year = 2100; } }else{ gst_TimeState.delta_year = 0; } GET_TIME(&sys_Time, &retCode); DownTime.tm_hour = hour; DownTime.tm_min = minute; DownTime.tm_sec = second; DownTime.tm_mday = date; DownTime.tm_mon = month - 1; DownTime.tm_year = year - 1900; DownTime.tm_isdst = 0; /*设置为非夏令时*/ NewSetTime.tv_sec = mktime(&DownTime); /*工夫间接用delta保护,不波及零碎工夫*/ gst_TimeState.delta = NewSetTime.tv_sec - sys_Time / Time_1S; /*LOG4C日志零碎工夫更新*/ sd_settimeofday(gst_TimeState.delta); gst_TimeState.TimeValid = 1; gst_TimeState.DateValid = 1; return 0;}void TIME_Gather_Time(){ struct timespec NewSetTime; struct tm DownTime; SYSTEM_TIME_TYPE sys_Time = 0; RETURN_CODE_TYPE retCode; GET_TIME(&sys_Time, &retCode); NewSetTime.tv_sec = sys_Time / Time_1S + gst_TimeState.delta; localtime_r(&NewSetTime.tv_sec, &DownTime); gst_TimeState.year = DownTime.tm_year + 1900 + gst_TimeState.delta_year; gst_TimeState.month = DownTime.tm_mon + 1; gst_TimeState.day = DownTime.tm_mday; gst_TimeState.hour = DownTime.tm_hour; gst_TimeState.minute = DownTime.tm_min; gst_TimeState.second = DownTime.tm_sec; gst_TimeState.millisecond = sys_Time / 1000000 % 1000; gst_TimeState.microsecond = sys_Time / 1000 % 1000; gst_TimeState.nanosecond = sys_Time % 1000; gst_TimeState.u40mic = ((gst_TimeState.hour * 3600 + gst_TimeState.minute * 60 + gst_TimeState.second)*1000 + gst_TimeState.millisecond)*25;}校时线程做如下的周期调用: ...

March 7, 2022 · 2 min · jiezi

关于线程:如何利用线程堆栈定位问题

01 背景针对在一些服务中会呈现的cpu飙高、死锁、线程假死等问题,总结和提炼排查问题的思路和解决方案十分重要。上述问题会波及到线程堆栈,本文将结合实际案例来论述一下线程堆栈的性能。 02 基本知识2.1 什么是线程堆栈线程堆栈是零碎过后某个时刻的线程运行状态(即霎时快照)。 线程堆栈的信息蕴含 线程的名字、ID、线程的数量线程的运行状态、锁的状态(锁被那个线程持有,哪个线程再期待锁)<!----> 调用堆栈(即函数的调用档次关系)。调用堆栈蕴含残缺的类名,所执行的办法,源代码的行数2.2 线程堆栈能剖析问题类型线程堆栈定位问题只能定位在以后线程上留下痕迹的问题 2.3 线程堆栈不能剖析的问题线程堆栈不能定位在线程堆栈上不留痕迹的问题: 并发bug导致的数据凌乱,这种问题在线程堆栈中没有任何航迹,所以这种问题线程堆栈无奈提供任何帮忙。数据库锁表的问题,表被锁,往往因为某个事务没有提交/回滚,但这些信息无奈在堆栈中体现呈现在线程上不留痕迹的问题只能通过其余伎俩来进行定位。在理论的零碎中,零碎的问题分为几种类型: 在堆栈中可能体现出问题的,应用线程堆栈进行定位无奈在线程中留下痕迹的问题定位,须要依赖于一个好的日志设计十分荫蔽的问题,只能依赖于丰盛的代码教训,如多线程导致的数据凌乱,以及前面提到的幽灵代码2.4 如何输入线程堆栈 kill -3 pid 命令只能打印那一瞬间java过程的堆栈信息,适宜在服务器响应慢,cpu、内存疾速飙升等异常情况下应用,能够不便地定位到导致异样产生的java类,解决如死锁、连贯超时等起因导致的零碎异样问题。该命令不会杀死过程。 备注: 将屏幕输入写入到文件中,重写文件内容 将屏幕输入增加到文件开端Linux常用命令 命令形容ps查找过程的pidpstack打印过程或者线程的栈信息strace统计每一步零碎调用破费的工夫2.5 剖析线程堆栈-线程状态的剖析2.5.1 概述2.5.2 Runnable从虚拟机的角度来看,线程处于正在运行的状态。 那么处于RUNNABLE的线程是不是肯定耗费CPU呢?实际上不肯定。上面的线程堆栈示意该线程正在从网络读取数据,只管上面这个线程显示为RUNNABLE状态,但实际上网 络IO,线程绝大多数工夫是被挂起,只有当数据达到之后,线程才被从新唤醒。挂起产生在本 地代码(Native)中,虚拟机基本不晓得,不像显式调用了Java的sleep()或者wait()等办法,虚构 机能晓得线程的真正状态,但对于本地代码中的挂起,虚拟机无奈真正地晓得线程状态,因 此它一律显示为RUNNABLE。像这种socket IO操作不会耗费大量的CPU,因为大多工夫在期待,只有数据到来之后,才耗费一点点CPU. Thread-39" daemon prio=1 tid=0x08646590 nid=0x666d runnable [5beb7000..5beb88b8] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.read(SocketInputStream.java:129) at java.io.BufferedInputStream.fill(BufferedInputStream.java:183) at java.io.BufferedInputStream.read(BufferedInputStream.java:201) - locked <0x47bfb940> (a java.io.BufferedInputStream) at org.postgresql.PG_Stream.ReceiveChar(PG_Stream.java:141) at org.postgresql.core.QueryExecutor.execute(QueryExecutor.java:68) - locked <0x47bfb758> (a org.postgresql.PG_Stream) at org.postgresql.Connection.ExecSQL(Connection.java:398)上面的线程正在执行纯Java代码指令,实实在在是耗费CPU的线程。 "Thread-444" prio=1 tid=0xa4853568 nid=0x7ade runnable [0xafcf7000..0xafcf8680] java.lang.Thread.State: RUNNABLE//实实在在再对应CPU运算指令at org.apache.commons.collections.ReferenceMap.getEntry(Unknown Source) at org.apache.commons.collections.ReferenceMap.get(Unknown Source)at org.hibernate.util.SoftLimitMRUCache.get(SoftLimitMRUCache.java:51) at org.hibernate.engine.query.QueryPlanCache.getNativeSQLQueryPlan()at org.hibernate.impl.AbstractSessionImpl.getNativeSQLQueryPlan()at org.hibernate.impl.AbstractSessionImpl.list()at org.hibernate.impl.SQLQueryImpl.list(SQLQueryImpl.java:164)at com.mogoko.struts.logic.user.LeaveMesManager.getCommentByShopId()at com.mogoko.struts.action.shop.ShopIndexBaseInfoAction.execute() ......上面的线程正在进行JNI本地办法调用,具体是否耗费CPU,要看TcpRecvExt的实现,如 果TcpRecvExt 是纯运算代码,那么是实实在在耗费CPU,如果TcpRecvExt()中存在挂起的代 码,那么该线程只管显示为RUNNABLE,但实际上也是不耗费CPU的。 ...

February 16, 2022 · 6 min · jiezi

关于线程:线程的创建方式安全状态

多线程明天咱们来聊聊多多线程 多线程创立形式通过继承Thread创立通过接口Runnable创立线程平安同步代码块同步办法Lock锁线程状态Thread与Runnable 创立Thread public class MyThread extends Thread { public MyThread(String name){ super(name); } public void run(){ for (int i = 0; i < 20; i++) { //getName()办法 来自父亲 System.out.println(getName()+i); } }}// 测试类 public class Demo { public static void main(String[] args) { System.out.println("这里是main线程"); MyThread mt = new MyThread("TR"); mt.start();//开启了一个新的线程 for (int i = 0; i < 20; i++) { System.out.println("MI:"+i); } }}Runnable public class MyRunnale implements Runnable{ @Override public void run(){ for(int i = 0 ; i < 20 ; i++){ System.out.println(Thread.cerrentThread().getName() + i); } }}// 测试类2 public class Demo02{ public static void mian(Stirng []args){ // 创立自定义类对象 线程工作对象 MyRunnable mr = new MyRunnable(); // 创立线程对象 Thread t = new Thread(mr,"Run对象"); t.start(); System.out.println("main的线程"); }}Thread和Runnable区别: 如果一个类继承Thread,他就不适宜资源共享,然而应用Runnable接口的话,则更容易实现资源共享. ...

September 1, 2021 · 2 min · jiezi

关于线程:多线程线程初体验

上节讲了下线程和过程的基础知识,然而对于Java来说,可能探讨线程的工夫会更多些,所以接下来的一系列文章都是着重在探讨线程。 创立线程创立的线程的形式是陈词滥调也是面试中喜爱问的问题之一了,网上的说法七嘴八舌,说什么实现Runnable接口和实现Callable接口是同一种类型,这种说法也不是说谬误,只不过须要看站在哪个角度看。然而这种细节其实没有必要太在意,不要钻牛角尖。 实现Runnable接口实现Runnable接口,而后重写run() 办法,该办法定义了线程的工作形式和工作内容。 public class ImplementsRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "------Runnable线程工作中。。。"); } public static void main(String[] args) { for (int i = 0; i < 50; i++) { new Thread(new ImplementsRunnable()).start(); } }}在main办法中,开启了50个线程运行,开启线程其实就是新建了一个Thread,而后把实现Runnable接口的类作为参数传进去,当初咱们来看看运行的后果 能够看到尽管咱们是依照程序来新建线程的,然而线程的先后执行程序是由CPU来管制的,能够说是不可控的,也正是这样能力阐明了多线程在运行。 实现Callable接口实现了接口后须要重写的是call() 办法 public class ImplementsCallable implements Callable { @Override public Object call() { System.out.println(Thread.currentThread().getName() + "--------callable线程工作中"); return "实现callable,有返回值"; } public static void main(String[] args) throws ExecutionException, InterruptedException { for (int i = 0; i < 50; i++) { ImplementsCallable callable = new ImplementsCallable(); FutureTask<String> task = new FutureTask<String>(callable); new Thread(task).start(); System.out.println(task.get()); } }}值得注意的是,在Thread类中的构造函数中,并没有参数为Callable的重载构造函数,基本上都是Runnable ...

June 25, 2021 · 3 min · jiezi

关于线程:简述CPU多核并发缓存架构JVM模型和JMM多线程内存模型

CPU多核并发缓存架构简述首先讲一下CPU的工作原理晚期的CPU性能低,并且是单核,CPU间接和主存交互,从主存读写数据间接实现。随着CPU的一直降级,主存和CPU的匹配越来越差,于是引入了缓存。 CPU读取数据首先查看寄存器中有没有他要的数据。如果寄存器没有,就在一级缓存查找。如果一级缓存没有,就在二级缓存查找。二级也没有,最初在主存中查找数据。留神:在最初和主存交互的时候有一点须要留神,因为这时有多个CPU须要从主存中取数据,CPU1取了数据,CPU2也取了数据,那么到底要存哪个数据?于是引入了缓存一致性协定MESI。这里不细谈这个。总之有了这个协定才能够保证数据的一致性。 JVM内存模型Java在运行程序的时候会主动将数据分成下面几个区域。蓝色代表线程的共享数据区域,绿色是各个线程的公有区域。 办法区:用于存储编译后的数据。包含:类信息、常量、动态变量还有运行时常量池(存储编译后的符号援用和编译生成的字面变量)JVM堆JVM内存最大的一块,次要用来为对象实例分配内存,还有他是垃圾回收治理的次要区域。程序计数器寄存以后线程的下一条操作的字节码行号,字节码解释器工作的时候读取这个行号,从而决定执行分支、循环、跳转、异样解决等性能。虚拟机栈随着线程的创立而呈现,每个办法执行的时候都会在这里记录他的参数信息,指针,返回值等信息。本地办法栈这部分次要与虚拟机用到的 Native 办法相干。 Java多线程内存模型概述(重要)这个模型是一个形象的,是一个标准。他规定了线程从内存中拜访变量的标准。首先JAVA会给每个线程分配内存用来为他们存储各自的公有数据,每个线程想要获取主存中的变量(共享变量),必须将这个变量复制一份到本人的工作内存,对他扭转后再放回主存中。具体拜访细节见图 Lock Unlock要想保障共享变量批改的一致性,每个线程在读取到数据后,必须给变量加锁,读取完之后再解锁。Read获取到主存中的数据,将他传送到线程工作的内存。Load在线程工作的内存中将获取的数据复制一份。Use如果线程计数器区读到了扭转数据相干的字节码操作数,就会对失去的正本数据传给执行引擎。Assign在虚拟机遇到赋值的字节码指令,将执行引擎传过来的值赋值给工作区内存的变量。Store将工作内存中的变量传给主内存。Write把获取到的工作变量写入到主内存中。图片起源:刘乃源讲义

June 9, 2021 · 1 min · jiezi

关于线程:再不解决延迟不当小心你的内存被打爆

摘要:这是在具体代码中发现的不当提早的问题,极其状况下可能把内存打爆。本文分享自华为云社区《线程中不当应用提早问题》,原文作者:技术火炬手 。 背景这是在具体代码中发现的不当提早的问题,极其状况下可能把内存打爆。 代码DevLicenseServiceRoaDelegateImpl.java 定义: 应用: signalRefreshHelp 定义 这段代码最大问题是应用提早算法不当,在极其状况下会导致内存暴涨,重大影响服务程序的性能。 不要应用sleep来实现提早应用sleep实现提早看起来十分直观,然而这个在高并发、多申请、长期运行的服务程序里必须特地小心。这是因为掂量服务程序性能的一个十分重要的指标是QPS, 就是服务程序的解决能力,个别状况下越大越好;服务程序的总并发能力等于每个线程的qps;单个线程的QPS = 1000毫秒 / (解决一个申请的毫秒);所以下面那个线程的QPS <= 1000 / 10000 = 0.1 (因为线程sleep了10000毫秒)。 这里的解决逻辑是谬误的!也有很重大的性能隐患,不过幸好调用这个api 申请不多,才没有导致重大问题。 开发者的用意是在创立一个工作后,提早10s执行该工作,解决时序图如下 如果工夫点t1 & t2 挨得很靠近的话,线程在执行job1 & job2 也是很靠近。 但理论的状况变成: 就算创立job1 & job2的工夫很靠近,但job2执行的工夫会比预期多了10s;间断提交的工作越多,越容易沉积,这些沉积的工作寄存在 blocking queue,始终到处理完毕才删除;如果这类申请很多的话,很容易引起内存爆掉。 解决方案抉择适合的数据结构,默认线程池关联的队列是LinkedBlockingQueue , 没有提早管制,能够应用DelayQueue DelayQueue外部应用了PriorityQueue 按工夫排序;须要本人应用Delayed 接口封装申请数据 上面是例子 测试代码,同时退出 3个须要提早10s的工作 测试后果: 合乎预期 点击关注,第一工夫理解华为云陈腐技术~

May 26, 2021 · 1 min · jiezi

关于操作系统:程序进程和线程有什么不同

本文非原创,译自https://www.backblaze.com/blo... 第一次翻译,文中有谬误的中央还请斧正,谢谢。 让咱们来说说线程想必你屡次据说过计算机程序中的“线程”了吧?然而你不能确定它是什么意思?过程又是怎么的呢?你可能晓得线程在某种程度上跟程序和过程是密切相关的,如果你不是计算机科学业余的话,兴许这就是你的了解了。 如果你是一名程序员,那么晓得这些术语的含意是相对必要的,但对于一个一般的计算机用户而言理解了它们也是有用的。有助于你应用Mac的流动监视器,Windows的工作管理器,或者Linux的Top(性能剖析工具),它能够帮忙你定位解决计算机上呈现问题的程序,或者是否须要装置更多的内存来使你的零碎运行的更好。 让咱们花几分钟来摸索计算机程序的世界,并弄请这些术语的含意。咱们将去简化并概括一些概念,涵盖的这些概念将有助于咱们理清这些术语之间的区别。 Programs(程序)首先,你可能晓得程序是存储在计算机上的旨在完特定工作的代码,程序有多种类型,其中包含计算机的函数程序和操作系统局部的程序,以及特定工作的其它程序。这些特定工作的程序也被称为“应用程序”,它包含诸如文本处理程序,web浏览程序,或通过电子邮件将音讯发送到另一台计算机的程序。 程序通常以计算机能够执行的模式存储在磁盘或者非易失性存储器中。在此之前,它们应用像C,Lisp,Pascal,或其它编程语言创立,这些编程语言应用波及逻辑,数据,设施治理,递归,以及用户交互的指令。为了在计算机中运行,最终后果是被编译成了二进制模式(1和0)的代码文本文件。另一种类型的程序被称为“解释型编程语言”,它是在运行的时候被解释成可执行代码,而不是为了运行而提前去编译。一些常见的解释型编程语言有Python,PHP,JavaScript和Ruby。 最终后果是雷同的,因为程序在运行的时候,它是以二进制的模式加载到内存中。计算机的CPU(Central Processing Unit)仅了解二进制指令,因而当CPU运行时程序须要被转换为二进制的模式。 二进制是计算机原生语言,因为电子电路有两种根本状态,关上或者敞开,它们用0和1来示意。在咱们日常应用的通用编码零碎中,是基于10进制的,每个数位都能够是从0到9的数字。在而二进制中,每个地位能够是0或1。 兴许你听过一个对于程序员的笑话,“世界上只有10种人,懂二进制的人和不懂二进制的人”十进制二进制00000100012001030011401005010160110701118100091001过程怎么工作?程序曾经以二进制的模式加载到计算机内存中了。那么接下来呢? 执行中的程序不只是须要通知计算机做什么的二进制代码。程序还须要内存和各种操作系统资源能力运行。“过程”就是咱们说的程序,它和它所须要去操作的所有资源一起被加载到内存中。“操作系统”是调配所有资源的大脑,它有多种类型,诸如macOS,IOS,Microsoft Windows,Linux,以及Android。操作系统负责管理将程序转换为运行中的过程所需的资源。 任何过程都须要一些必要的资源,如寄存器,程序计数器和栈。“寄存器”是计算机处理器(CPU)用于数据存储区域的局部。寄存器能够寄存指令,存储地址,或过程须要的其它类型的数据。“程序计数器”也被称为指令指针,它用于批示计算机在其程序序列中的地位。“栈”是一种存储计算机程序的以后子程序信息(这里集体了解为你调试代码时的执行上下文)的数据结构,被用于过程的暂存空间。它跟为过程动态分配的内存(被称为“堆”)是须要辨别开来的。 单个程序能够有多个实例,且运行中的程序的每个实例都是一个过程。每个过程有独自的内存地址空间,这意味着过程是独立运行的并且和其它过程是互相隔离的。它不能间接拜访其它过程的共享数据。从一个过程切换到另一个过程须要一些工夫(绝对地)来保留和加载寄存器,内存映射以及其它资源。 过程的这种独立是很重要的,因为操作系统会尽可能的隔离过程以保障一个过程的问题不会影响到另一个过程。你必定碰到过计算机一个应用程序卡死或者呈现问题的状况,并且你可能在不影响其它程序的状况下退出该程序。 线程如何工作?你还在跟着咱们往下浏览吗?咱们终于讲到了线程! 线程是过程的执行单元。一个过程能够有一个或者多个线程。 当一个过程开始时,它会被指定内存和资源。过程中的每个线程共享内存和资源。在单线程过程中,过程只蕴含一个线程。过程和线程是雷同的,并且只有一件事在产生。 在多线程过程中,过程蕴含多个线程,而且该过程在同时做多个事件(从技术角度而言,有时简直等于同时——想理解更多参见“并行和并发?”上面的局部) 咱们谈到了过程或线程可用的两种内存栈和堆。辨别这两类过程内存是很重要的,因为每个线程都有它本人的栈,然而过程中的所有线程将共享堆。 线程有时被称为轻量级过程,是因为它们都有本人的栈而且能够访问共享数据。因为线程跟过程以及过程中的其它线程共享雷同的地址空间(示意计算机实体所占用的内存大小),两个线程之间的通信开销就很低,这是无利的。不利的是过程中的一个线程有问题必定会影响到其它线程以及过程自身的存活。 过程 VS 线程咱们来回顾一下: 程序以编程代码的文本文件开始程序会被编译或解释成二进制的模式程序会被加载进内存程序能够运行一个或多个过程过程通常是和其它过程独立开的线程作为过程的子集存在线程之间通信比过程之间通信要容易的多线程更容易被雷同过程下的其它线程导致的问题影响线程 vs 过程 —— 劣势 和 劣势 线程过程过程操作是重量级的线程操作是更轻量的每个过程都有本人的内存空间线程应用它们所属过程的内存过程之间通信较慢,因为过程都有不同的内存地址线程之间通信比过程之间通信更快,因为对立过程的线程共享它们所属过程的内存过程之间上下文切换是代价更高的雷同过程下的线程上下文切换是代价较低的过程不与其它过程共享内存线程和同一过程下的其它线程共享内存并行和并发?你兴许有这样一个疑难,过程或者线程是否能在同时运行?答案是:这要看状况。对于多处理器或CPU内核(与古代处理器一样)的零碎,多个过程或线程能够并行的执行。对于单个处理器,它是不可能有多个线程或过程真正的同时执行。在这种状况下,CPU在运行的过程或线程之间共享,应用过程调度算法来划分CPU运行的工夫(此处不是很了解),进而产生并行执行的错觉。调配给每个工作的工夫被称为“工夫切片”。在工作之间来回切换很快,简直是无奈觉察的。术语并行(真正同时执行)和并发(过程及时的交替执行产生的同时执行的景象),用真正同时执行的操作和靠近同时执行的操作来辨别。 为什么抉择过程而不是线程,或者线程而不是过程?那么 当程序员在创立同时执行多个工作的程序时该怎么去抉择过程或线程呢?咱们在下面曾经概括了它们的差别,当初让咱们来看一个实在的例子,咱们很多人都在应用的程序——Coogle Chrome。 Google在设计Chrome浏览器的时候,它们必须思考如何同时解决许多须要计算机、通信和网络资源的不同工作。每个浏览器窗口或者选项卡与网络的多个服务通信去获取文本,程序,图像,音频,视频,以及其它资源,继而渲染这些数据以供显示和与用户交互。除此之外,浏览器能够关上多个窗口,每个窗口都有很多工作。 Google必须思考如何去解决工作的隔离。他们抉择把Chrome中每个浏览窗口作为一个独自的过程去运行,而不是像其它浏览器一样作为一个或多个线程去运行。这样做给Google带来了许多益处。将运行中的每个窗口作为一个过程爱护了总体应用程序不被渲染引擎的异样和故障影响,并限度每个渲染引擎过程对其余过程和零碎其余部分的拜访。隔离了过程中的JavaScript程序避免它占用太多的CPU工夫和内存从而导致整个浏览器失去响应 Google认真衡量了多过程的设计。每个浏览器窗口开始一个新的过程在内存和资源方面比起应用线程有更高的固定开销。然而他们敢说他们的办法最终会缩小总体的内存收缩。 当内存不够的时应用过程代替线程也能提供更好的内存用法。不沉闷的窗口会被操作系统视为较低的优先级,并且当其它过程须要内存时有资格被调换到磁盘中去。这有助于放弃用户显示的窗口更好的响应。如果窗口应用的是线程,想要齐全的分隔开已用和未应用的内存是将会更艰难,无疑节约了内存和性能。 您能够在Google的Chromium blog或Chrome Introduction Comic上理解更多的Google对Chrome的设计决策。上面的屏幕截图将展现运行在MacBook Air上关上了多个选项卡的Google Chrome过程。有些Chrome过程应用了很多的CPU工夫和资源,而有些过程应用的非常少。你能够看到每个过程也有许多线程正在运行。 零碎上的流动监视器或工作管理器在微调你的计算机或者定位解决问题时是个不错的帮手。如果你的计算机或程序运行的很慢,或者浏览器窗口在一段时间没有响应,你能够应用零碎监视器查看它的状态。有时你将会看到过程被标记为“没有响应”。试图退出那个过程而后看看你的零碎是否会运行的更快。如果某个应用程序占用着大量的内存,你应该思考抉择其它的应用程序来实现这个工作。 残余一点附录未齐全翻译,有趣味请移步原文。https://www.backblaze.com/blo...

April 22, 2021 · 1 min · jiezi

关于swoole:Swoole的进程数相关

在swoole的配置项中能够设置swoole应用的过程数,而后当咱们启动swoole服务后再服务器就能看到相干启动的过程 应用过程树示意 swoole官网对于过程的一个总结: Swoole的主过程是一个多线程的程序。其中有一组很重要的线程,称之为Reactor线程,这组线程就是真正解决TCP连贯,收发数据的线程 Swoole的主线程在接管到新的连贯后将这个连贯调配给一个固定的Reactor线程,并由这个线程负责监听此socket。在socket可读时读取数据,并进行协定解析,将申请投递到Worker过程,在socket可写时将数据发送给TCP客户端 Reactor线程数的这个参数能够调节主过程内事件处理线程的数量,能够充分利用多核。默认会启用跟CPU核数雷同的数量。倡议是设置为CPU核数的1-4倍 线程之间是无锁的,一个指令能够被并行执行。思考到操作系统调度存在肯定水平的性能损失,能够设置CPU核数为2,最大化利用多核 参考:https://segmentfault.com/q/10...

April 13, 2021 · 1 min · jiezi

关于线程:线程

过程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元。一个程序至多有一个过程,一个过程至多有一个线程。

March 11, 2021 · 1 min · jiezi

关于线程:进程线程与协程傻傻分不清一文带你吃透

前言欢送来到操作系统系列,仍然采纳图解 + 大白话的模式来解说,让小白也能看懂,帮忙大家疾速科普入门 本篇开始介绍过程、线程、协程,置信很多小白们对这几个概念了解的不清晰,这里全副给你们安顿的明明白白,咱们开始进入注释吧 内容纲要 小故事小明(操作系统)开办了一家互联网小公司,因为筹备同时开发A与B两个软件,所以小明请了两个开发团队来做这件事件,别离是小王开发团队与小李开发团队,可是公司特地小,只有一个房间(C P U),而且房间(C P U)只能包容一个开发团队,为了能两个软件开发不被耽搁,小明(操作系统)决定,上午小王团队开发,下午小李团队开发(这个过程称为调度)。 小李(过程)与小王(过程)身为团队负责人,他们要操心的事件比拟多,须要对软件进行剖析整顿,做架构设计,最初再把工作细化调配给团队的每个开发人员(线程),在团队替换房间的时候,还须要把整个软件开发进度记录下来,不便下次接着开发,相比开发人员就轻松多了,每个人只负责一小块,须要记录的也只有一小块。 通过这个小故事,大伙也看进去了,一个过程治理着多个线程,就像团队负责人(过程)治理着多个开发人员(线程)一样。 过程什么是过程你关上网易云音乐会产生一个过程 ,你关上QQ会产生一个过程 ,你电脑上运行的程序都是过程 ,过程就是这么简略暴力。 当初咱们思考一个问题,有一个过程读取硬盘里的文件,这个文件特地大,须要读取很长时间,如果 C P U 始终傻傻的等硬盘返回数据,那 C P U 的利用率是非常低的。 就像烧开水,你会傻傻等水烧开吗?很显著,这段时间齐全能够去做其余的事件(比方玩玩赛博朋克2077),水烧开了再过去把水倒入水杯中,这样不香吗? C P U 也是一样,它发现 过程 在读取硬盘文件,不须要阻塞期待硬盘返回数据,间接去执行其余过程 ,当硬盘返回数据时,C P U 会收到 中断 的信号,于是 C P U 再回到之前的 过程 持续运行 这种多程序 交替执行 的形式,就是 C P U 治理多过程初步思维。 可能会有人问了, 交替执行会不会很慢,这个不必放心,因为 C P U 的执行速度与切换速度十分的快,可能就是几十或几百毫秒,超出了人类的感知,一秒钟内可能就交替运行了多个过程,所以给咱们产生 并行 的错觉,其实这叫并发。 单核 多过程交替执行 就是并发,多过程在多核运行就是并行。 过程的控制结构发明任何货色的时候,都要先无形,才有物,你造房子、造汽车或造其余货色,都要有设计图(构造),再依据设计图来发明, 过程也不例外,它也有属于本人的设计图,那就是 过程管制块(process control block,PCB),前面就简称 P C B 好了 ...

March 9, 2021 · 3 min · jiezi

关于线程:Linux内核-进程管理

1. 过程和线程1.1 定义过程是处于运行状态的程序和相干资源的总称,是资源分配的最小单位。 线程是过程的外部的一个执行序列,是CPU调度的最小单位。 有一段可执行程序代码。有一段过程专用的零碎堆栈空间和零碎空间堆栈。有过程描述符,用于形容过程的相干信息。有独立的存储空间,也就是专有的用户空间,相应的又会有用户空间堆栈。Linux零碎对于线程实现十分非凡,他并不辨别线程和过程,线程只是一种非凡的过程罢了。从下面四点因素来看,领有前三点而缺第四点因素的就是线程,如果齐全没有第四点的用户空间,那就是零碎线程,如果是共享用户空间,那就是用户线程。 1.2 次要区别过程作为分配资源的根本单位,而把线程作为独立运行和独立调度的根本单位,因为线程比过程更小,基本上不领有系统资源,故对它的调度所付出的开销就会小得多,能更高效的进步零碎多个程序间并发执行的水平。 过程和线程的次要差异在于它们是不同的操作系统资源管理形式。过程有独立的地址空间,一个过程解体后,在保护模式下不会对其它过程产生影响,而线程只是一个过程中的不同执行门路。线程有本人的堆栈和局部变量,但线程之间没有独自的地址空间,一个线程死掉就等于整个过程死掉,所以多过程的程序要比多线程的程序强壮,但在过程切换时,消耗资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用过程。 总结:linux中,过程和线程惟一区别是有没有独立的地址空间。 2. 过程描述符及工作构造32位机器上,大概有1.7KB,过程描述符残缺形容一个正在执行的过程的所有信息。 工作队列(双向循环链表) 过程描述符struct task_struct(源代码 | linnux/sched.h | v5.4) struct task_struct { volatile long state; // -1为不可运行, 0为可运行, >0为已中断 int lock_depth; // 锁的深度 unsigned int policy; // 调度策略:个别有FIFO,RR,CFS pid_t pid; // 过程标识符,用来代表一个过程 struct task_struct *parent; // 父过程 struct list_head children; // 子过程 struct list_head sibling; // 兄弟过程}2.1 调配过程描述符2.1.1 slab分配器linux采纳slab分配器调配task_struct构造 目标:对象复用和缓存着色。 slab分配器动静生成task_struct,只需在栈底(绝对于向下增长的栈)或栈顶(绝对于向上增长的栈)创立一个新构造struct thread_info。 2.1.2 过程描述符寄存PID最大值默认为32768(short int 短整形的最大值<linux/threads.h>)可通过批改/proc/sys/kernel/pid_max进步下限。 current宏查找以后正在运行过程的过程描述符。 x86零碎中,current把栈指针后13个无效位屏蔽掉,用来计算出thread_info的偏移。 ...

March 8, 2021 · 1 min · jiezi

关于线程:微服务容错时这些技术你要立刻想到

摘要:随同着微服务架构被宣传得热火朝天,一些概念也被推到了咱们背后。服务熔断、服务降级,好高大上的样子,以前可望不可即,今日终于揭开它神秘面纱。随同着微服务架构被宣传得热火朝天,一些概念也被推到了咱们背后。服务熔断、服务降级,好高大上的样子,以前可望不可即,今日终于揭开它神秘面纱。 服务雪崩效应的定义很简略,是一种因服务提供者的不可用导致服务调用者的不可用,并将不可用逐步放大的过程。 能够联合下图进行了解: 服务雪崩 上图中,A作为根底的服务提供者,为B和C提供服务,D、E、F是B和C服务的调用者,当A不可用时,将引起B和C的不可用,并将这种不可用放大到D、E、F,从而可能导致整个零碎的不可用,服务雪崩的产生可能导致分布式系统的瘫痪。 服务雪崩效应的产生个别有三个流程,服务提供者不可用 -> 重试加大流量 -> 服务调用者不可用 服务提供者不可用的呈现的起因有很多,可能是因为服务器的宕机或者网络故障,也可能是因为程序存在的Bug,也有可能是大量的申请导致服务提供者的资源受限无奈及时响应,还有可能是因为缓存击穿造成服务提供者超负荷运行等等,毕竟没有人能保障软件的齐全正确性。 在服务提供者不可用产生之后,用户可能无法忍受长时间的期待,一直地发送雷同的申请,服务调用者从新调用服务提供者,同时服务提供者中可能存在对异样的重试机制,这些都会加大对服务提供者的申请流量。然而此时的服务提供者曾经是一艘破船,它也无能无力,无奈返回无效的后果。 最初是服务调用者因为服务提供者的不能用导致了本身的解体。当服务调用者应用同步调用的时候,大量的期待线程将会耗尽线程池中的资源,最终导致服务调用者的宕机,无奈响应用户的申请,服务雪崩效应就此产生了。 断路器在分布式系统中,不同服务之间产生的调用十分常见,当服务提供者不可用时就很有可能产生服务雪崩的效应,导致整个零碎的不可用。所以为了预防这种申请的产生,能够通过断路器模式进行预防(类比电路中的断路器,在电路过大的时候主动断开,避免电线过热侵害整条电路)。 断路器模式背地的思维很简略,将近程函数调用包装到一个断路器对象中,用于监控函数调用过程的失败。一旦该函数调用的产生失败的次数在一段时间内达到肯定的阀值,那么这个断路器将会跳闸,而后接下来工夫里对该被爱护函数调用的线程将会被断路器间接返回一个谬误,而不再产生该函数的实在调用。这样子就防止了服务调用者在服务提供者不可用时发送申请,从而缩小线程池中资源的耗费,爱护了服务调用者。 断路器时序图 尽管下面的断路器在关上的时候防止了被爱护的函数调用,然而当状况恢复正常时,须要内部干涉来重置断路器,使得函数调用能够从新产生。所以正当的断路器应该具备以下的开关转化逻辑,它须要一个机制来管制它的从新闭合,图6-3中是通过一个重置工夫来决定。 断路器状态图 敞开状态: 断路器处于敞开状态,统计调用失败次数,在一段时间内达到肯定的阀值后断路器关上。关上状态: 断路器处于关上状态,对函数调用间接返回失败谬误,不产生真正的函数调用。设置了一个重置工夫窗,在重置工夫窗完结后,断路器来到半开状态。半开状态: 断路器处于半开状态,此时容许进行函数调用,当调用都胜利了(或者胜利达到肯定的比例),敞开断路器,否则认为服务没有复原,从新关上断路器。断路器的关上能保障服务调用者在调用异样服务时,疾速返回后果,防止大量的同步期待,缩小服务调用者的资源耗费。并且断路器能在关上的一段时间后持续侦测申请执行后果,提供断路器敞开的可能,复原服务的调用。 服务降级操作断路器是为了隔断服务调用者和异样服务提供者,避免了服务雪崩的景象,是一种爱护的措施。而服务降级的意思是在整体资源不够的时候,适当的放弃局部服务,将次要的资源投放到外围服务中,待渡过难关之后,再把敞开的服务重启回来。 在Hystrix中,当服务间调用产生问题时,它将采纳备用的fallback办法代替主办法执行并返回后果,这就进行了服务降级,同时触发了断路器的逻辑。当调用服务失败次数在一段时间内超过了断路器的阀值时(此时始终调用fallback中的逻辑返回后果),断路器将关上,此时将不再调用函数,而是疾速失败,间接执行fallback逻辑,服务降级,缩小服务调用者的资源耗费,爱护服务调用者中的线程资源。 资源隔离在货船中,为了避免漏水和火灾的扩散,个别会将货仓进行宰割,防止了一个货仓出事导致整艘船沉没的喜剧。同样的,在Hystrix中,也采纳了这样的舱壁模式,将零碎中的服务提供者隔离起来,一个服务提供者提早升高或者失败,并不会导致整个零碎的失败,同时也可能管制调用这些服务的并发度。 线程与线程池Hystrix中通过将调用服务线程与服务拜访的执行线程分隔开来,调用线程可能空进去去做其余的工作而不至于被服务调用的执行的阻塞过长的工夫。 在Hystrix中应用独立的线程池对应每一个服务提供者,来隔离和限度这些服务,于是,某个服务提供者的高提早或者饱和资源受限只会产生在该服务提供者对应的线程池中。如上图中,Dependency I的调用失败或者高提早仅会导致本身对应的线程池中的5个线程的阻塞,并不会影响其余服务提供者的线程池。零碎齐全与服务提供者申请隔离开来,即便服务提供者对应的线程齐全耗尽,并不会影响零碎中的其余申请。 留神在对应服务提供者的线程池被占满时,Hystrix会进入了fallback逻辑,疾速失败,爱护服务调用者的资源稳固。 信号量除了线程池外,Hystrix还能够通过信号量(计数器)来限度单个服务提供者的并发量。如果通过信号量来控制系统负载,将不再容许设置超时管制和异步化调用,这就示意在服务提供者呈现高提早,其调用线程将会被阻塞,直至服务提供者的网络申请超时,如果对服务提供者的稳定性有足够的信念,能够通过信号量来控制系统的负载。 总结咱们在这篇文章介绍了熔断、服务雪崩、服务降级等概念。在解决微服务容错时,这些都是罕用的技术,咱们须要首先理解其概念。 点击关注,第一工夫理解华为云陈腐技术~

January 28, 2021 · 1 min · jiezi

关于线程:java线程小结

线程状态 NEW顾名思义,这个状态,只存在于线程刚创立,未start之前 RUNNABLE这个状态的线程,其正在JVM中执行,然而这个"执行",不肯定是真的在运行, 也有可能是在期待CPU资源。所以,在网上,有人把这个状态辨别为READY和RUNNING两个,一个示意的start了,资源一到位随时能够执行,另一个示意真正的执行中 BLOCKED这个状态,个别是线程期待获取一个锁,来继续执行下一步的操作,比拟经典的就是synchronized关键字,这个关键字润饰的代码块或者办法,均须要获取到对应的锁,在未获取之前,其线程的状态就始终未BLOCKED,如果线程长时间处于这种状态下,咱们就是当心看是否呈现死锁的问题了。 WAITING一个线程会进入这个状态,肯定是执行了如下的一些代码,例如 Object.wait()Thread.join()LockSupport.park()当一个线程执行了Object.wait()的时候,它肯定在期待另一个线程执行Object.notify()或者Object.notifyAll()。或者一个线程thread,其在主线程中被执行了thread.join()的时候,主线程即会期待该线程执行实现。当一个线程执行了LockSupport.park()的时候,其在期待执行LockSupport.unpark(thread)。当该线程处于这种期待的时候,其状态即为WAITING。须要关注的是,这边的期待是没有工夫限度的,当发现有TIMED_WAITING这个状态和WAITING状态的区别就是,这个状态的期待是有肯定时效的,即能够了解为WAITING状态期待的工夫是永恒的,即必须等到某个条件合乎能力持续往下走,否则线程不会被唤醒。然而TIMED_WAITING,期待一段时间之后,会唤醒线程去从新获取锁。当执行如下代码的时候,对应的线程会进入到TIMED_WAITING状态 Thread.sleep(long)Object.wait(long)Thread.join(long)LockSupport.parkNanos()LockSupport.parkUntil()TERMINATED 即为线程执行完结之后的状态 创立线程的三种形式: 继承自Thread类实现Runnable接口实现Callable接口举荐应用第二种 保障高并发场景下的线程平安,能够从一下四个维度考量 数据单线程内可见。单线程总是平安的ThreadLocal 就是采纳这种形式来实现线程平安的。只读对象。线程安全类。同步与锁机制java.util.concurrent包下的分类 线程同步类并发汇合类线程治理类锁相干类参考资料:Java线程的6种状态剖析 《码出高效 Java开发手册》

September 14, 2020 · 1 min · jiezi

关于线程:线程和线程池知识和原理

August 26, 2020 · 0 min · jiezi

并发编程之线程共享和协作一

更多Android架构进阶视频学习请点击:https://space.bilibili.com/47...本篇文章将从以下几个内容来阐述线程共享和协作: [基础概念之CPU核心数、线程数,时间片轮转机制解读][线程之间的共享][线程间的协作] 一、基础概念CPU核心数、线程数两者的关系:cpu的核心数与线程数是1:1的关系,例如一个8核的cpu,支持8个线程同时运行。但在intel引入超线程技术以后,cpu与线程数的关系就变成了1:2。此外在开发过程中并没感觉到线程的限制,那是因为cpu时间片轮转机制(RR调度)的算法的作用。什么是cpu时间片轮转机制看下面1.2. CPU时间片轮转机制含义就是:cpu给每个进程分配一个“时间段”,这个时间段就叫做这个进程的“时间片”,这个时间片就是这个进程允许运行的时间,如果当这个进程的时间片段结束,操作系统就会把分配给这个进程的cpu剥夺,分配给另外一个进程。如果进程在时间片还没结束的情况下阻塞了,或者说进程跑完了,cpu就会进行切换。cpu在两个进程之间的切换称为“上下文切换”,上下文切换是需要时间的,大约需要花费5000~20000(5毫秒到20毫秒,这个花费的时间是由操作系统决定)个时钟周期,尽管我们平时感觉不到。所以在开发过程中要注意上下文切换(两个进程之间的切换)对我们程序性能的影响。 二、 线程之间的共享synchronized内置锁线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。 Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。volatile 关键字volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 private volatile static boolean ready;private static int number;不加volatile时,子线程无法感知主线程修改了ready的值,从而不会退出循环,而加了volatile后,子线程可以感知主线程修改了ready的值,迅速退出循环。但是volatile不能保证数据在多个线程下同时写时的线程安全,参见代码:thread-platformsrccomchjthreadcapt01volatilesNotSafe.javavolatile最适用的场景:一个线程写,多个线程读。线程私有变量 ThreadLocal + get() 获取每个线程自己的threadLocals中的本地变量副本。+ set() 设置每个线程自己的threadLocals中的线程本地变量副本。ThreadLocal有一个内部类ThreadLocalMap: public T get() { Thread t = Thread.currentThread(); //根据当前的线程返回一个ThreadLocalMap.点进去getMap ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } //点击去getMap(t)方法发现其实返回的是当前线程t的一个内部变量ThreadLocal.ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } //由此可以知道,当调用ThreadLocal的get方法是,其实返回的是当前线程的threadLocals(类型是ThreadLocal.ThreadLocalMap)中的变量。调用set方法也类似。 //举例一个使用场景 /** * ThreadLocal使用场景:把数据库连接对象存放在ThreadLocal当中. * 优点:减少了每次获取Connection需要创建Connection * 缺点:因为每个线程本地会存放一份变量,需要考虑内存的消耗问题。 * @author luke Lin * */ public class ConnectionThreadLocal { private final static String DB_URL = "jdbc:mysql://localhost:3306:test"; private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){ protected Connection initialValue() { try { return DriverManager.getConnection(DB_URL); } catch (SQLException e) { e.printStackTrace(); } return null; }; }; /** * 获取连接 * @return */ public Connection getConnection(){ return connectionHolder.get(); } /** * 释放连接 */ public void releaseConnection(){ connectionHolder.remove(); } } //解决ThreadLocal中弱引用导致内存泄露的问题的建议 + 声明ThreadLoal时,使用private static修饰 + 线程中如果本地变量不再使用,即使使用remove()三、 线程间的协作wait() notify() notifyAll() ...

November 4, 2019 · 2 min · jiezi

深入分析synchronized实现原理

实现原理Synchronized可以保证一个在多线程运行中,同一时刻只有一个方法或者代码块被执行,它还可以保证共享变量的可见性和原子性在Java中每个对象都可以作为锁,这是Synchronized实现同步的基础。具体的表现为一下3种形式: 普通同步方法,锁是当前实例对象;静态同步方法,锁是当前类的Class对象;同步方法快,锁是Synchronized括号中配置的对象。当一个线程试图访问同步代码块时,它必须先获取到锁,当同步代码块执行完毕或抛出异常时,必须释放锁。那么它是如何实现这一机制的呢?我们先来看一个简单的synchronized的代码: public class SyncDemo { public synchronized void play() {} public void learn() { synchronized(this) { } }}利用javap工具查看生成的class文件信息分析Synchronized,下面是部分信息 public com.zzw.juc.sync.SyncDemo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/zzw/juc/sync/SyncDemo; public synchronized void play(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lcom/zzw/juc/sync/SyncDemo; public void learn(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_1 5: monitorexit 6: goto 14 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow 14: return Exception table: from to target type 4 6 9 any 9 12 9 any从上面利用javap工具生成的信息我们可以看到同步方法是利用ACC_SYNCHRONIZED这个修饰符来实现的,同步代码块是利用monitorenter和monitorexit这2个指令来实现的。 ...

November 4, 2019 · 2 min · jiezi

阿里巴巴开源-Dragonwell-JDK-最新版本-811GA-发布

导读:新版本主要有三大变化:同步了 OpenJDK 上游社区 jdk8u222-ga 的最新更新;带来了正式的 feature:G1ElasticHeap;发布了用户期待的 Windows 实验版本 Experimental Windows version。距离 Dragonwell JDK 第一个正式版本 8.0.0-GA 发布已经过去 3 个月了,项目在 Github 上的 stars 继续攀升达到了 1900。今天我们带来了最新版本 8.1.1-GA 的发布,包含了全新的特性和更新。详情见下文。 龙井 8.1.1-GA 的新变化新版本里我们同步了 OpenJDK 上游社区 jdk8u222-ga 的最新更新,带来了上游稳定版本的最新安全更新和补丁。 在 8.0.0-GA 发布的时候,我们介绍了 Dragonwell 第三个新特性 ElasticHeap 的一些情况,很多用户已经跃跃欲试了,这次发布我们带来了正式的 feature:G1ElasticHeap。能够在不影响 Java 业务运行的前提下,动态节约 Java 进程物理内存。 另外,我们还发布了用户期待的 Windows 实验版本 Experimental Windows version,使用 Windows 开发的小伙伴们可以更加方便的使用 Dragonwell JDK 进行相应的开发工作。 G1ElasticHeap从 feature 的名字上我们可以看到 ElasticHeap 是基于 G1 GC 开发的,所以想要使用这个功能的小伙伴,需要开启 G1 GC(-XX:+UseG1GC)。在 8.0.0-GA 正式版介绍时,我们介绍了部分技术背景,由于 Java 自动管理内存的特性,整个 Java Heap 的地址空间和物理内存将被 Java 进程占用,即使使用率不高,回收后也并不会归还给操作系统,导致 Java 进程会有较高的常驻内存。 ...

October 17, 2019 · 2 min · jiezi

前端面试每日-31-第172天

今天的知识点 (2019.10.05) —— 第172天[html] HTML的注释有几种写法?有什么规范吗?[css] 你知道什么是流体排版吗?说说它的原理是什么?[js] ES5和ES6、ES7有什么区别?[软技能] 请解释下单线程与多线程之间的区别?《论语》,曾子曰:“吾日三省吾身”(我每天多次反省自己)。 前端面试每日3+1题,以面试题来驱动学习,每天进步一点! 让努力成为一种习惯,让奋斗成为一种享受!相信 坚持 的力量!!!欢迎在 Issues 和朋友们一同讨论学习! 项目地址:前端面试每日3+1 【推荐】欢迎跟 jsliang 一起折腾前端,系统整理前端知识,目前正在折腾 LeetCode,打算打通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个Star, 同时欢迎微信扫码关注 前端剑解 公众号,并加入 “前端学习每日3+1” 微信群相互交流(点击公众号的菜单:进群交流)。 学习不打烊,充电加油只为遇到更好的自己,365天无节假日,每天早上5点纯手工发布面试题(死磕自己,愉悦大家)。希望大家在这浮夸的前端圈里,保持冷静,坚持每天花20分钟来学习与思考。在这千变万化,类库层出不穷的前端,建议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢迎大家到Issues交流,鼓励PR,感谢Star,大家有啥好的建议可以加我微信一起交流讨论!希望大家每日去学习与思考,这才达到来这里的目的!!!(不要为了谁而来,要为自己而来!)交流讨论欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个[Star] https://github.com/haizlin/fe...

October 5, 2019 · 1 min · jiezi

前端面试每日-31-第134天

今天的知识点 (2019.08.28) —— 第134天[html] Web Worker线程的限制是什么?[css] transition、animation、transform三者有什么区别?[js] [请写出如下代码运行的结果并解释为什么?[代码]](https://github.com/haizlin/fe... var type = 'images'; var size = {width: 800, height: 600}; var format = ['jpg', 'png']; function change(type, size, format){ type = 'video'; size = {width: 1024, height: 768}; format.push('map'); } change(type, size, format); console.log(type, size, format);[软技能] 你在工作中有用到过websocket吗?用它来解决什么问题?《论语》,曾子曰:“吾日三省吾身”(我每天多次反省自己)。 前端面试每日3+1题,以面试题来驱动学习,每天进步一点! 让努力成为一种习惯,让奋斗成为一种享受!欢迎在 Issues 和朋友们一同讨论学习! 项目地址:前端面试每日3+1 【推荐】欢迎跟 jsliang 一起折腾前端,系统整理前端知识,目前正在折腾 LeetCode,打算打通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个Star, 同时欢迎微信扫码关注 前端剑解 公众号,并加入 “前端学习每日3+1” 微信群相互交流(点击公众号的菜单:进群交流)。 学习不打烊,充电加油只为遇到更好的自己,365天无节假日,每天早上5点纯手工发布面试题(死磕自己)。希望大家在这浮夸的前端圈里,保持冷静,坚持每天花20分钟来学习与思考。在这千变万化,类库层出不穷的前端,不建议大家等到要找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢迎大家到Issues交流,鼓励PR,感谢Star,大家有啥好的建议可以加我微信一起交流讨论!交流讨论欢迎大家前来讨论,如果觉得对你的学习有一定的帮助,欢迎点个[Star] 前端面试每日3+1

August 28, 2019 · 1 min · jiezi

DLedger-基于-raft-协议的-commitlog-存储库

“点击获取上云帮助文档” 尊敬的阿里云用户: 您好!为方便您试用开源 RocketMQ 客户端访问阿里云MQ,我们申请了专门的优惠券,优惠券可以直接抵扣金额。请填写下您公司账号信息,点击上图,了解更多哦。 一、DLedger引入目的 在 RocketMQ 4.5 版本之前,RocketMQ 只有 Master/Slave 一种部署方式,一组 broker 中有一个 Master ,有零到多个 Slave,Slave 通过同步复制或异步复制的方式去同步 Master 数据。Master/Slave 部署模式,提供了一定的高可用性。 但这样的部署模式,有一定缺陷。比如故障转移方面,如果主节点挂了,还需要人为手动进行重启或者切换,无法自动将一个从节点转换为主节点。因此,我们希望能有一个新的多副本架构,去解决这个问题。 新的多副本架构首先需要解决自动故障转移的问题,本质上来说是自动选主的问题。这个问题的解决方案基本可以分为两种: 利用第三方协调服务集群完成选主,比如 zookeeper 或者 etcd。这种方案会引入了重量级外部组件,加重部署,运维和故障诊断成本,比如在维护 RocketMQ 集群还需要维护 zookeeper 集群,并且 zookeeper 集群故障会影响到 RocketMQ 集群。利用 raft 协议来完成一个自动选主,raft 协议相比前者的优点是不需要引入外部组件,自动选主逻辑集成到各个节点的进程中,节点之间通过通信就可以完成选主。因此最后选择用 raft 协议来解决这个问题,而 DLedger 就是一个基于 raft 协议的 commitlog 存储库,也是 RocketMQ 实现新的高可用多副本架构的关键。 二、DLedger 设计理念1. DLedger 定位 Raft 协议是复制状态机的实现,这种模型应用到消息系统中就会存在问题。对于消息系统来说,它本身是一个中间代理,commitlog 状态是系统最终状态,并不需要状态机再去完成一次状态构建。因此 DLedger 去掉了 raft 协议中状态机的部分,但基于raft协议保证commitlog 是一致的,并且是高可用的。 另一方面 DLedger 又是一个轻量级的 java library。它对外提供的 API 非常简单,append 和 get。Append 向 DLedger 添加数据,并且添加的数据会对应一个递增的索引,而 get 可以根据索引去获得相应的数据。因此 DLedger 是一个 append only 的日志系统。 ...

August 8, 2019 · 2 min · jiezi

浅析-Linux-进程与线程

简介进程与线程是所有的程序员都熟知的概念,简单来说进程是一个执行中的程序,而线程是进程中的一条执行路径。进程是操作系统中基本的抽象概念,本文介绍 Linux 中进程和线程的用法以及原理,包括创建、消亡等。 进程创建与执行Linux 中进程的创建与执行分为两个函数,分别是 fork 和 exec,如下代码所示: int main() { pid_t pid; if ((pid = fork() < 0) { printf("fork error\n"); } else if (pid == 0) { // child if (execle("/home/work/bin/test1", "test1", NULL) < 0) { printf("exec error\n"); } } // parent if (waitpid(pid, NULL) < 0) { printf("wait error\n"); }}fork 从当前进程创建一个子进程,此函数返回两次,对于父进程而言,返回的是子进程的进程号,对于子进程而言返回 0。子进程是父进程的副本,拥有与父进程一样的数据空间、堆和栈的副本,并且共享代码段。 由于子进程通常是为了调用 exec 装载其它程序执行,所以 Linux 采用了写时拷贝技术,即数据段、堆和栈的副本并不会在 fork 之后就真的拷贝,只是将这些内存区域的访问权限变为只读,如果父子进程中有任一个要修改这些区域,才会修改对应的内存页生成新的副本,这样子是为了提高性能。 fork 之后父进程先执行还是子进程先执行是不确定的,所以如果要求父子进程进行同步,往往需要使用进程间通信。fork 之后子进程会继承父进程的很多东西,如: 打开的文件实际用户 ID、组用户 ID 等进程组当前工作目录信号屏蔽和安排...父子进程的区别在于: ...

July 16, 2019 · 2 min · jiezi

Java并发23并发设计模式-两阶段终止模式优雅地终止线程

前面我们都是在讲如何创建线程,接下来我们说下如何终止线程。 java的线程小节中,我曾讲过:线程执行完或者出现异常就会进入终止状态。这样看,终止一个线程看上去很简单啊!一个线程执行完自己的任务,自己进入终止状态,这的确很简单。不过我们今天谈到的“优雅地终止线程”,不是自己终止自己,而是在一个线程 T1 中,终止线程 T2;这里所谓的“优雅”,指的是给 T2 一个机会料理后事,而不是被直接终止。 Java 语言的 Thread 类中曾经提供了一个 stop() 方法,用来终止线程,可是早已不建议使用了,原因是这个方法用是直接终止的线程,线程并没有机会料理后事。 如何理解两阶段终止模式前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式。顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程 T1 向线程 T2发送终止指令,而第二阶段则是线程 T2响应终止指令 两阶段终止模式示意图### 那在 Java 语言里,终止指令是什么呢?这个要从 Java 线程的状态转换过程说起。我们在 java的线程小节中曾经提到过 Java 线程的状态转换图。 从这个图里你会发现,Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态,而实际上线程也可能处在休眠状态,也就是说,我们要想终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。如何做到呢?这个要靠 Java Thread 类提供的interrupt() 方法,它可以将休眠状态的线程转换到 RUNNABLE 状态。 线程转换到 RUNNABLE 状态之后,我们如何再将其终止呢?RUNNABLE 状态转换到终止状态,优雅的方式是让 Java 线程自己执行完 run() 方法,所以一般我们采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。这个过程其实就是我们前面提到的第二阶段:响应终止指令 综合上面这两点,我们能总结出终止指令,其实包括两方面内容:interrupt() 方法和线程终止的标志位。 用两阶段终止模式终止监控操作实际工作中,有些监控系统需要动态地采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统,详细过程如下图所示。出于对性能的考虑(有些监控项对系统性能影响很大,所以不能一直持续监控),动态采集功能一般都会有终止操作。 动态采集功能示意图### 下面的示例代码是监控代理简化之后的实现,start() 方法会启动一个新的线程 rptThread 来执行监控数据采集和回传的功能,stop() 方法需要优雅地终止线程 rptThread,那 stop() 相关功能该如何实现呢? class Proxy { boolean started = false; // 采集线程 Thread rptThread; // 启动采集功能 synchronized void start(){ // 不允许同时启动多个采集线程 if (started) { return; } started = true; rptThread = new Thread(()->{ while (true) { // 省略采集、回传实现 report(); // 每隔两秒钟采集、回传一次数据 try { Thread.sleep(2000); } catch (InterruptedException e) { } } // 执行到此处说明线程马上终止 started = false; }); rptThread.start(); } // 终止采集功能 synchronized void stop(){ // 如何实现? }} 按照两阶段终止模式,我们首先需要做的就是将线程 rptThread 状态转换到 RUNNABLE,做法很简单,只需要在调用 rptThread.interrupt() 就可以了。线程 rptThread 的状态转换到 RUNNABLE 之后,如何优雅地终止呢?下面的示例代码中,我们选择的标志位是线程的中断状态:Thread.currentThread().isInterrupted() ,需要注意的是,我们在捕获 Thread.sleep() 的中断异常之后,通过 Thread.currentThread().interrupt() 重新设置了线程的中断状态,因为 JVM 的异常处理会清除线程的中断状态。 ...

July 15, 2019 · 2 min · jiezi

从入门到放弃Java并发编程NIOChannel

前言上篇[【从入门到放弃-Java】并发编程-NIO使用]()简单介绍了nio的基础使用,本篇将深入源码分析nio中channel的实现。 简介channel即通道,可以用来读、写数据,它是全双工的可以同时用来读写操作。这也是它与stream流的最大区别。 channel需要与buffer配合使用,channel通道的一端是buffer,一端是数据源实体,如文件、socket等。在nio中,通过channel的不同实现来处理 不同实体与数据buffer中的数据传输。 channel接口: package java.nio.channels;import java.io.IOException;import java.io.Closeable;/** * A nexus for I/O operations. * * <p> A channel represents an open connection to an entity such as a hardware * device, a file, a network socket, or a program component that is capable of * performing one or more distinct I/O operations, for example reading or * writing. * * <p> A channel is either open or closed. A channel is open upon creation, * and once closed it remains closed. Once a channel is closed, any attempt to * invoke an I/O operation upon it will cause a {@link ClosedChannelException} * to be thrown. Whether or not a channel is open may be tested by invoking * its {@link #isOpen isOpen} method. * * <p> Channels are, in general, intended to be safe for multithreaded access * as described in the specifications of the interfaces and classes that extend * and implement this interface. * * * @author Mark Reinhold * @author JSR-51 Expert Group * @since 1.4 */public interface Channel extends Closeable { /** * Tells whether or not this channel is open. * * @return <tt>true</tt> if, and only if, this channel is open */ public boolean isOpen(); /** * Closes this channel. * * <p> After a channel is closed, any further attempt to invoke I/O * operations upon it will cause a {@link ClosedChannelException} to be * thrown. * * <p> If this channel is already closed then invoking this method has no * effect. * * <p> This method may be invoked at any time. If some other thread has * already invoked it, however, then another invocation will block until * the first invocation is complete, after which it will return without * effect. </p> * * @throws IOException If an I/O error occurs */ public void close() throws IOException;}常见的channel实现有: ...

July 8, 2019 · 12 min · jiezi

11JavaScript-线程机制与事件机制

JavaScript线程机制与事件机制一、进程与线程进程(process)程序的一次执行,它占有一片独有的内存空间。可以通过windows任务管理器查看进程。线程(thread)是进程内的一个独立执行单元。是程序执行的一个完整流程。是CPU的最小调度单元。进程与线程图解 相关知识应用程序必须运行在某个进程的某个线程上。一个进程中至少有一个运行的线程:主线程,进程启动后自动创建。一个进程中也可以同时运行多个线程,我们会说程序是多线程运行的。一个进程内的数据可以供其中的多个线程中直接共享。多个进程之间的数据是不能直接共享的。线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用。相关问题(1)何为多进程与多线程? 多进程运行:一个应用程序可以同时启动多个实例运行。多线程:在一个进程内,同时有多个线程运行。(2)比较单线程与多线程? 多线程 优点:能有效提升CPU的利用率。缺点: 创建多线程开销。线程间切换开销。死锁与状态同步问题。单线程 优点:顺序编程简单易懂。缺点:效率低。(3)JS是单线程还是多线程? JS是单线程运行的。但是使用H5中的 Web Workers可以多线程运行。(4)浏览器运行是单线程还是多线程? 浏览器都是多线程运行的。(5)浏览器运行是单进程还是多进程? 有的是单进程: 老版Firefox老版IE有的是多进程: Chrome新版Firefox新版IE如何查看浏览器是否是多进程运行的呢? 任务管理器==>进程二、浏览器内核(1)浏览器内核是支撑浏览器运行的最核心的程序。 (2)不同的浏览器内核不一样: Chrome,Safari:webkitFirefox:GeckoIE:Trident360,搜狗等国内浏览器:Trident+webkit(3)内核由很多模块组成: 主线程 js引擎模块:负责js程序的编译与运行。html,css文档解析模块:负责页面文本的解析。DOM/CSS模块:负责DOM/CSS在内存中的相关处理。布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)分线程 定时器模块:负责定时器的管理。DOM事件响应模块:负责事件的管理。网络请求模块:负责服务器请求(常规/ajax)。三、定时器引发的思考(1)定时器真是定时执行的吗? 定时器并不能保证真正定时执行。一般会延迟一丁点(可以接受), 也有可能延迟很长时间(不能接受)。<button id="btn">启动定时器</button>document.getElementById('btn').onclick = function () { var start = Date.now() console.log('启动定时器前...') setTimeout(function () { console.log('定时器执行了', Date.now()-start) }, 200) console.log('启动定时器后...')} 给上面回调函数加一个长时间的任务: document.getElementById('btn').onclick = function () { var start = Date.now() console.log('启动定时器前...') setTimeout(function () { console.log('定时器执行了', Date.now()-start) }, 200) console.log('启动定时器后...') // 做一个长时间的工作 for (var i = 0; i < 1000000000; i++) {}}结果: ...

June 27, 2019 · 2 min · jiezi

原理解析-深入了解-Apache-Flink-的网络协议栈

作者:Nico Kruber 翻译:曹英杰 Flink 的网络协议栈是组成 flink-runtime 模块的核心组件之一,是每个 Flink 作业的核心。它连接所有 TaskManager 的各个子任务(Subtask),因此,对于 Flink 作业的性能包括吞吐与延迟都至关重要。与 TaskManager 和 JobManager 之间通过基于 Akka 的 RPC 通信的控制通道不同,TaskManager 之间的网络协议栈依赖于更加底层的 Netty API。 本文将首先介绍 Flink 暴露给流算子(Stream operator)的高层抽象,然后详细介绍 Flink 网络协议栈的物理实现和各种优化、优化的效果以及 Flink 在吞吐量和延迟之间的权衡。 1.逻辑视图Flink 的网络协议栈为彼此通信的子任务提供以下逻辑视图,例如在 A 通过 keyBy() 操作进行数据 Shuffle : 这一过程建立在以下三种基本概念的基础上: ▼ 子任务输出类型(ResultPartitionType):Pipelined(有限的或无限的):一旦产生数据就可以持续向下游发送有限数据流或无限数据流。Blocking:仅在生成完整结果后向下游发送数据。 ▼ 调度策略:同时调度所有任务(Eager):同时部署作业的所有子任务(用于流作业)。上游产生第一条记录部署下游(Lazy):一旦任何生产者生成任何输出,就立即部署下游任务。上游产生完整数据部署下游:当任何或所有生产者生成完整数据后,部署下游任务。 ▼ 数据传输:高吞吐:Flink 不是一个一个地发送每条记录,而是将若干记录缓冲到其网络缓冲区中并一次性发送它们。这降低了每条记录的发送成本因此提高了吞吐量。低延迟:当网络缓冲区超过一定的时间未被填满时会触发超时发送,通过减小超时时间,可以通过牺牲一定的吞吐来获取更低的延迟。 我们将在下面深入 Flink 网络协议栈的物理实现时看到关于吞吐延迟的优化。对于这一部分,让我们详细说明输出类型与调度策略。首先,需要知道的是子任务的输出类型和调度策略是紧密关联的,只有两者的一些特定组合才是有效的。 Pipelined 结果是流式输出,需要目标 Subtask 正在运行以便接收数据。因此需要在上游 Task 产生数据之前或者产生第一条数据的时候调度下游目标 Task 运行。批处理作业生成有界结果数据,而流式处理作业产生无限结果数据。 批处理作业也可能以阻塞方式产生结果,具体取决于所使用的算子和连接模式。在这种情况下,必须等待上游 Task 先生成完整的结果,然后才能调度下游的接收 Task 运行。这能够提高批处理作业的效率并且占用更少的资源。 下表总结了 Task 输出类型以及调度策略的有效组合: ...

June 26, 2019 · 3 min · jiezi

一条数据的漫游奇遇记

阿里妹导读:数据库存储引擎是一个有历史的技术,经过数十年的发展,已经出现很多优秀成熟的产品。阿里巴巴 X-Engine 团队撰写的论文 "X-Engine: An Optimized Storage Engine for Large-scale E-Commerce Transaction Processing",详细讲述了团队在数据库存储引擎上所做的原创性工作,今年早些时候已经被 SIGMOD'19 Industrial Track 接收(SIGMOD 是数据库领域最重要也是最有影响力的会议之一)。本文将对这篇论文做一个前导性分析。背景X-Engine 是阿里数据库产品事业部自研的 OLTP 数据库存储引擎,作为自研数据库POLARDB X 的存储引擎,已经广泛应用在阿里集团内部诸多业务系统中,其中包括交易历史库,钉钉历史库等核心应用,为业务大幅缩减了成本,同时也作为双十一大促的关键数据库技术,挺过了数百倍平时流量的冲击。 数据库存储引擎是一个有历史的技术,经过数十年的发展,已经出现很多优秀成熟的产品。各式存储引擎已经在索引组织,缓存管理,事务处理,查询优化方方面面都做过细致的研究。即便如此,这个领域的演进仍在持续,每年都会涌现很多的新技术。 近年来,LSM (Log-Structured Merge-Tree)结构受到越来越多的关注,虽然这个技术本身出现很多年了,不算什么新事物,不过早先在 KV 存储系统中被应用的更多一些,近年开始在数据库存储引擎领域崭露头角,RocksDB 即是典型代表。 LSM 之所以变得流行,一是因为其简单,二是特点鲜明。写入模型是简单的追加,不会更新既有的数据,数据组织为简单的逻辑排序,由此带来的特点是写强而读弱,持久化数据只读的特点便于压缩。但是大多数数据库的应用场景其实都是读多写少的,直接使用 LSM 结构未必合适,想要另辟蹊径,须得扬长辟短。 架构X-Engine 使用了 LSM 作为基础架构,目标是作为一个通用的高性能低成本存储引擎,追求读写性能更为均衡,因此在其上做了大量的改进,主要围绕几个方向进行: 利用先天优势,持续优化写性能。优化 compaction 降低对系统性能的冲击,使得系统性能表现趋于平稳。利用持久化数据层只读特点,发挥压缩优势降低成本。利用天然分层结构,结合硬件能力使用冷热分层结构,降低综合成本。利用精细化访问机制和缓存技术,弥补读性能短板。X-Engine 的整体架构如下图,根据数据冷热进行分层代替 LSM 本身的持久化数据分层,热数据层和数据更新使用内存存储,利用了大量内存数据库的技术(Lock-Free index structure/append only)提高事务处理的性能,设计了一套事务处理流水线处理机制,把事务处理的几个阶段并行起来,提升吞吐。而访问频度低的冷(温)数据逐渐淘汰或是合并到持久化的存储层次中,结合当前丰富的存储设备层次体系(NVM/SSD/HDD)进行存储。 我们对性能影响比较大的 compaction 过程做了大量优化,主要是拆分数据存储粒度,利用数据更新热点较为集中的特征,尽可能的在合并过程中复用数据,精细化控制 LSM 的形状,减少 I/O 和计算代价,并同时极大的减少了合并过程中的空间放大。同时使用更细粒度的访问控制和缓存机制,优化读的性能。 既然 X-Engine 是以 LSM 为基础架构的,所以一切还要从 LSM 本身说起。 LSM基本逻辑一条数据在 LSM 结构中的旅程,从写入 WAL(Write Ahead Log) 开始,然后进入MemTable,这是 Ta 整个生命周期的第一处落脚点。随后,flush 操作将 Ta 刻在更稳固的介质上,compaction 操作将Ta带往更深远的去处,或是在途中丢弃,取决于 Ta 的继任者何时到来。 ...

June 25, 2019 · 2 min · jiezi

同学要不要来挑战双11零点流量洪峰

阿里妹导读:双十一的零点,整个电商系统的请求速率到达峰值。如果将这些请求流量只分配给少部分 server,这些机器接收到的请求速率会远超过处理速率,新来的任务来不及处理,就会产生请求任务堆积。今年的中间件性能挑战赛就围绕“挑战双11零点流量洪峰”展开。自2015年开始,中间件性能挑战赛已经成功举办了四届,被历年大赛选手称为“中间件技术的风向标”。接下来,跟随阿里巴巴中间件团队的郭浩,一起来围观赛题,解读赛题。 在现代分布式应用中,服务请求是由物理机或虚拟机组成的 server 池进行处理的。 通常,server 池规模巨大且服务容量各不相同,受网络、内存、CPU、下游服务等各种因素影响,一个 server 的服务容量始终处于动态变动和趋于稳定的状态,如何设计和实现这种系统的负载均衡算法是一个极具挑战的难题。 自适应负载均衡的需求背景负载均衡有两个主要目标: 保持较短的请求响应时间和较小的请求阻塞概率;负载均衡算法的 overhead 在可控级别,不占用过多的 CPU 、网络等资源。自适应负载均衡是指无论系统处于空闲、稳定还是繁忙状态,负载均衡算法都会自动评估系统的服务能力,进行合理的流量分配,使整个系统始终保持较好的性能,不产生饥饿或者过载、宕机。 这种算法对于现在的电商系统、数据中心、云计算等领域都很有必要,使用自适应负载均衡能够更合理的利用资源,提高性能。 对用户而言,一旦产生任务堆积,请求会变慢甚至超时,体验严重下降,甚至导致服务不可用。而处理请求的机器也会由于堆积的任务越来越多而发生严重过载,直到被打垮。剩余的尚未宕机的其它机器会逐渐重复这个过程,直至整个应用不可用,发生系统故障。 为了避免这种情况发生,我们可能会想到一种常用的办法:在服务上线前提前进行压测,使用压测的容量作为限流值,当线上服务的请求速率大于限流值的时候,服务拒绝新到的服务,从而保障服务始终可用。但是这种方式也存在问题:压测时测试的容量进行限流通常会趋于保守,不能充分发挥异构系统的全部性能;也无法智能地应对由于网络、下游服务变化而导致的容量下降等问题,系统仍然存在宕机风险。 因此,我们需要具备自适应能力的负载均衡算法,来更好地进行流量分配调度以及稳定性保障,追求极致性能,挑战大促等场景下的流量洪峰。 结合中间件性能挑战赛的赛题 我们结合「第五届中间件性能挑战赛」中的初赛场景,来一起探讨一下设计和实现一个自适应的负载均衡的基本思路。 本次挑战赛的场景由施压程序(阿里云性能测试PTS)、服务调用方(Consumer)和三个规格不同的服务提供方(Provider) 组成。在评测过程中,每个程序都部署在不同的物理机上,以避免因 CPU、网络资源的竞争,导致评测程序抖动,影响最终评测成绩。 Becnhmarker 负责请求 Consumer, Consumer 收到请求后,从三台物理规格不同、服务响应时间和最大并发都不同的 Provider 中选择一个进行调用并返回结果。选择哪一个 Provider 进行调用的流程就是本次挑战赛需要实现的负载均衡算法。 为了简化环境部署和提升性能,本次挑战赛没有使用服务注册和发现机制。三个 Provider 对应的 URL 都已经被直接配置在了 Consumer 中,选手在开发和测试时可直接通过 Provider-small 等 hostname 访问相应的 Provider。 赛题分析题目描述很简单,不考虑 Consumer 直接拒绝的情况下,场景可以简化为 3 选 1 的问题,但如何进行这个决策则是本次挑战赛考察的难点和重点。 官方题目组提供了 Random 算法作为默认实现:从 3 个 Provider 中随机取任意一个。对于单 dispatcher (在本次赛题中是 Consumer) 同构系统的场景,Random可以达到渐近负载均衡, 每个 Provider 接收到的总请求数接近。但是对于多 dispatcher 或异构系统而言,Random 算法由于缺少全局状态,无法保证全局随机,极端条件下,多个 dispatcher 可能将请求同时分配到一台 Provider 上,导致系统存在服务过载和宕机的风险;异构系统中,不同 Provider 服务容量实际是不同的,即使每个 Provider 请求速率相同也会产生空闲、稳定、过载等不同的服务状态,无法实现最优流量分配,更不能做到响应时间最小。显而易见,Random 并不是符合赛题要求的自适应算法。 ...

June 21, 2019 · 1 min · jiezi

再一次生产-CPU-高负载排查实践

前言前几日早上打开邮箱收到一封监控报警邮件:某某 ip 服务器 CPU 负载较高,请研发尽快排查解决,发送时间正好是凌晨。 其实早在去年我也处理过类似的问题,并记录下来:《一次生产 CPU 100% 排查优化实践》 不过本次问题产生的原因却和上次不太一样,大家可以接着往下看。 <!--more--> 问题分析收到邮件后我马上登陆那台服务器,看了下案发现场还在(负载依然很高)。 于是我便利用这类问题的排查套路定位一遍。 首先利用 top -c 将系统资源使用情况实时显示出来 (-c 参数可以完整显示命令)。 接着输入大写 P 将应用按照 CPU 使用率排序,第一个就是使用率最高的程序。 果不其然就是我们的一个 Java 应用。 这个应用简单来说就是定时跑一些报表使的,每天凌晨会触发任务调度,正常情况下几个小时就会运行完毕。 常规操作第二步自然是得知道这个应用中最耗 CPU 的线程到底再干嘛。 利用 top -Hp pid 然后输入 P 依然可以按照 CPU 使用率将线程排序。 这时我们只需要记住线程的 ID 将其转换为 16 进制存储起来,通过 jstack pid >pid.log 生成日志文件,利用刚才保存的 16 进制进程 ID 去这个线程快照中搜索即可知道消耗 CPU 的线程在干啥了。 如果你嫌麻烦,我也强烈推荐阿里开源的问题定位神器 arthas 来定位问题。 比如上述操作便可精简为一个命令 thread -n 3 即可将最忙碌的三个线程快照打印出来,非常高效。 更多关于 arthas 使用教程请参考官方文档。由于之前忘记截图了,这里我直接得出结论吧: ...

June 18, 2019 · 1 min · jiezi

如何设计和实现自适应的负载均衡

本文是第五届中间件性能挑战赛的赛题解析,参与比赛,赢取最高10万元奖金。 在现代分布式应用中,服务请求是由物理机或虚拟机组成的 server 池进行处理的。 通常,server 池规模巨大且服务容量各不相同,受网络、内存、CPU、下游服务等各种因素影响,一个 server 的服务容量始终处于动态变动和趋于稳定的状态,如何设计和实现这种系统的负载均衡算法是一个极具挑战的难题。 阿里巴巴中间件公众号对话框发送“挑战赛”,获取上一届优秀选手的解题思路,点击这里。 自适应负载均衡的需求背景负载均衡有两个主要目标: 保持较短的请求响应时间和较小的请求阻塞概率;负载均衡算法的 overhead 在可控级别,不占用过多的 CPU 、网络等资源。自适应负载均衡是指无论系统处于空闲、稳定还是繁忙状态,负载均衡算法都会自动评估系统的服务能力,进行合理的流量分配,使整个系统始终保持较好的性能,不产生饥饿或者过载、宕机。 这种算法对于现在的电商系统、数据中心、云计算等领域都很有必要,使用自适应负载均衡能够更合理的利用资源,提高性能。例如,在双十一零点,用户集中下单支付,整个电商系统的请求速率到达峰值。如果将这些请求流量只分配给少部分 server,这些机器接收到的请求速率会远超过处理速率,新来的任务来不及处理,产生请求任务堆积。 对用户而言,一旦产生任务堆积,请求会变慢甚至超时,体验严重下降,甚至导致服务不可用。而处理请求的机器也会由于堆积的任务越来越多而发生严重过载,直到被打垮。剩余的尚未宕机的其它机器会逐渐重复这个过程,直至整个应用不可用,发生系统故障。 为了避免这种情况发生,我们可能会想到一种常用的办法:在服务上线前提前进行压测,使用压测的容量作为限流值,当线上服务的请求速率大于限流值的时候,服务拒绝新到的服务,从而保障服务始终可用。但是这种方式也存在问题:压测时测试的容量进行限流通常会趋于保守,不能充分发挥异构系统的全部性能;也无法智能地应对由于网络、下游服务变化而导致的容量下降等问题,系统仍然存在宕机风险。 因此,我们需要具备自适应能力的负载均衡算法,来更好的进行流量分配调度以及稳定性保障,追求极致性能,挑战大促等场景下的流量洪峰。 结合中间件性能挑战赛的赛题 我们结合「第五届中间件性能挑战赛」中的初赛场景,来一起探讨一下设计和实现一个自适应的负载均衡的基本思路。 本次挑战赛的场景由施压程序(阿里云性能测试PTS)、服务调用方(Consumer)和三个规格不同的服务提供方(Provider) 组成。在评测过程中,每个程序都部署在不同的物理机上,以避免因 CPU、网络资源的竞争,导致评测程序抖动,影响最终评测成绩。 Becnhmarker 负责请求 Consumer, Consumer 收到请求后,从三台物理规格不同、服务响应时间和最大并发都不同的 Provider 中选择一个进行调用并返回结果。选择哪一个 Provider 进行调用的流程就是本次挑战赛需要实现的负载均衡算法。 为了简化环境部署和提升性能,本次挑战赛没有使用服务注册和发现机制。三个 Provider 对应的 URL 都已经被直接配置在了 Consumer 中,选手在开发和测试时可直接通过 Provider-small 等 hostname 访问相应的 Provider。 赛题分析题目描述很简单,不考虑 Consumer 直接拒绝的情况下,场景可以简化为 3 选 1 的问题,但如何进行这个决策则是本次挑战赛考察的难点和重点。 官方题目组提供了Random算法作为默认实现:从 3 个 Provider 中随机取任意一个。对于单 dispatcher (在本次赛题中是 Consumer) 同构系统的场景,Random可以达到渐近负载均衡, 每个 Provider 接收到的总请求数接近。但是对于多 dispatcher 或异构系统而言,Random 算法由于缺少全局状态,无法保证全局随机,极端条件下,多个 dispatcher 可能将请求同时分配到一台 Provider 上,导致系统存在服务过载和宕机的风险;异构系统中,不同 Provider 服务容量实际是不同的,即使每个 Provider 请求速率相同也会产生空闲、稳定、过载等不同的服务状态,无法实现最优流量分配,更不能做到响应时间最小。显而易见,Random并不是符合赛题要求的自适应算法。 ...

June 14, 2019 · 1 min · jiezi

全栈之路JAVA基础课程三20190614v10

欢迎进入JAVA基础课程 本系列文章将主要针对JAVA一些基础知识点进行讲解,为平时归纳所总结,不管是刚接触JAVA开发菜鸟还是业界资深人士,都希望对广大同行带来一些帮助。若有问题请及时留言或加QQ:243042162。 谨记:最近习大大大力倡导“不忘初心、牢记使命”的主题教育,提出了“守初心、担使命、找差距、抓落实”的总体要求,这正好也映射在了我们个人生活和工作中。人到中年,最多的是懒,缺乏了学生时代的初心,不管你处于哪个年纪,请摆脱借口,端正态度,以家庭为寄托,以未来为展望,将技术提升和家庭教育落到实处,让自己有生之年还能得到质的飞跃。并发和多线程1. 进程和线程 进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 线程:进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 区别:进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。 a.创建线程的几种方式(1)继承 Thread(2)实现 Runnable 接口(3)应用程序可以使用 Executor 框架来创建线程池b.线程的几种状态(早上打车去上班)(1)新建(准备叫一辆嘀嘀打车)(2)可运行(找到一辆可以带你去上班的车)(3)运行(司机接到你,带你去上班)(4)阻塞(路上堵车了):等待阻塞-wait、同步阻塞-同步锁、其他阻塞- Thread.sleep(long ms)或 t.join ()方法(5)死亡(到公司了,付钱下车)c.同步方法和同步代码块同步方法默认用 this 或者当前类 class 对象作为锁;同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法。死锁概念:两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。举例:某计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。产生死锁原因:1. 系统资源的竞争2. 进程推进顺序非法3. 死锁产生的必要条件产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。(1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。(2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。(3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。 代码块/** * 死锁实例 * t1先运行,这个时候flag==true,先锁定obj1,然后睡眠1秒钟 * 而t1在睡眠的时候,另一个线程t2启动,flag==false,先锁定obj2,然后也睡眠1秒钟 * t1睡眠结束后需要锁定obj2才能继续执行,而此时obj2已被t2锁定 * t2睡眠结束后需要锁定obj1才能继续执行,而此时obj1已被t1锁定 * t1、t2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 */ class DeadLock implements Runnable{ private static Object obj1 = new Object(); private static Object obj2 = new Object(); private boolean flag; public DeadLock(boolean flag){ this.flag = flag; } @Override public void run(){ System.out.println(Thread.currentThread().getName() + "运行"); if(flag){ synchronized(obj1){ System.out.println(Thread.currentThread().getName() + "已经锁住obj1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj2){ // 执行不到这里 System.out.println("1秒钟后,"+Thread.currentThread().getName() + "锁住obj2"); } } }else{ synchronized(obj2){ System.out.println(Thread.currentThread().getName() + "已经锁住obj2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj1){ // 执行不到这里 System.out.println("1秒钟后,"+Thread.currentThread().getName() + "锁住obj1"); } } } }}public class LockDemo { public static void main(String[] args) { Thread t1 = new Thread(new DeadLock(true), "线程1"); Thread t2 = new Thread(new DeadLock(false), "线程2"); t1.start(); t2.start(); }}运行结果 ...

June 14, 2019 · 1 min · jiezi

走进KeyDB

KeyDB项目是从redis fork出来的分支。众所周知redis是一个单线程的kv内存存储系统,而KeyDB在100%兼容redis API的情况下将redis改造成多线程。 项目git地址:https://github.com/JohnSully/KeyDB 网上公开的技术细节比较少,本文基本是通过阅读源码总结出来的,如有错漏之处欢迎指正。 多线程架构线程模型KeyDB将redis原来的主线程拆分成了主线程和worker线程。每个worker线程都是io线程,负责监听端口,accept请求,读取数据和解析协议。如图所示: KeyDB使用了SO_REUSEPORT特性,多个线程可以绑定监听同个端口。每个worker线程做了cpu绑核,读取数据也使用了SO_INCOMING_CPU特性,指定cpu接收数据。解析协议之后每个线程都会去操作内存中的数据,由一把全局锁来控制多线程访问内存数据。主线程其实也是一个worker线程,包括了worker线程的工作内容,同时也包括只有主线程才可以完成的工作内容。在worker线程数组中下标为0的就是主线程。主线程的主要工作在实现serverCron,包括: 处理统计客户端链接管理db数据的resize和reshard处理aofreplication主备同步cluster模式下的任务链接管理在redis中所有链接管理都是在一个线程中完成的。在KeyDB的设计中,每个worker线程负责一组链接,所有的链接插入到本线程的链接列表中维护。链接的产生、工作、销毁必须在同个线程中。每个链接新增一个字段int iel; /* the event loop index we're registered with */用来表示链接属于哪个线程接管。KeyDB维护了三个关键的数据结构做链接管理: clients_pending_write:线程专属的链表,维护同步给客户链接发送数据的队列clients_pending_asyncwrite:线程专属的链表,维护异步给客户链接发送数据的队列clients_to_close:全局链表,维护需要异步关闭的客户链接分成同步和异步两个队列,是因为redis有些联动api,比如pub/sub,pub之后需要给sub的客户端发送消息,pub执行的线程和sub的客户端所在线程不是同一个线程,为了处理这种情况,KeyDB将需要给非本线程的客户端发送数据维护在异步队列中。同步发送的逻辑比较简单,都是在本线程中完成,以下图来说明如何同步给客户端发送数据: 如上文所提到的,一个链接的创建、接收数据、发送数据、释放链接都必须在同个线程执行。异步发送涉及到两个线程之间的交互。KeyDB通过管道在两个线程中传递消息: int fdCmdWrite; //写管道int fdCmdRead; //读管道本地线程需要异步发送数据时,先检查client是否属于本地线程,非本地线程获取到client专属的线程ID,之后给专属的线程管到发送AE_ASYNC_OP::CreateFileEvent的操作,要求添加写socket事件。专属线程在处理管道消息时将对应的请求添加到写事件中,如图所示: redis有些关闭客户端的请求并非完全是在链接所在的线程执行关闭,所以在这里维护了一个全局的异步关闭链表。 锁机制KeyDB实现了一套类似spinlock的锁机制,称之为fastlock。fastlock的主要数据结构有: struct ticket{ uint16_t m_active; //解锁+1 uint16_t m_avail; //加锁+1};struct fastlock{ volatile struct ticket m_ticket; volatile int m_pidOwner; //当前解锁的线程id volatile int m_depth; //当前线程重复加锁的次数};使用原子操作__atomic_load_2,__atomic_fetch_add,__atomic_compare_exchange来通过比较m_active=m_avail判断是否可以获取锁。fastlock提供了两种获取锁的方式: try_lock:一次获取失败,直接返回lock:忙等,每1024 * 1024次忙等后使用sched_yield 主动交出cpu,挪到cpu的任务末尾等待执行。在KeyDB中将try_lock和事件结合起来,来避免忙等的情况发生。每个客户端有一个专属的lock,在读取客户端数据之前会先尝试加锁,如果失败,则退出,因为数据还未读取,所以在下个epoll_wait处理事件循环中可以再次处理。 Active-ReplicaKeyDB实现了多活的机制,每个replica可设置成可写非只读,replica之间互相同步数据。主要特性有: 每个replica有个uuid标志,用来去除环形复制新增加rreplay API,将增量命令打包成rreplay命令,带上本地的uuidkey,value加上时间戳版本号,作为冲突校验,如果本地有相同的key且时间戳版本号大于同步过来的数据,新写入失败。采用当前时间戳向左移20位,再加上后44位自增的方式来获取key的时间戳版本号。结束语云数据库Redis版(ApsaraDB for Redis)是一种稳定可靠、性能卓越、可弹性伸缩的数据库服务。基于飞天分布式系统和全SSD盘高性能存储,支持主备版和集群版两套高可用架构。提供了全套的容灾切换、故障迁移、在线扩容、性能优化的数据库解决方案。 本文作者:羽洵阅读原文 本文为云栖社区原创内容,未经允许不得转载。

June 14, 2019 · 1 min · jiezi

Logtail提升采集性能

默认性能限制为防止滥用消耗过多机器资源,我们对默认安装的Logtail进行了一系列的资源限制。默认安装的Logtail最多日志采集速度为20M/s,20个并发发送。 其他资源限制请参考:启动参数 https://help.aliyun.com/docum... 中的默认配置。 采集能力单核能力 如果放开发送流控,Logtail默认单核的能力大致如下(具体根据不同正则、日志类型、采集提取的key数量、机器配置等会有一定浮动): 备注:测试环境 CPU :Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz MEM : 64GB OS : Linux version 2.6.32-220.23.2.ali1113.el5.x86_64 多核能力 Logtail默认只开一个线程处理数据,如果开启多核,性能会有提升,但并不是线性关系,实测最多开到8个线程后,性能几乎没有上涨。 极简模式最高性能可达:440MB/s正则最高性能可达:70MB/s分隔符最高性可达:75MB/sJSON最高性能可达:75MB/s日志格式建议根据您的使用目的,合理选择对应的日志格式 搬数据:使用极简模式,性能最高数据分析:多字符分隔符>单字符分隔符>JSON模式>正则模式Java堆栈类型数据:正则模式注意:正则模式采集性能和正则优化有非常大关系。如何放开资源限制可通过调整Logtail的启动参数来放开默认的资源限制,下面我们推荐2种配置方式: 注意:Logtail使用短连接发送数据,如果发送并发过高,建议调整服务器的tcp参数,防止过多time_wait调整方式:sudo sysctl -w net.ipv4.tcp_tw_timeout=5单核小资模式 在配置文件末尾追加以下两个参数,注意JSON需合法。 多核极致模式 在配置文件末尾追加以下几个参数,需保证,注意JSON需合法。 注意:需保证 cpu_usage_limit > process_thread_count 本文作者:元乙 原文链接 本文为云栖社区原创内容,未经允许不得转载。

June 6, 2019 · 1 min · jiezi

漫谈分布式计算框架

摘要: 本文主要谈了一些分布式计算框架方面的心得。如果问 mapreduce 和 spark 什么关系,或者说有什么共同属性,你可能会回答他们都是大数据处理引擎。如果问 spark 与 tensorflow 呢,就可能有点迷糊,这俩关注的领域不太一样啊。但是再问 spark 与 MPI 呢?这个就更远了。虽然这样问多少有些不严谨,但是它们都有共同的一部分,这就是我们今天谈论的一个话题,一个比较大的话题:分布式计算框架。 不管是 mapreduce,还是 spark 亦或 tensorflow,它们都是利用分布式的能力,运行某些计算,解决一些特定的问题。从这个 level 讲,它们都定义了一种“分布式计算模型”,即提出了一种计算的方法,通过这种计算方法,就能够解决大量数据的分布式计算问题。它们的区别在于提出的分布式计算模型不同。Mapreduce 正如其名,是一个很基本的 map-reduce 式的计算模型(好像没说一样)。Spark 定义了一套 RDD 模型,本质上是一系列的 map/reduce 组成的一个 DAG 图。Tensorflow 的计算模型也是一张图,但是 tensorflow 的图比起 spark 来,显得更“复杂”一点。你需要为图中的每个节点和边作出定义。根据这些定义,可以指导 tensorflow 如何计算这张图。Tensorflow 的这种具体化的定义使它比较适合处理特定类型的的计算,对 tensorflow 来讲就是神经网络。而 spark 的 RDD 模型使它比较适合那种没有相互关联的的数据并行任务。那么有没有一种通用的、简单的、性能还高的分布式计算模型?我觉着挺难。通用往往意味着性能不能针对具体情形作出优化。而为专门任务写的分布式任务又做不到通用,当然也做不到简单。 插一句题外话,分布式计算模型有一块伴随的内容,就是调度。虽然不怎么受关注,但这是分布式计算引擎必备的东西。mapreduce 的调度是 yarn,spark 的调度有自己内嵌的调度器,tensorflow 也一样。MPI 呢?它的调度就是几乎没有调度,一切假设集群有资源,靠 ssh 把所有任务拉起来。调度实际上应当分为资源调度器和任务调度器。前者用于向一些资源管理者申请一些硬件资源,后者用于将计算图中的任务下发到这些远程资源进行计算,其实也就是所谓的两阶段调度。近年来有一些 TensorflowOnSpark 之类的项目。这类项目的本质实际上是用 spark 的资源调度,加上 tensorflow 的计算模型。 当我们写完一个单机程序,而面临数据量上的问题的时候,一个自然的想法就是,我能不能让它运行在分布式的环境中?如果能够不加改动或稍加改动就能让它分布式化,那就太好了。当然现实是比较残酷的。通常情况下,对于一个一般性的程序,用户需要自己手动编写它的分布式版本,利用比如 MPI 之类的框架,自己控制数据的分发、汇总,自己对任务的失败做容灾(通常没有容灾)。如果要处理的目标是恰好是对一批数据进行批量化处理,那么 可以用 mapreduce 或者 spark 预定义的 api。对于这一类任务,计算框架已经帮我们把业务之外的部分(脚手架代码)做好了。同样的,如果我们的任务是训练一个神经网络,那么用 tensorflow pytorch 之类的框架就好了。这段话的意思是,如果你要处理的问题已经有了对应框架,那么拿来用就好了。但是如果没有呢?除了自己实现之外有没有什么别的办法呢? ...

June 6, 2019 · 2 min · jiezi

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

前言前段时间写过一篇《线程池没你想的那么简单》,和大家一起撸了一个基本的线程池,具备: 线程池基本调度功能。线程池自动扩容缩容。队列缓存线程。关闭线程池。这些功能,最后也留下了三个待实现的 features 。 执行带有返回值的线程。异常处理怎么办?所有任务执行完怎么通知我?这次就实现这三个特性来看看 j.u.c 中的线程池是如何实现这些需求的。 再看本文之前,强烈建议先查看上文《线程池没你想的那么简单》任务完成后的通知大家在用线程池的时候或多或少都会有这样的需求: 线程池中的任务执行完毕后再通知主线程做其他事情,比如一批任务都执行完毕后再执行下一波任务等等。 以我们之前的代码为例: 总共往线程池中提交了 13 个任务,直到他们都执行完毕后再打印 “任务执行完毕” 这个日志。执行结果如下: 为了简单的达到这个效果,我们可以在初始化线程池的时候传入一个接口的实现,这个接口就是用于任务完成之后的回调。 public interface Notify { /** * 回调 */ void notifyListen() ;}以上就是线程池的构造函数以及接口的定义。 所以想要实现这个功能的关键是在何时回调这个接口? 仔细想想其实也简单:只要我们记录提交到线程池中的任务及完成的数量,他们两者的差为 0 时就认为线程池中的任务已执行完毕;这时便可回调这个接口。 所以在往线程池中写入任务时我们需要记录任务数量: 为了并发安全的考虑,这里的计数器采用了原子的 AtomicInteger 。 而在任务执行完毕后就将计数器 -1 ,一旦为 0 时则任务任务全部执行完毕;这时便可回调我们自定义的接口完成通知。 JDK 的实现这样的需求在 jdk 中的 ThreadPoolExecutor 中也有相关的 API ,只是用法不太一样,但本质原理都大同小异。 我们使用 ThreadPoolExecutor 的常规关闭流程如下: executorService.shutdown(); while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) { logger.info("thread running"); }线程提交完毕后执行 shutdown() 关闭线程池,接着循环调用 awaitTermination() 方法,一旦任务全部执行完毕后则会返回 true 从而退出循环。 ...

June 6, 2019 · 2 min · jiezi

MySQL80-新特性-说说InnoDB-Log-System的隐藏参数

InnoDB在设计lock-free的log system时,除了已有的参数外,还通过宏控制隐藏了一些参数,如果你使用源码编译时,打开cmake选项-DENABLE_EXPERIMENT_SYSVARS=1, 就可以看到这些参数了。本文主要简单的过一下这些隐藏的参数所代表的含义 A.innodb_log_write_eventsinnodb_log_flush_events两者的含义类似,表示用来唤醒等待log write/flush的event的个数,默认值都是2048比如你要等待的位置在lsnA,那么计算的slot为:slot = (lsnA - 1) /OS_FILE_LOG_BLOCK_SIZE & (innodb_log_write/flush_events - 1)这意味着:如果事务的commit log的end lsn落在相同block里,他们可能产生event的竞争当然如果不在同一个block的时候,如果调大参数,就可以减少竞争,但也会有无效的唤醒唤醒操作通常由后台线程log_write_notifier 或者log_flush_notifier异步来做,但如果推进的log write/flush还不足一个block的话,那就log_writter/flusher自己去唤醒了。 B.innodb_log_recent_written_size, 默认1MB表示recent_written这个link_buf的大小,其实控制了并发往log buffer中同时拷贝的事务日志量,向前由新的日志加入,后面由log writer通过写日志向前推进,如果写的慢的话,那这个link_buf很可能用满,用户线程就得spin等待。再慢io的系统上,我们可以稍微调大这个参数 innodb_Log_recent_closed_size, 默认2MB表示recent closed这个link_buf的大小,也是维护可以并发往flush list上插入脏页的并罚度,如果插入脏页速度慢,或者lin_buf没有及时合并推进,就会spin wait 简单说下link_buf, 这本质上是一个数组,但使用无锁的使用方式来维护lsn的推进,比如获得一个lsn开始和结束,那就通过设置buf[start_lsn] = end_lsn的类似方式来维护lsn链,基于lsn是连续值的事实,最终必然不会出现空洞,所以在演化的过程中,可以从尾部推进连续的lsn,头部插入新的值.如果新插入的值超过了尾部,表示buf满了,就需要spin wait了C.innodb_log_wait_for_write_spin_delay, innodb_log_wait_for_write_timeout 从8.0版本开始用户线程不再自己去写redo,而是等待后台线程去写,这两个变量控制了spin以及condition wait的timeout时间,当spin一段时间还没推进到某个想要的lsn点时,就会进入condition wait 另外两个变量innodb_log_wait_for_flush_spin_delayinnodb_log_wait_for_flush_timeout含义类似,但是是等待log flush到某个指定lsn 注意在实际计算过程中,最大spin次数,会考虑到cpu利用率,以及另外两个参数:innodb_log_spin_cpu_abs_lwminnodb_log_spin_cpu_pct_hwm 如果是等待flush操作的话,还收到参数innodb_log_wait_for_flush_spin_hwm限制,该参数控制了等待flush的时间上限,如果平均等待flush的时间超过了这个上限的话, 就没必要去spin,而是直接进入condition wait 关于spin次数的计算方式在函数log_max_spins_when_waiting_in_user_thread中": 函数的参数即为配置项innodb_log_wait_for_write_spin_delay或innodb_log_wait_for_flush_spin_delay值 static inline uint64_t log_max_spins_when_waiting_in_user_thread( uint64_t min_non_zero_value) { uint64_t max_spins; /* Get current cpu usage. */ const double cpu = srv_cpu_usage.utime_pct; /* Get high-watermark - when cpu usage is higher, don't spin! */ const uint32_t hwm = srv_log_spin_cpu_pct_hwm; if (srv_cpu_usage.utime_abs < srv_log_spin_cpu_abs_lwm || cpu >= hwm) { /* Don't spin because either cpu usage is too high or it's almost idle so no reason to bother. */ max_spins = 0; } else if (cpu >= hwm / 2) { /* When cpu usage is more than 50% of the hwm, use the minimum allowed number of spin rounds, not to increase cpu usage too much (risky). */ max_spins = min_non_zero_value; } else { /* When cpu usage is less than 50% of the hwm, choose maximum spin rounds in range [minimum, 10*minimum]. Smaller usage of cpu is, more spin rounds might be used. */ const double r = 1.0 * (hwm / 2 - cpu) / (hwm / 2); max_spins = static_cast<uint64_t>(min_non_zero_value + r * min_non_zero_value * 9); } return (max_spins);}D.以下几个参数是后台线程等待任务时spin及condition wait timeout的值log_writer线程:innodb_log_writer_spin_delay,innodb_log_writer_timeout ...

June 4, 2019 · 2 min · jiezi

Schedulerx20分布式计算原理最佳实践

1. 前言Schedulerx2.0的客户端提供分布式执行、多种任务类型、统一日志等框架,用户只要依赖schedulerx-worker这个jar包,通过schedulerx2.0提供的编程模型,简单几行代码就能实现一套高可靠可运维的分布式执行引擎。 这篇文章重点是介绍基于schedulerx2.0的分布式执行引擎原理和最佳实践,相信看完这篇文章,大家都能写出高效率的分布式作业,说不定速度能提升好几倍:) 2. 可扩展的执行引擎Worker总体架构参考Yarn的架构,分为TaskMaster, Container, Processor三层: TaskMaster:类似于yarn的AppMaster,支持可扩展的分布式执行框架,进行整个jobInstance的生命周期管理、container的资源管理,同时还有failover等能力。默认实现StandaloneTaskMaster(单机执行),BroadcastTaskMaster(广播执行),MapTaskMaster(并行计算、内存网格、网格计算),MapReduceTaskMaster(并行计算、内存网格、网格计算)。Container:执行业务逻辑的容器框架,支持线程/进程/docker/actor等。Processor:业务逻辑框架,不同的processor表示不同的任务类型。以MapTaskMaster为例,大概的原理如下图所示: 3. 分布式编程模型之Map模型Schedulerx2.0提供了多种分布式编程模型,这篇文章主要介绍Map模型(之后的文章还会介绍MapReduce模型,适用更多的业务场景),简单几行代码就可以将海量数据分布式到多台机器上进行分布式跑批,非常简单易用。 针对不同的跑批场景,map模型作业还提供了并行计算、内存网格、网格计算三种执行方式: 并行计算:子任务300以下,有子任务列表。内存网格:子任务5W以下,无子任务列表,速度快。网格计算:子任务100W以下,无子任务列表。4. 并行计算原理因为并行任务具有子任务列表: 如上图,子任务列表可以看到每个子任务的状态、机器,还有重跑、查看日志等操作。 因为并行计算要做到子任务级别的可视化,并且worker挂了、重启还能支持手动重跑,就需要把task持久化到server端: 如上图所示: server触发jobInstance到某个worker,选中为master。MapTaskMaster选择某个worker执行root任务,当执行map方法时,会回调MapTaskMaster。MapTaskMaster收到map方法,会把task持久化到server端。同时,MapTaskMaster还有个pull线程,不停拉取INIT状态的task,并派发给其他worker执行。5. 网格计算原理网格计算要支持百万级别的task,如果所有任务都往server回写,server肯定扛不住,所以网格计算的存储实际上是分布式在用户自己的机器上的: 如上图所示: server触发jobInstance到某个worker,选中为master。MapTaskMaster选择某个worker执行root任务,当执行map方法时,会回调MapTaskMaster。MapTaskMaster收到map方法,会把task持久化到本地h2数据库。同时,MapTaskMaster还有个pull线程,不停拉取INIT状态的task,并派发给其他worker执行。6. 最佳实践6.1 需求 举个例子: 读取A表中status=0的数据。处理这些数据,插入B表。把A表中处理过的数据的修改status=1。数据量有4亿+,希望缩短时间。6.2 反面案例 我们先看下如下代码是否有问题? public class ScanSingleTableProcessor extends MapJobProcessor { private static int pageSize = 1000; @Override public ProcessResult process(JobContext context) { String taskName = context.getTaskName(); Object task = context.getTask(); if (WorkerConstants.MAP_TASK_ROOT_NAME.equals(taskName)) { int recordCount = queryRecordCount(); int pageAmount = recordCount / pageSize;//计算分页数量 for(int i = 0 ; i < pageAmount ; i ++) { List<Record> recordList = queryRecord(i);//根据分页查询一页数据 map(recordList, "record记录");//把子任务分发出去并行处理 } return new ProcessResult(true);//true表示执行成功,false表示失败 } else if ("record记录".equals(taskName)) { //TODO return new ProcessResult(true); } return new ProcessResult(false); }}如上面的代码所示,在root任务中,会把数据库所有记录读取出来,每一行就是一个Record,然后分发出去,分布式到不同的worker上去执行。逻辑是没有问题的,但是实际上性能非常的差。结合网格计算原理,我们把上面的代码绘制成下面这幅图: ...

May 31, 2019 · 2 min · jiezi

MySQL-80-技术详解

MySQL 8.0 简介MySQL 5.7 到 8.0,Oracle 官方跳跃了 Major Version 版本号,随之而来的就是在 MySQL 8.0 上做了许多重大更新,在往企业级数据库的路上大步前行,全新 Data Dictionary 设计,支持 Atomic DDL,全新的版本升级策略,安全和账号管理加强,InnoDB 功能增强等,目前小版本已经 release 到 8.0.16,新的功能仍然在持续推出。RDS MySQL 8.0 产品是阿里云推出的 MySQL 系列云产品之一,使用完全兼容 MySQL 8.0 的阿 里云 AliSQL 8.0 分支,除了官方在 MySQL 8.0 推出的全新功能外,AliSQL 沉淀了许多在 Alibaba 集团电商业务和云上几十万客户在使用 MySQL 过程中遇到的问题和需求,以此来加固AliSQL, 提升 AliSQL 的性能和稳定性。下面分别对 MySQL 8.0 和 AliSQL 8.0 相关的版本和功能做简短的介绍: MySQL 8.0 版本更新1. 数据字典 MySQL 8.0 摒弃了 Server Layer 定义的 FRM 文件和其它非事务表,使用了一组 InnoDB 表来 保存数据字典,支持事务特性。 ...

May 30, 2019 · 2 min · jiezi

开源性能可视化工具FlameScope模式识别

文章翻译原文链接 FlameScope是一个新的开源性能可视化工具,它使用次秒级偏移热图和火焰图来分析周期活动、方差、扰动。我们在Netflix TechBlog上面,发表了技术文章Netflix FlameScope,以及工具的源代码。火焰图很好理解,次秒级偏移热图理解起来要困难些(我最近发明的它)。FlameScope可以该帮助你理解后者。 总而言之,次秒级偏移热图是这样的:x轴是一整秒,y轴是这一秒里的几分之一秒。这每个几分之一秒都被称作一个桶(或者说盒),表示这几分之一秒里,事件数量的聚合。盒子颜色深度表示发生的次数,颜色越深表示次数越多。 下图一个真实的CPU上的次秒级偏移热图样本: 这张图中能分析出什么信息来呢?为了能把各种不同模式区分开来展示,我在这篇文章里先画了一些人工合成的样本。实际使用FlameScope工具时,可以选择你的各个模式,还能生成火焰图,显示对应的代码路径(这里我不展示火焰图)。 周期活动1 . 一个线程,每秒一次 线程在每秒钟内的同样的偏移里醒来,做几毫秒的工作,然后回到睡眠。 2 . 一个线程,两次每秒 每500ms唤醒一次。既可能是两个线程,也可能是一个线程500ms 唤醒一次。 3 . 两个线程 看起来像两个线程均1s唤醒一次 4 . 一个忙等待线程,每秒一次 这个线程做约20ms的工作,然后睡1s。这是一个常见的模式,导致每秒钟唤醒抵消匍匐前进。 5 . 一个忙等待线程,两次每秒 每500ms唤醒一次。有可能是单线程程序,每秒唤醒两次。 6 . 一个计算较密集的忙等线程 斜率高,每秒做更多的工作,大约是80毫秒。 7 . 一个计算较不密集的忙等线程 斜率低,每秒做的工作较少,可能只有几毫秒。 8 . 一个忙等待线程,每5秒钟唤醒一次 现在5秒唤醒一次。 我们可以根据夹角和唤醒的时间间隔,计算每个唤醒的CPU繁忙时间:busy_time = (1000 ms / (热图行数 时间长度) tan(夹角)例如45°夹角的线:busy_time = (1000 ms / (50 1)) tan(45) = 20ms 方差9 . cpu利用率100% ...

May 29, 2019 · 1 min · jiezi

什么会导致Java应用程序的CPU使用率飙升

问题无限循环的while会导致CPU使用率飙升吗?经常使用Young GC会导致CPU占用率飙升吗?具有大量线程的应用程序的CPU使用率是否较高?CPU使用率高的应用程序的线程数是多少?处于BLOCKED状态的线程会导致CPU使用率飙升吗?分时操作系统中的CPU是消耗us还是sy?思路1.如何计算CPU使用率?CPU%= 1 - idleTime / sysTime * 100 idleTime:CPU空闲的时间sysTime:CPU处于用户模式和内核模式的时间总和2.与CPU使用率有关的是什么?人们常说,计算密集型程序的CPU密集程度更高。 那么,JAVA应用程序中的哪些操作更加CPU密集? 以下列出了常见的CPU密集型操作: 频繁的GC; 如果访问量很高,可能会导致频繁的GC甚至FGC。当调用量很大时,内存分配将如此之快以至于GC线程将连续执行,这将导致CPU飙升。序列化和反序列化。稍后将给出一个示例:当程序执行xml解析时,调用量会增加,从而导致CPU变满。序列化和反序列化;正则表达式。 我遇到了正则表达式使CPU充满的情况; 原因可能是Java正则表达式使用的引擎实现是NFA自动机,它将在字符匹配期间执行回溯。我写了一篇文章“ 正则表达式中的隐藏陷阱 ”来详细解释原因。线程上下文切换; 有许多已启动的线程,这些线程的状态在Blocked(锁定等待,IO等待等)和Running之间发生变化。当锁争用激烈时,这种情况很容易发生。有些线程正在执行非阻塞操作,例如while (true)语句。如果在程序中计算需要很长时间,则可以使线程休眠。3. CPU是否与进程和线程相关?现在,分时操作系统使用循环方式为进程调度分配时间片。如果进程正在等待或阻塞,那么它将不会使用CPU资源。线程称为轻量级进程,并共享进程资源。因此,线程调度在CPU中也是分时的。但在Java中,我们使用JVM进行线程调度。因此,通常,线程调度有两种模式:时间共享调度和抢占式调度。 答案1. while的无限循环会导致CPU使用率飙升吗?是。 首先,无限循环将调用CPU寄存器进行计数,此操作将占用CPU资源。那么,如果线程始终处于无限循环状态,CPU是否会切换线程? 除非操作系统时间片到期,否则无限循环不会放弃占用的CPU资源,并且无限循环将继续向系统请求时间片,直到系统没有空闲时间来执行任何其他操作。 stackoverflow中也提出了这个问题:为什么无意的无限循环增加了CPU的使用? https://stackoverflow.com/questions/2846165/why-does-an-infinite-loop-of-the-unintended-kind-increase-the-cpu-use 2.频繁的Young GC会导致CPU占用率飙升吗?是。 Young GC本身就是JVM用于垃圾收集的操作,它需要计算内存和调用寄存器。因此,频繁的Young GC必须占用CPU资源。 让我们来看一个现实世界的案例。for循环从数据库中查询数据集合,然后再次封装新的数据集合。如果内存不足以存储,JVM将回收不再使用的数据。因此,如果所需的存储空间很大,您可能会收到CPU使用率警报。 3.具有大量线程的应用程序的CPU使用率是否较高?不时。 如果通过jstack检查系统线程状态时线程总数很大,但处于Runnable和Running状态的线程数不多,则CPU使用率不一定很高。 我遇到过这样一种情况:系统线程的数量是1000+,其中超过900个线程处于BLOCKED和WAITING状态。该线程占用很少的CPU。 但是大多数情况下,如果线程数很大,那么常见的原因是大量线程处于BLOCKED和WAITING状态。 4.对于CPU占用率高的应用程序,线程数是否较大?不是。 高CPU使用率的关键因素是计算密集型操作。如果一个线程中有大量计算,则CPU使用率也可能很高。这也是数据脚本任务需要在大规模集群上运行的原因。 5.处于BLOCKED状态的线程是否会导致CPU占用率飙升?不会。 CPU使用率的飙升更多是由于上下文切换或过多的可运行状态线程。处于阻塞状态的线程不一定会导致CPU使用率上升。 6.如果分时操作系统中CPU的值us或sy值很高,这意味着什么?您可以使用命令查找CPU的值us和sy值top,如以下示例所示: us:用户空间占用CPU的百分比。简单来说,高我们是由程序引起的。通过分析线程堆栈很容易找到有问题的线程。sy:内核空间占用CPU的百分比。当sy为高时,如果它是由程序引起的,那么它基本上是由于线程上下文切换。经验如何找出CPU使用率高的原因?下面简要描述分析过程。 如果发现应用程序服务器的CPU使用率很高,请首先检查线程数,JVM,系统负载等参数,然后使用这些参数来证明问题的原因。其次,使用jstack打印堆栈信息并使用工具分析线程使用情况(建议使用fastThread,一个在线线程分析工具)。 以下是一个真实案例: 一天晚上,我突然收到一条消息,说CPU使用率达到了100%。所以立即,我倾倒了用jstack打印的堆栈信息。 进一步检查日志: onsumer_ODC_L_nn_jmq919_1543834242875 - priority:10 - threadid:0x00007fbf7011e000 - nativeid:0x2f093 - state:RUNNABLEstackTrace:java.lang.Thread.State:RUNNABLEat java.lang.Object.hashCode(Native Method)at java.util.HashMap.hash(HashMap.java:362)at java.util.HashMap.getEntry(HashMap.java:462)at java.util.HashMap.containsKey(HashMap.java:449)at com.project.order.odc.util.XmlSerializableTool.deSerializeXML(XMLSerializableTool.java:100)at com.project.plugin.service.message.resolver.impl.OrderFinishMessageResolver.parseMessage(OrderFinishMessageResolver.java:55)at com.project.plugin.service.message.resolver.impl.OrderFinishMessageResolver.parseMessage(OrderFinishMessageResolver.java:21)at com.project.plugin.service.message.resolver.impl.AbstractResolver.resolve(AbstractResolver.java:28)at com.project.plugin.service.jmq.AbstractListener.onMessage(AbstractListener.java:44)现在通过这个日志找到了问题:用于反序列化MQ消息实体的方法导致CPU使用率飙升。 ...

May 24, 2019 · 1 min · jiezi

如何编写快速且线程安全的Python代码

概述如今我也是使用Python写代码好多年了,但是我却很少关心GIL的内部机制,导致在写Python多线程程序的时候。今天我们就来看看CPython的源代码,探索一下GIL的源码,了解为什么Python里要存在这个GIL,过程中我会给出一些示例来帮助大家更好的理解GIL。 GIL概览有如下代码: static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */这行代码位于Python2.7源码ceval.c文件里。在类Unix操作系统中,PyThread_type_lock对应C语言里的mutex_t类型。在Python解释器开始运行时初始化这个变量 voidPyEval_InitThreads(void){ interpreter_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock);}所有Python解释器里执行的c代码都必须获取这个锁,作者一开始为求简单,所以使用这种单线程的方式,后来每次想移除时,都发现代价太高了。 GIL对程序中的线程的影响很简单,你可以在手背上写下这个原则:“一个线程运行Python,而另外一个线程正在等待I / O.”Python代码可以使用threading.Lock或者其他同步对象,来释放CPU占用,让其他程序得以执行。 什么时候线程切换? 每当线程开始休眠或等待网络I / O时,另一个线程都有机会获取GIL并执行Python代码。CPython还具有抢先式多任务处理:如果一个线程在Python 2中不间断地运行1000个字节码指令,或者在Python 3中运行15毫秒,那么它就会放弃GIL而另一个线程可能会运行。 协作式多任务每当运行一个任务,比如网络I/O,持续的时间很长或者无法确定运行时间,这时可以放弃GIL,这样另一个线程就可以接受并运行Python。 这种行为称为协同多任务,它允许并发; 许多线程可以同时等待不同的事件。假设有两个链接socket的线程 def do_connect(): s = socket.socket() s.connect(('python.org', 80)) # drop the GILfor i in range(2): t = threading.Thread(target=do_connect) t.start()这两个线程中一次只有一个可以执行Python,但是一旦线程开始连接,它就会丢弃GIL,以便其他线程可以运行。这意味着两个线程都可以等待它们的套接字同时连接,他们可以在相同的时间内完成更多的工作。接下来,让我们打开Python的源码,来看看内部是如何实现的(位于socketmodule.c文件里): static PyObject *sock_connect(PySocketSockObject *s, PyObject *addro){ sock_addr_t addrbuf; int addrlen; int res; /* convert (host, port) tuple to C address */ getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen); Py_BEGIN_ALLOW_THREADS res = connect(s->sock_fd, addr, addrlen); Py_END_ALLOW_THREADS /* error handling and so on .... */}Py_BEGIN_ALLOW_THREADS宏指令用于释放GIL,他的定义很简单: ...

May 22, 2019 · 2 min · jiezi

Rxjava2x源码解析一-订阅流程

现在网上已经有大量的源码分析文章,各种技术的都有。但我觉得很多文章对初学者并不友好,让人读起来云里雾里的,比源码还源码。究其原因,是根本没有从学习者的角度去分析。在自己完成了源码阅读之后,却忘记了自己是如何一步步提出问题,进而走到这里的。 所以,我想在本篇及以后的文章中,花更多的精力去进行源码的分析,争取用浅显易懂的语言,用适合的逻辑去组织内容。这样不至于陷入源码里,导致文章难懂。尽量让更多的人愿意去读源码。 阅读本文,你需要对 RxJava2 的一些基本使用有所了解,不过不用太深。这里推荐下Season_zlc的给初学者的RxJava2.0教程(一),比较浅显易懂。 提到 RxJava,你第一个想到的词是什么? “异步”。 RxJava 在 GitHub 上的官网主页也说了,“RxJava is a Java VM implementation of Reactive Extensions: a library for composing asynchronous and event-based programs by using observable sequences.”(RxJava是一个使用可观测序列来组建异步、基于事件的程序的库,它是 Reactive Extensions 在Java虚拟机上的一个实现)。它的优点嘛,用扔物线凯哥的话讲,就是“简洁”,并且“随着程序逻辑变得越来越复杂,它依然能够保持简洁”。 这里要注意一点,虽然对大多数人来讲,更多的是使用 RxJava 来配合 Retrofit、OkHttp 进行网络请求框架的封装及数据的异步处理,但是,RxJava和网络请求本质上没有半毛钱的关系。它的本质,官网已经说的很明白了,就是“异步”。 RxJava 基于观察者模式实现,基于事件流进行链式调用。 首先,我们需要添加必要的依赖,这里以最新的2.2.8版本为例: implementation "io.reactivex.rxjava2:rxjava:2.2.8"当然,对于 Android 项目来讲,我们一般还需要添加一个补充库: implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'这个库其实就是提供了 Android 相关的主线程的支持。 然后写个简单的代码,就可以开始我们的源码分析啦。 // 上游 observable Observable<Integer> observable = Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { Log.d(TAG, "subscribe: "); emitter.onNext(1); emitter.onNext(2); emitter.onComplete(); } }); // 下游 observer Observer<Integer> observer = new Observer<Integer>() { @Override public void onSubscribe(Disposable d) { // onSubscribe 方法会最先被执行 Log.d(TAG, "onSubscribe: "); } @Override public void onNext(Integer integer) { Log.d(TAG, "onNext: "); } @Override public void onError(Throwable e) { Log.d(TAG, "onError: "); } @Override public void onComplete() { Log.d(TAG, "onComplete: "); } }; // 将上游和下游进行关联 observable.subscribe(observer);为便于理解,我故意将可以链式调用的代码,拆成了三部分。你完全可以写成下面的链式风格: ...

May 21, 2019 · 5 min · jiezi

借助混沌工程工具-ChaosBlade-构建高可用的分布式系统

在分布式架构环境下,服务间的依赖日益复杂,可能没有人能说清单个故障对整个系统的影响,构建一个高可用的分布式系统面临着很大挑战。在可控范围或环境下,使用 ChaosBlade 工具,对系统注入各种故障,持续提升分布式系统的容错和弹性能力,以构建高可用的分布式系统。 ChaosBlade 是什么?ChaosBlade 是一款遵循混沌工程实验原理,建立在阿里巴巴近十年故障测试和演练实践基础上,并结合了集团各业务的最佳创意和实践,提供丰富故障场景实现,帮助分布式系统提升容错性和可恢复性的混沌工程工具。点击这里,了解详情。 ChaosBlade 无需编译,下载解压即可使用,支持基础资源、Java 应用、容器服务类的混沌实验,特点是操作简洁、无侵入、扩展性强。 ChaosBlade @GitHub,点击进入 下面我们以微服务分布式系统举例,一步一步构建高可用的分布式系统。 构建高可用的分布式系统ChaosBlade 的使用方式 ChaoBlade 通过 CLI 方式调用,比如我们模拟 A 服务调用 B 提供的 com.alibaba.demo.HelloService 服务下的 hello 服务延迟 3 秒,我们可以在 B 应用上注入延迟故障,仅需两步操作:第一步:准备阶段。由于 Java 应用的故障注入是通过 Java Agent 机制实现,所以首先要先挂载 agent,执行的命令是 blade prepare jvm --process <PROCESS NAME OF B APPLICATION>第二步:执行阶段,注入故障。执行命令是 blade create dubbo delay --time 3000 --service com.alibaba.demo.HelloService --methodname hello --provider,即对 B 服务提供方提供的 com.alibaba.demo.HelloService#hello 服务注入 3 秒延迟。 ChaosBlade 使用简洁,如果想了解命令的如何使用,可在命令后面添加 -h 参数,比如 blade create dubbo delay -h。更详细的 chaosblade 操作,可详见新手指南 ...

May 14, 2019 · 2 min · jiezi

3分钟干货之什么是线程安全

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。 这个问题有值得一提的地方,就是线程安全也是有几个级别的: 1)不可变像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用 2)绝对线程安全不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet 3)相对线程安全相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。 4)线程非安全这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

May 10, 2019 · 1 min · jiezi

大侦探福老师幽灵Crash谜踪案

闲鱼Flutter技术的基础设施已基本趋于稳定,就在我们准备松口气的时候,一个Crash却异军突起冲击着我们的稳定性防线!闲鱼技术火速成立侦探小组执行嫌犯侦查行动,经理重重磨难终于在一个隐蔽的角落将其绳之以法! 幽灵Crash问题要从闲鱼Flutter基础设施上一次大规模升级说起。2018年我们对闲鱼的Flutter基建作了比较大的重构,目标在于提高基建的稳定性和可扩展性。这个过程当然是挑战重重,在上一次大规模的重构集成发版后,我们虽然没有发现非常明显的异常问题,但是Crash率却出现了一个比较明显的增长。虽然总体数值还在可控范围之内,但这一个Crash却占据了几乎一大半。这个问题引起了我们警觉,我们立刻成立专项小组重点进行排查。 一般Crash Log能够为我们定位Crash提供主要信息,我们一起看看这个Crash的Log: Thread 0 Crashed:0 libobjc.A.dylib 0x00000001c1b42b00 objc_object::release() :16 (in libobjc.A.dylib)1 libobjc.A.dylib 0x00000001c1b4338c (anonymous namespace)::AutoreleasePoolPage::pop(void*) :676 (in libobjc.A.dylib)2 CoreFoundation 0x00000001c28e8804 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ :28 (in CoreFoundation)3 CoreFoundation 0x00000001c28e8534 __CFRunLoopDoTimer :864 (in CoreFoundation)4 CoreFoundation 0x00000001c28e7d68 __CFRunLoopDoTimers :248 (in CoreFoundation)5 CoreFoundation 0x00000001c28e2c44 __CFRunLoopRun :1880 (in CoreFoundation)6 CoreFoundation 0x00000001c28e21cc _CFRunLoopRunSpecific :436 (in CoreFoundation)7 GraphicsServices 0x00000001c4b59584 _GSEventRunModal :100 (in GraphicsServices)8 UIKitCore 0x00000001efb59054 _UIApplicationMain :212 (in UIKitCore)9 Runner 0x0000000102df4eb4 main main.m:49 (in Runner)10 libdyld.dylib 0x00000001c23a2bb4 _start :4 (in libdyld.dylib)这是一个很典型的野指针Crash Log,是其中一种俗称的Over released问题。但是具体是哪个对象和方法,很难直接从Log上面得知,况且ARC下面的野指针更令人费解。 ...

May 10, 2019 · 2 min · jiezi

图床失效了也许你应该试试这个工具

前言经过几个小伙伴的提醒,发现个人博客中的许多图片都裂了无法访问;原因就不多说,既然出现问题就得要解决。 原本我的处理方式非常简单粗暴:找到原有的图片重新下载下来上传到新的可用图床再把图片地址替换。 这样搞了一两篇之后我就绝望了。。。 之前为了代码能在公众号里也有好的阅读体验,所以能截图的我绝不贴代码,导致一篇文章多的得有十几张图片。 好在哪位大佬说过“以人肉XX为耻”,这种重复劳动力完全可自动化;于是便有了本次的这个工具。 它可以一行命令把你所有 Markdown 写的内容中的图片全部替换为新的图床。 运行效果如下: 使用可以直接在这个地址下载 jar 包运行:https://github.com/crossoverJ... 当然也可以下载源码编译运行: git clone https://github.com/crossoverJie/blog.toolboxmvn clean packagejava -jar nows-0.0.1-SNAPSHOT.jar --app.downLoad.path=/xx/img /xx/xx/path 100看运行方式也知道,其实就是用 SpringBoot 写了一个工具用于批量下载文中出现的图片同时上传后完成替换。 其中 app.downLoad.path 是用于将下载的图片保存到本地磁盘的目录。/xx/xx/path 则是扫描 .md 文件的目录,会递归扫描所有出所有文件。100 则是需要替换文件的数量,默认是按照文件修改时间排序。如果自己的图片较多的话还是有几个坑需要注意下。 线程数量默认是启动了两个线程去遍历文件、上传下载图片、更新文本等内容,其中的网络 IO 其实挺耗时的,所以其实可以适当的多开些线程来提高任务的执行效率。 但线程过多也许会触发图床的保护机制,同时也和自己电脑配置有关,这个得结合实际情况考虑了。 所以可以通过 --app.thread=6 这样的参数来调整线程数量。 图床限制这个是图片过多一定是大概率出现的,上传请求的频次过高很容易被限流封 IP。 {"code":"error","msg":"Upload file count limit. Time left 1027 second."}目前来看是封 IP 居多,所以可以通过走代理、换网络的方式来解决。 当然如果是自搭图床可以无视。 重试由于我使用的是免费图床,上传过程中偶尔也会出现上传失败的情况,因此默认是有 5 次重试机制的;如果五次都失败了那么大概率是 IP 被封了。 即便是 ip 被封后只要换了新的 ip 重新执行程序它会自动过滤掉已经替换的图片,不会再做无用功,这点可以放心。图片保存 ...

May 9, 2019 · 1 min · jiezi

MySQL-InnoDB特性-Buffer-Pool漫谈

缓存管理是DBMS的核心系统,用于管理数据页的访问、刷脏和驱逐;虽然操作系统本身有page cache,但那不是专门为数据库设计的,所以大多数数据库系统都是自己来管理缓存。由于几乎所有的数据页访问都涉及到Buffer Pool,因此buffer pool的并发访问控制尤为重要,可能会影响到吞吐量和响应时间,本文主要回顾一下MySQL的buffer Pool最近几个版本的发展(若有遗漏,欢迎评论补充), 感受下最近几年这一块的进步 MySQL5.5之前只能设置一个buffer pool, 通过innodb_buffer_pool_size来控制, 刷脏由master线程承担,扩展性差。 MySQL 5.5引入参数innodb_buffer_pool_instances,将buffer pool拆分成多个instance,从而减少对buffer pool的访问控制,这时候的刷脏还是由Master线程来承担。 MySQL 5.6引入了buffer Pool page Id转储和导入特性,也就是说可以随时把内存中的page no存下来到文件里,在重启时会自动把这些Page加载到内存中,使内存保持warm状态. 此外该版本第一次引入了page cleaner,将flush list/lru上的刷脏驱逐工作转移到单独线程,减少了master线程的负担 MySQL 5.7这个版本发布了一个重要特性:online buffer pool resize. 当然是否是online需要打一个问号,因为在resize的过程中需要拿很多全局大锁,在高负载场景下很容易导致实例Hang住(81615)。 和之前不同,buffer pool被分成多个instance,每个instance又由多个chunk组成,每个chunk的大小受到参数innodb_buffer_pool_chunk_size控制,默认128MB, buffer pool resize都是以chunk为单位增加或减少的。另外一个需要注意的点是:你配置的Buffer Pool Size可能比你实际使用的内存要大,尤其对于大Bp而言,这是因为内部做了对齐处理, buffer pool size必须以 innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances来做向上对齐(80350) 我们知道通常数据文件的IO都被设置成O_DIRECT, 但每次修改后依然需要去做fsync,来持久化元数据信息,而对于某些文件系统而言是没必要做fsync的,因此加入了新选项O_DIRECT_NO_FSYNC,这个需求来自于facebook. 他们也对此做了特殊处理:除非文件size变化,否则不做fsync。(最近在buglist上对这个参数是否安全的讨论也很有意思,官方文档做了新的说明,感兴趣的可以看看 [94912:O_DIRECT_NO_FSYNC possible write hole](https://bugs.mysql.com/bug.php?id=94912))) 再一个重要功能是终于引入了multiple page cleaner, 可以多个后台线程并发刷脏页,提供了更好的刷脏性能,有效避免用户线程进入single page flush。当然这还不够完美,主要有四点: 用户线程依然会进入single page flush,而一旦大量线程进入,就会导致严重性能下降:超频繁的fsync,激烈的dblwr竞争,线程切换等等当redo空间不足时,用户线程也会进入page flush,这在高负载场景下是很常见的,你会发现系统运行一段时间后,性能急剧下降。这是因为redo产生太快,而page flush又跟不上,导致checkpoint无法推进。那么用户线程可能就要过来做fuzzy checkpoint了。那时候性能基本上没法看了。dblwr成为重要的单点瓶颈。 如果你的服务器不支持原子写的话,必须打开double write buffer。写入Ibdata一段固定区域,这里是有锁包含的,区分为两部分:single page flush和batch flush, 但无论如何,即使拆分了多个page cleaner,最终扩展性还是受限于dblwr没有专用的lru evict线程,都是Page cleaner键值的。举个简单的例子,当buffer pool占满,同时又有很多脏页时,Page cleaner可能忙于刷脏,而用户线程则得不到free page,从而陷入single page flush如果你对上述几个问题极不满意,可以尝试percona server, 他们向来擅长优化Io bound场景的性能,并且上述几个问题都解决了,尤其是dblwr,他们做了多分区的改进。 ...

April 29, 2019 · 1 min · jiezi

Kubernetes从懵圈到熟练读懂这一篇集群节点不下线

排查完全陌生的问题,完全不熟悉的系统组件,是售后工程师的一大工作乐趣,当然也是挑战。今天借这篇文章,跟大家分析一例这样的问题。排查过程中,需要理解一些自己完全陌生的组件,比如systemd和dbus。但是排查问题的思路和方法基本上还是可以复用了,希望对大家有所帮助。 问题一直在发生I'm NotReady 阿里云有自己的Kubernetes容器集群产品。随着Kubernetes集群出货量的剧增,线上用户零星的发现,集群会非常低概率地出现节点NotReady情况。据我们观察,这个问题差不多每个月,就会有一两个客户遇到。在节点NotReady之后,集群Master没有办法对这个节点做任何控制,比如下发新的Pod,再比如抓取节点上正在运行Pod的实时信息。 需要知道的Kubernetes知识 这里我稍微补充一点Kubernetes集群的基本知识。Kubernetes集群的“硬件基础”,是以单机形态存在的集群节点。这些节点可以是物理机,也可以是虚拟机。集群节点分为Master和Worker节点。Master节点主要用来负载集群管控组件,比如调度器和控制器。而Worker节点主要用来跑业务。Kubelet是跑在各个节点上的代理,它负责与管控组件沟通,并按照管控组件的指示,直接管理Worker节点。 当集群节点进入NotReady状态的时候,我们需要做的第一件事情,肯定是检查运行在节点上的kubelet是否正常。在这个问题出现的时候,使用systemctl命令查看kubelet状态,发现它作为systemd管理的一个daemon,是运行正常的。当我们用journalctl查看kubelet日志的时候,发现下边的错误。 什么是PLEG 这个报错很清楚的告诉我们,容器runtime是不工作的,且PLEG是不健康的。这里容器runtime指的就是docker daemon。Kubelet通过直接操作docker daemon来控制容器的生命周期。而这里的PLEG,指的是pod lifecycle event generator。PLEG是kubelet用来检查容器runtime的健康检查机制。这件事情本来可以由kubelet使用polling的方式来做。但是polling有其成本上的缺陷,所以PLEG应用而生。PLEG尝试以一种“中断”的形式,来实现对容器runtime的健康检查,虽然实际上,它同时用了polling和”中断”两种机制。 基本上看到上边的报错,我们可以确认,容器runtime出了问题。在有问题的节点上,通过docker命令尝试运行新的容器,命令会没有响应。这说明上边的报错是准确的. 容器runtimeDocker Daemon调用栈分析 Docker作为阿里云Kubernetes集群使用的容器runtime,在1.11之后,被拆分成了多个组件以适应OCI标准。拆分之后,其包括docker daemon,containerd,containerd-shim以及runC。组件containerd负责集群节点上容器的生命周期管理,并向上为docker daemon提供gRPC接口。 在这个问题中,既然PLEG认为容器运行是出了问题,我们需要先从docker daemon进程看起。我们可以使用kill -USR1 <pid>命令发送USR1信号给docker daemon,而docker daemon收到信号之后,会把其所有线程调用栈输出到文件/var/run/docker文件夹里。 Docker daemon进程的调用栈相对是比较容易分析的。稍微留意,我们会发现大多数的调用栈都类似下图中的样子。通过观察栈上每个函数的名字,以及函数所在的文件(模块)名称,我们可以看到,这个调用栈下半部分,是进程接到http请求,做请求路由的过程;而上半部分则进入实际的处理函数。最终处理函数进入等待状态,等待的是一个mutex实例。 到这里,我们需要稍微看一下ContainerInspectCurrent这个函数的实现,而最重要的是,我们能搞明白,这个函数的第一个参数,就是mutex的指针。使用这个指针搜索整个调用栈文件,我们会找出,所有等在这个mutex上边的线程。同时,我们可以看到下边这个线程。 这个线程上,函数ContainerExecStart也是在处理具体请求的时候,收到了这个mutex这个参数。但不同的是,ContainerExecStart并没有在等待mutex,而是已经拿到了mutex的所有权,并把执行逻辑转向了containerd调用。关于这一点,我们可以使用代码来验证。前边我们提到过,containerd向上通过gRPC对docker daemon提供接口。此调用栈上半部分内容,正是docker daemon在通过gRPC请求来呼叫containerd。 Containerd调用栈分析 与输出docker daemon的调用栈类似,我们可以通过kill -SIGUSR1 <pid>命令来输出containerd的调用栈。不同的是,这次调用栈会直接输出到messages日志。 Containerd作为一个gRPC的服务器,它会在接到docker daemon的远程请求之后,新建一个线程去处理这次请求。关于gRPC的细节,我们这里其实不用关注太多。在这次请求的客户端调用栈上,可以看到这次调用的核心函数是Start一个进程。我们在containerd的调用栈里搜索Start,Process以及process.go等字段,很容易发现下边这个线程。 这个线程的核心任务,就是依靠runC去创建容器进程。而在容器启动之后,runC进程会退出。所以下一步,我们自然而然会想到,runC是不是有顺利完成自己的任务。查看进程列表,我们会发现,系统中有个别runC进程,还在执行,这不是预期内的行为。容器的启动,跟进程的启动,耗时应该是差不对的,系统里有正在运行的runC进程,则说明runC不能正常启动容器。 什么是DbusRunC请求Dbus 容器runtime的runC命令,是libcontainer的一个简单的封装。这个工具可以用来管理单个容器,比如容器创建,或者容器删除。在上节的最后,我们发现runC不能完成创建容器的任务。我们可以把对应的进程杀掉,然后在命令行用同样的命令尝试启动容器,同时用strace追踪整个过程。 分析发现,runC停在了向带有org.free字段的dbus写数据的地方。那什么是dbus呢?在Linux上,dbus是一种进程间进行消息通信的机制。 原因并不在Dbus 我们可以使用busctl命令列出系统现有的所有bus。如下图,在问题发生的时候,我看到客户集群节点Name的编号非常大。所以我倾向于认为,dbus某些相关的数据结构,比如Name,耗尽了引起了这个问题。 Dbus机制的实现,依赖于一个组件叫做dbus-daemon。如果真的是dbus相关数据结构耗尽,那么重启这个daemon,应该是可以解决这个问题。但不幸的是,问题并没有这么直接。重启dbus-daemon之后,问题依然存在。 在上边用strace追踪runC的截图中,我提到了,runC卡在向带有org.free字段的bus写数据的地方。在busctl输出的bus列表里,显然带有这个字段的bus,都在被systemd使用。这时,我们用systemctl daemon-reexec来重启systemd,问题消失了。所以基本上我们可以判断一个方向:问题可能跟systemd有关系。 Systemd是硬骨头Systemd是相当复杂的一个组件,尤其对没有做过相关开发工作的同学来说,比如我自己。基本上,排查systemd的问题,我用到了四个方法,(调试级别)日志,core dump,代码分析,以及live debugging。其中第一个,第三个和第四个结合起来使用,让我在经过几天的鏖战之后,找到了问题的原因。但是这里我们先从“没用”的core dump说起。 没用的Core Dump 因为重启systemd解决了问题,而这个问题本身,是runC在使用dbus和systemd通信的时候没有了响应,所以我们需要验证的第一件事情,就是systemd不是有关键线程被锁住了。查看core dump里所有线程,只有以下一个线程,此线程并没有被锁住,它在等待dbus事件,以便做出响应。 ...

April 23, 2019 · 1 min · jiezi

不可错过的CMS学习笔记

引子带着问题去学习一个东西,才会有目标感,我先把一直以来自己对CMS的一些疑惑罗列了下,希望这篇学习笔记能解决掉这些疑惑,希望也能对你有所帮助。CMS出现的初衷、背景和目的?CMS的适用场景?CMS的trade-off是什么?优势、劣势和代价CMS会回收哪个区域的对象?CMS的GC Roots包括那些对象?CMS的过程?CMS和Full gc是不是一回事?CMS何时触发?CMS的日志如何分析?CMS的调优如何做?CMS扫描那些对象?CMS和CMS collector的区别?CMS的推荐参数设置?为什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?一、基础知识CMS收集器:Mostly-Concurrent收集器,也称并发标记清除收集器(Concurrent Mark-Sweep GC,CMS收集器),它管理新生代的方式与Parallel收集器和Serial收集器相同,而在老年代则是尽可能得并发执行,每个垃圾收集器周期只有2次短停顿。我之前对CMS的理解,以为它是针对老年代的收集器。今天查阅了《Java性能优化权威指南》和《Java性能权威指南》两本书,确认之前的理解是错误的。CMS的初衷和目的:为了消除Throught收集器和Serial收集器在Full GC周期中的长时间停顿。CMS的适用场景:如果你的应用需要更快的响应,不希望有长时间的停顿,同时你的CPU资源也比较丰富,就适合适用CMS收集器。二、CMS的过程CMS的正常过程这里我们首先看下CMS并发收集周期正常完成的几个状态。(STW)初始标记:这个阶段是标记从GcRoots直接可达的老年代对象、新生代引用的老年代对象,就是下图中灰色的点。这个过程是单线程的(JDK7之前单线程,JDK8之后并行,可以通过参数CMSParallelInitialMarkEnabled调整)。并发标记:由上一个阶段标记过的对象,开始tracing过程,标记所有可达的对象,这个阶段垃圾回收线程和应用线程同时运行,如上图中的灰色的点。在并发标记过程中,应用线程还在跑,因此会导致有些对象会从新生代晋升到老年代、有些老年代的对象引用会被改变、有些对象会直接分配到老年代,这些受到影响的老年代对象所在的card会被标记为dirty,用于重新标记阶段扫描。这个阶段过程中,老年代对象的card被标记为dirty的可能原因,就是下图中绿色的线:预清理:预清理,也是用于标记老年代存活的对象,目的是为了让重新标记阶段的STW尽可能短。这个阶段的目标是在并发标记阶段被应用线程影响到的老年代对象,包括:(1)老年代中card为dirty的对象;(2)幸存区(from和to)中引用的老年代对象。因此,这个阶段也需要扫描新生代+老年代。【PS:会不会扫描Eden区的对象,我看源代码猜测是没有,还需要继续求证】可中断的预清理:这个阶段的目标跟“预清理”阶段相同,也是为了减轻重新标记阶段的工作量。可中断预清理的价值:在进入重新标记阶段之前尽量等到一个Minor GC,尽量缩短重新标记阶段的停顿时间。另外可中断预清理会在Eden达到50%的时候开始,这时候离下一次minor gc还有半程的时间,这个还有另一个意义,即避免短时间内连着的两个停顿,如下图资料所示:在预清理步骤后,如果满足下面两个条件,就不会开启可中断的预清理,直接进入重新标记阶段:Eden的使用空间大于“CMSScheduleRemarkEdenSizeThreshold”,这个参数的默认值是2M;Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,这个参数的默认值是50%。如果不满足上面两个条件,则进入可中断的预清理,可中断预清理可能会执行多次,那么退出这个阶段的出口有两个(源码参见下图):* 设置了CMSMaxAbortablePrecleanLoops,并且执行的次数超过了这个值,这个参数的默认值是0;* CMSMaxAbortablePrecleanTime,执行可中断预清理的时间超过了这个值,这个参数的默认值是5000毫秒。 如果是因为这个原因退出,gc日志打印如下:有可能可中断预清理过程中一直没等到Minor gc,这时候进入重新标记阶段的话,新生代还有很多活着的对象,就回导致STW变长,因此CMS还提供了CMSScavengeBeforeRemark参数,可以在进入重新标记之前强制进行依次Minor gc。(STW)重新标记:重新扫描堆中的对象,进行可达性分析,标记活着的对象。这个阶段扫描的目标是:新生代的对象 + Gc Roots + 前面被标记为dirty的card对应的老年代对象。如果预清理的工作没做好,这一步扫描新生代的时候就会花很多时间,导致这个阶段的停顿时间过长。这个过程是多线程的。并发清除:用户线程被重新激活,同时将那些未被标记为存活的对象标记为不可达;并发重置:CMS内部重置回收器状态,准备进入下一个并发回收周期。CMS的异常情况上面描述的是CMS的并发周期正常完成的情况,但是还有几种CMS并发周期失败的情况:并发模式失败(Concurrent mode failure):CMS的目标就是在回收老年代对象的时候不要停止全部应用线程,在并发周期执行期间,用户的线程依然在运行,如果这时候如果应用线程向老年代请求分配的空间超过预留的空间(担保失败),就回触发concurrent mode failure,然后CMS的并发周期就会被一次Full GC代替——停止全部应用进行垃圾收集,并进行空间压缩。如果我们设置了UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction参数,其中CMSInitiatingOccupancyFraction的值是70,那预留空间就是老年代的30%。晋升失败:新生代做minor gc的时候,需要CMS的担保机制确认老年代是否有足够的空间容纳要晋升的对象,担保机制发现不够,则报concurrent mode failure,如果担保机制判断是够的,但是实际上由于碎片问题导致无法分配,就会报晋升失败。永久代空间(或Java8的元空间)耗尽,默认情况下,CMS不会对永久代进行收集,一旦永久代空间耗尽,就回触发Full GC。三、CMS的调优针对停顿时间过长的调优首先需要判断是哪个阶段的停顿导致的,然后再针对具体的原因进行调优。使用CMS收集器的JVM可能引发停顿的情况有:(1)Minor gc的停顿;(2)并发周期里初始标记的停顿;(3)并发周期里重新标记的停顿;(4)Serial-Old收集老年代的停顿;(5)Full GC的停顿。其中并发模式失败会导致第(4)种情况,晋升失败和永久代空间耗尽会导致第(5)种情况。针对并发模式失败的调优想办法增大老年代的空间,增加整个堆的大小,或者减少年轻代的大小以更高的频率执行后台的回收线程,即提高CMS并发周期发生的频率。设置UseCMSInitiatingOccupancyOnly和CMSInitiatingOccupancyFraction参数,调低CMSInitiatingOccupancyFraction的值,但是也不能调得太低,太低了会导致过多的无效的并发周期,会导致消耗CPU时间和更多的无效的停顿。通常来讲,这个过程需要几个迭代,但是还是有一定的套路,参见《Java性能权威指南》中给出的建议,摘抄如下:> 对特定的应用程序,该标志的更优值可以根据 GC 日志中 CMS 周期首次启动失败时的值得到。具体方法是,在垃圾回收日志中寻找并发模式失效,找到后再反向查找 CMS 周期最近的启动记录,然后根据日志来计算这时候的老年代空间占用值,然后设置一个比该值更小的值。增多回收线程的个数CMS默认的垃圾收集线程数是*(CPU个数 + 3)/4*,这个公式的含义是:当CPU个数大于4个的时候,垃圾回收后台线程至少占用25%的CPU资源。举个例子:如果CPU核数是1-4个,那么会有1个CPU用于垃圾收集,如果CPU核数是5-8个,那么久会有2个CPU用于垃圾收集。针对永久代的调优如果永久代需要垃圾回收(或元空间扩容),就会触发Full GC。默认情况下,CMS不会处理永久代中的垃圾,可以通过开启CMSPermGenSweepingEnabled配置来开启永久代中的垃圾回收,开启后会有一组后台线程针对永久代做收集,需要注意的是,触发永久代进行垃圾收集的指标跟触发老年代进行垃圾收集的指标是独立的,老年代的阈值可以通过CMSInitiatingPermOccupancyFraction参数设置,这个参数的默认值是80%。开启对永久代的垃圾收集只是其中的一步,还需要开启另一个参数——CMSClassUnloadingEnabled,使得在垃圾收集的时候可以卸载不用的类。四、CMS的trade-off是什么?优势低延迟的收集器:几乎没有长时间的停顿,应用程序只在Minor gc以及后台线程扫描老年代的时候发生极其短暂的停顿。劣势更高的CPU使用:必须有足够的CPU资源用于运行后台的垃圾收集线程,在应用程序线程运行的同时扫描堆的使用情况。【PS:现在服务器的CPU资源基本不是问题,这个点可以忽略】CMS收集器对老年代收集的时候,不再进行任何压缩和整理的工作,意味着老年代随着应用的运行会变得碎片化;碎片过多会影响大对象的分配,虽然老年代还有很大的剩余空间,但是没有连续的空间来分配大对象,这时候就会触发Full GC。CMS提供了两个参数来解决这个问题:(1)UseCMSCompactAtFullCollection,在要进行Full GC的时候进行内存碎片整理;(2)CMSFullGCsBeforeCompaction,每隔多少次不压缩的Full GC后,执行一次带压缩的Full GC。会出现浮动垃圾;在并发清理阶段,用户线程仍然在运行,必须预留出空间给用户线程使用,因此CMS比其他回收器需要更大的堆空间。五、几个问题的解答为什么ParNew可以和CMS配合使用,而Parallel Scanvenge不可以?答:这个跟Hotspot VM的历史有关,Parallel Scanvenge是不在“分代框架”下开发的,而ParNew、CMS都是在分代框架下开发的。CMS中minor gc和major gc是顺序发生的吗?答:不是的,可以交叉发生,即在并发周期执行过程中,是可以发生Minor gc的,这个找个gc日志就可以观察到。CMS的并发收集周期合适触发?由下图可以看出,CMS 并发周期触发的条件有两个:阈值检查机制:老年代的使用空间达到某个阈值,JVM的默认值是92%(jdk1.5之前是68%,jdk1.6之后是92%),或者可以通过CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly两个参数来设置;这个参数的设置需要看应用场景,设置得太小,会导致CMS频繁发生,设置得太大,会导致过多的并发模式失败。例如动态检查机制:JVM会根据最近的回收历史,估算下一次老年代被耗尽的时间,快到这个时间的时候就启动一个并发周期。设置UseCMSInitiatingOccupancyOnly这个参数可以将这个特性关闭。CMS的并发收集周期会扫描哪些对象?会回收哪些对象?答:CMS的并发周期只会回收老年代的对象,但是在标记老年代的存活对象时,可能有些对象会被年轻代的对象引用,因此需要扫描整个堆的对象。CMS的gc roots包括哪些对象?答:首先,在JVM垃圾收集中Gc Roots的概念如何理解(参见R大对GC roots的概念的解释);第二,CMS的并发收集周期中,如何判断老年代的对象是活着?我们前面提到了,在CMS的并发周期中,仅仅扫描Gc Roots直达的对象会有遗漏,还需要扫描新生代的对象。如下图中的蓝色字体所示,CMS中的年轻代和老年代是分别收集的,因此在判断年轻代的对象存活的时候,需要把老年代当作自己的GcRoots,这时候并不需要扫描老年代的全部对象,而是使用了card table数据结构,如果一个老年代对象引用了年轻代的对象,则card中的值会被设置为特殊的数值;反过来判断老年代对象存活的时候,也需要把年轻代当作自己的Gc Roots,这个过程我们在第三节已经论述过了。如果我的应用决定使用CMS收集器,推荐的JVM参数是什么?我自己的应用使用的参数如下,是根据PerfMa的xxfox生成的,大家也可以使用这个产品调优自己的JVM参数:-Xmx4096M -Xms4096M -Xmn1536M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+CMSClassUnloadingEnabled -XX:+ParallelRefProcEnabled -XX:+CMSScavengeBeforeRemark -XX:ErrorFile=/home/admin/logs/xelephant/hs_err_pid%p.log -Xloggc:/home/admin/logs/xelephant/gc.log -XX:HeapDumpPath=/home/admin/logs/xelephant -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryErrorCMS相关的参数总结(需要注意的是,这里我没有考虑太多JDK版本的问题,JDK1.7和JDK1.8这些参数的配置,有些默认值可能不一样,具体使用的时候还需要根据具体的版本来确认怎么设置)| 编号 | 参数名称 | 解释 || — | — | — || 1 | UseConcMarkSweepGC | 启用CMS收集器 || 2 | UseCMSInitiatingOccupancyOnly | 关闭CMS的动态检查机制,只通过预设的阈值来判断是否启动并发收集周期 || 3 | CMSInitiatingOccupancyFraction | 老年代空间占用到多少的时候启动并发收集周期,跟UseCMSInitiatingOccupancyOnly一起使用 || 4 | ExplicitGCInvokesConcurrentAndUnloadsClasses | 将System.gc()触发的Full GC转换为一次CMS并发收集,并且在这个收集周期中卸载 Perm(Metaspace)区域中不需要的类 || 5 | CMSClassUnloadingEnabled | 在CMS收集周期中,是否卸载类 || 6 | ParallelRefProcEnabled | 是否开启并发引用处理 || 7 | CMSScavengeBeforeRemark | 如果开启这个参数,会在进入重新标记阶段之前强制触发一次minor gc |参考资料从实际案例聊聊Java应用的GC优化理解CMS垃圾回收日志图解CMS垃圾回收机制,你值得拥有为什么CMS虽然是老年代的gc,但仍要扫描新生代的?R大对GC roots的概念的解释Introduce to CMS Collector《深入理解Java虚拟机》《Java性能权威指南》Oracle的GC调优手册what-is-the-threshold-for-cms-old-gc-to-be-triggeredFrequently Asked Questions about Garbage Collection in the Hotspot Java VirtualMachineJava SE HotSpot at a Glancexxfox:PerfMa的参数调优神器详解CMS垃圾回收机制ParNew和PSYoungGen和DefNew是一个东西么?Java SE的内存管理白皮书Garbage Collection in Elasticsearch and the G1GCA Heap of Trouble毕玄的文章:为什么不建议JVM源码分析之SystemGC完全解读读者讨论关于CMS收集器的回收范围,下面这张图是有误导的,从官方文档上看来,CMS收集器包括年轻代和老年代的收集,只不过对年轻代的收集的策略和ParNew相同,这个可以从参考资料16的第11页看到。concurrent mode failure和promotion failed触发的Full GC有啥不同?(这个问题是我、阿飞、蒋晓峰一起讨论的结果)答:concurrent mode failure触发的"Full GC"不是我们常说的Full GC——正常的Full GC其实是整个gc过程包括ygc和cms gc。也就是说,这个问题本身是有问题的,concurrent mode failure的时候触发的并不是我们常说的Full GC。然后再去讨论一个遗漏的知识点:CMS gc的并发周期有两种模式:foreground和background。concurrent mode failure触发的是foreground模式,会暂停整个应用,会将一些并行的阶段省掉做一次老年代收集,行为跟Serial-Old的一样,至于在这个过程中是否需要压缩,则需要看三个条件:(1)我们设置了UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction,前者设置为true,后者默认是0,前者表示是在Full GC的时候执行压缩,后者表示是每隔多少个进行压缩,默认是0的话就是每次Full GC都压缩;(2)用户调用了System.gc(),而且DisableExplicitGC没有开启;(3)young gen报告接下来如果做增量收集会失败。promotion failed触发的是我们常说的的Full GC,对年轻代和老年代都会回收,并进行整理。promotion failed和concurrent mode failure的触发原因有啥不同?promotion failed是说,担保机制确定老年代是否有足够的空间容纳新来的对象,如果担保机制说有,但是真正分配的时候发现由于碎片导致找不到连续的空间而失败;concurrent mode failure是指并发周期还没执行完,用户线程就来请求比预留空间更大的空间了,即后台线程的收集没有赶上应用线程的分配速度。什么情况下才选择使用CMS收集器呢?我之前的观念是:小于8G的都用CMS,大于8G的选择G1。蒋晓峰跟我讨论了下这个观念,提出了一些别的想法,我觉得也有道理,记录在这里:除了看吞吐量和延时,还需要看具体的应用,比方说ES,Lucene和G1是不兼容的,因此默认的收集器就是CMS,具体见可参考资料17和18。小于3G的堆,如果不是对延迟有特别高的需求,不建议使用CMS,主要是由于CMS的几个缺点导致的:(1)并发周期的触发比例不好设置;(2)抢占CPU时间;(3)担保判断导致YGC变慢;(4)碎片问题,更详细的讨论参见资料19。本文作者:杜琪阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 10, 2019 · 1 min · jiezi

燃烧我的卡路里 ---- Flutter瘦内存瘦包之图片组件

背景在电商类APP里,图片到现在为止仍然是最重要的信息承载媒介,不得不说逛淘宝的过程,其实就是一个看图片的过程。而商品详情页中的图片,通常是页面中内存占用最多的内容,占用了整个页面内存的超过 50%。闲鱼在Flutter化的过程中,选择了商品详情页作为第一个落地的场景。通过多版本的迭代完善,基于Flutter的详情页已经在闲鱼稳定运行。然而正因为详情页的图片量大,导致Flutter里图片相关的问题一直挥之不去。1:内存问题 — 连续push flutter界面内存累积2:安装包问题 — 过渡时期两份重复资源文件。3:寻址缓存问题 — 原有的寻址缓存策略无法复用。4:图片复用问题 — Native和Flutter重复下载相同图片。解决方案—-FXTexImage_V1为了解决这些问题,我们尝试着寻找一种新的思路,一种能够将flutter与native串联起来的思路。而之前做视频播放器的方案给了我们启发。熟悉Flutter的同学应该都知道,Flutter的视频组件是基于一个Flutter提供的一个叫“外接纹理”的技术实现的,关于flutter外接纹理,本人另外有一篇文章有更详细的论述,这里不再赘述。https://mp.weixin.qq.com/s/KkCsBvnRayvpXdI35J3fnw我们将每一张图片假想成一个:静态的视频。图片的内容由一个external_texture来负责显示,而这个external_texture则由native端提供具体的渲染数据。通过这种方案,我们便可以通过external_texture这座桥梁,将flutter作为native端图片的一个最终展示场所。而所有的下载、缓存、裁剪等逻辑都可以复用原来的native图片库。基于这个基本框架,我们形成了我们第一版本的图片渲染组件:FXTexImage—-V1。这个组件很好的解决了Flutter引入的安装包、图片缓存、图片复用等问题。但是图片最大的问题:内存问题,并没有得到解决。内存优化—-FXTexImage_V2为了用户体验,通常会有连续push若干个界面的场景(比如闲鱼的详情页,点击底部的推荐列表,可以一直往下push新的详情页),这种场景下,每一个界面都有大量的图片展示。所以在引入flutter以后,闲鱼在iPhone 6P等机型上通常只能push10个左右详情页就挂了。在考虑到在显示过程中,真正用户可见的页面,其实只有当前栈顶的两个页面,基于这个特征我们就做了优化逻辑:1:在push详情页过程中,我们只保留了当前展示页和当前页的前一页的图片资源,而之前的资源全部都做了释放(只是图片资源的释放,整个页面还有页面中的其他元素还是做了保留)。2:为了做到用户无感知,我们在pop过程中,会预先去加载当前界面下一个界面的图片资源。通过这种方式,理论上我们可以释放掉不可见的资源,从而保证在持续Push界面过程中内存缓慢增长,但是实践过程中发现内存仍然持续增长。经过排查,我们发现flutter 1.0版本以及0.8.2版本里,SurfaceTextureRegistry提供了release方法,这里将会把创建的SurfaceTexture进行释放。然而测试过程中发现,单单对SurfaceTexture释放,并没有完全释放内存,当反复创建对象时仍然会闪退。为此,我们在AndroidExternalTextureGL的析构函数中增加了纹理的释放glDeleteTextures逻辑。然而,AndroidExternalTextureGL的析构是在flutter的GPU线程调用的,而external_texture的release方法通常是在主线程,也就是PlatForm线程调用的。不同线程调用的问题就是会导致一个诡异的问题:推测是不同线程释放的逻辑影响了GL环境,导致文字渲染出了问题。所以,为了解决该问题,我们删除了SurfaceTextureRegistry的release方法里面SufaceTexture的释放逻辑,并且将SurfaceTexture的释放,放到AndroidExternalTextureGL析构阶段,通过Jni调用java方法实现资源释放。AndroidExternalTextureGL::~AndroidExternalTextureGL(){ if (state_ == AttachmentState::attached) { Detach(); if (texture_name_ != 0) { glDeleteTextures(1, &texture_name_); texture_name_ = 0; } } Release(); state_ = AttachmentState::detached;}void AndroidExternalTextureGL::Release() { JNIEnv* env = fml::jni::AttachCurrentThread(); fml::jni::ScopedJavaLocalRef<jobject> surfaceTexture = surface_texture_.get(env); if (!surfaceTexture.is_null()) { SurfaceTextureRelease(env, surfaceTexture.obj()); }}void SurfaceTextureRelease(JNIEnv* env, jobject obj) { env->CallVoidMethod(obj, g_release_method); FML_CHECK(CheckException(env));} g_release_method = env->GetMethodID(g_surface_texture_class->obj(), “release”, “()V”);CPU优化—-FXTexImage_V3通过外界纹理渲染图片+不可见页面资源释放,我们解决了上述提出的一系列问题,但是又引入了新的问题:CPU偏高,滑动帧率偏低。通过测试,在详情页滑动过程中,IOS和Android的CPU都比Flutter原生组件高10%以上,这个显然无法应用。经过排查,发现CPU高的原因是:IOS端: iOS的IOSExternalTextureGL模型是一个拉数据的模型,native端register一个CVPixelBuffer的生产者,当需要绘制时,都会调用一次这个生产者的copyPixelbuffer方法去拉一次数据。然后将拉到的CVPixelBuffer对象转换成GPU Texture。这里每一次转换都换造成CPU较大开销。并且这种拉数据的机制就要求这个生产者的必须一直保留着这个CVPixelBuffer对象(否则界面重刷以后,图片区域就显示白屏)。Android端: android 的数据存储在SurfaceTexture中。每一次external_texture layer需要绘制时候都会从SurfaceTexture中去update 数据到纹理中,由于SurfaceTexture使用基于EGLImage共享内存,所以虽然没有双份内存的问题,但是每一次update 都会带来较大的CPU开销。在之前外接纹理的文章中,我们提出了一种新的基于共享上下文的外接纹理方案。并在我们视频的拍摄和编辑中得到了很好的应用。该方案当初提出来,是为了解决视频数据从CPU -> GPU -> CPU -> GPU 输送的问题而提出来的。但是在图片这个场景下, 新的外接纹理方案下,一张图片在native端加载完成以后,立刻被转换成一个OpenGL的Texture,然后图片的资源马上被释放。当界面刷新时,对于同一张图片的重新渲染,IOSExternalTextureGL不需要再去做将数据(CVPixelBuffer或者SurfaceTexture)转换到Texture的逻辑,而是直接使用之前创建好的Texture。经过这一步优化,我们很好的限制了iOS的CPU和内存,Android的CPU。通过测试对比,V3版本的图片组件,相比于Flutter原生图片组件,在详情页正常滑动操作过程中,平均CPU高出3%左右,虽然仍差于原生组件,单相对是可以接受的。结果内存: 基于新图片组件,我们很好的限制住了连续push 下的内存增长速度,顺利的将iPhone 6P上的详情页push 最大数量从10个增加到了30个以上。在同一线上版本中,我们通过控制ABTest开关,测试新的图片渲染方案和Flutter自带图片组件方案的Abort率,发现新图片组件下闲鱼的Abort率降低20%。安装包: 新组件下,所有的资源组件与原来native资源共用,所有flutter期间新引入的资源出了gif图,全部删除,减少安装包900k+。并且后续可以不用继续新增。寻址策略: 复用native图片组件,基于阿里系自己的图片下载组件,这样可以做到随着集团组件升级版本,兼容版本过程中各种新的寻址方式和图片格式。图片复用:复用native图片组件,当图片地址命中缓存,可直接缓存加载,尺寸不一致时可以预先返回缓存图同时加载大图,这样大大增强详情页大图预览的浏览体验。遗留问题图片组件已经在闲鱼上全量部署,然而还是有一些问题没有得到很好的解决,上文提到过CPU比原生图片组件高3%左右,虽然用户没有感官体验,但是还是有优化空间。还有就是Flutter针对ExternalTexture的纹理渲染时没有开启抗锯齿,导致小图在大区域渲染时比原生组件效果要差。这里还需要继续排查原因。最后,FXTexImage组件还在持续优化中,当解决上述遗留问题以后便会在Github上开源。本文作者:闲鱼技术-炉军阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 8, 2019 · 1 min · jiezi

从Thread.join说开去

WAITING(TIMED_WAITING) 与 BLOCKED看《Java特种兵》的时候发现,Thread.join可以使线程进入WAITING状态,再结合姊妹篇线程的状态我们可以了解到,有两个类状态非常接近:WAITING(TIMED_WAITING) 与 BLOCKED,这两者都会让线程看上去“阻塞”在某处了。什么时候线程会进入WAITING(无限期等待)的状态中呢?常用的有两个,分别是①Object.wait without timeout,②Thread.join without timeout【另外还有③LockSupport的park方法,④Conditon的await方法】;TIMED_WAITING除了①Object.wait with timeout、②Thread.join with timeout,还需要添加一条③Thread.sleep方法【另外还有④LockSupport的parkNanos方法,带有时间】。在进入WAITING状态前,线程会将持有的锁先释放掉。WAITING状态中的线程需要被其他线程对同一个对象调用notify()或notifyAll()方法才能唤醒。被notifyAll()唤醒后的线程,拿不到锁的线程将进入BLOCKED状态,直到它们拿到锁为止。简而言之,WAITING类状态中的线程和BLOCKED状态中的线程的区别在于:WAITING状态的线程需要被其他线程唤醒;BLOCKED中的线程,需要等待其他线程释放锁,此处的锁特指synchronized块。见下图Join为什么会使线程进入WAITING(TIMED_WAITING) 状态中呢?我们看一下底层代码 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException(“timeout value is negative”); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }从12、20行中可以看出,join方法底层调用的就是wait方法。sleep与wait同样会使线程进入TIMED_WAITING状态中的sleep和wait方法,它们之间有什么区别呢?wait()wait()方法释放锁wait()是Object类的方法wait()是非静态方法public final void wait() throws InterruptedException { //…}wait()应该被notify()或者notifyAll()方法唤醒wait()方法需要在循环中调用(推荐不强制)waint()方法必须放在synchronized上下文(synchronized方法或代码块)中,不然会抛出IllegalMonitorStateExceptionsleep()sleep()方法不会释放锁sleep()是java.lang.Thread类的方法sleep()是静态方法public static void sleep(long millis, int nanos) throws InterruptedException { //… }sleep()方法是在特定时间后结束sleep()方法最好不在放在循环中sleep()方法可以在任意地方执行wait为什么要放在循环中?synchronized(obj) { while (!condition) { obj.wait(); }}这里引用一段《Effective Java》始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性(liveness)是必要的。如果条件已经成立,并且在线程等待之前,notify (或者notifyAll)方法已经被调用, 则无法保证该线程将会从等待中苏醒过来。 在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性(safety)是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系。当条件不成立时,有下面一些理由可使一个线程苏醒过来:- 另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态。 - 条件并不成立,但是另一个线程可能意外地或恶意地调用了 notify。在公有可访问的对象上等待,这些类实际上把自己暴露在了这种危险的境地中。公有可访问对象的同步方法中包含的wait都会出现这样知问题。- 通知线程(notifying thread)在唤醒等待线程时可能会过度“大方”。例如,即使只有某一些等待线程的条件已经被满足,但是通知线程可能仍然调用notifyAll。 - 在没有通知的情况下,等待线程也可能(但很少)会苏醒过来。这被称为“伪唤醒 (spurious wakeup)”我们针对【跳过等待】和【继续等待】举个形象的例子:针对【前置判断,跳过等待】:如果是两个狙击手,在同时等待一个人(锁),判断条件是这个人还活着。如果没有前置的判断,在等待前不校验这个人是否活着,那么当狙击手甲杀死目标并通知狙击手乙之后,乙才进入等待状态,那么乙将会死等一个已经被杀死的目标,乙将失去活性(liveness)。针对【后置判断,继续等待】:还是两个狙击手,如果他们被唤醒后,没有后置校验,那么将导致可笑的错误,比如狙击手甲已经将目标杀死了,狙击手乙被唤醒后,没有再校验条件,直接开枪杀人,将会杀死目标两次。如果是幂等的还则罢了,不幂等的将导致错误。综上所述,wait前后都要校验,而最好的办法就是循环。Thread.join后谁在WAITING?从上文中我们已经知道了,join可以使线程处于WAITING状态,那问题来了,是子线程处于WAITING状态还是父线程处于WAITING状态?我们做个小试验:public class TestJoin { public static void main(String[] args) throws InterruptedException { Thread.currentThread().setName(“TestJoin main….”); Thread joinThread = new Thread(new Runnable() { @Override public void run() { for (; ; ) { } } }, “join thread”); joinThread.start(); joinThread.join(); }}按照在线程的状态中提供的方法,我们可以得到:子线程即join的线程依旧是RUNNABLE状态"join thread" #10 prio=5 os_prio=31 tid=0x00007fca1b801000 nid=0x5503 runnable [0x0000700001725000]java.lang.Thread.State: RUNNABLE at com.meituan.java8.thread.TestJoin$1.run(TestJoin.java:13) at java.lang.Thread.run(Thread.java:748)Locked ownable synchronizers: - None父线程(在此例中是主线程)为WAITING状态"TestJoin main…." #1 prio=5 os_prio=31 tid=0x00007fca1c003000 nid=0x1903 in Object.wait() [0x00007000003ec000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076ad94fa0> (a java.lang.Thread) at java.lang.Thread.join(Thread.java:1252) - locked <0x000000076ad94fa0> (a java.lang.Thread) at java.lang.Thread.join(Thread.java:1326) at com.meituan.java8.thread.TestJoin.main(TestJoin.java:20) Locked ownable synchronizers: - None我们对代码做稍稍改动,可以验证sleep后的线程在什么状态 Thread joinThread = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.MINUTES.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } } }, “sleeping thread”);“sleeping thread” #10 prio=5 os_prio=31 tid=0x00007f92620bc000 nid=0x5503 waiting on condition [0x00007000077b7000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at com.meituan.java8.thread.TestJoin$1.run(TestJoin.java:16) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None参考文档https://javaconceptoftheday.c…A thread enters into WAITING state when it calls wait() or join() method on an object. Before entering into WAITING state, thread releases the lock of the object it holds. It will remain in WAITING state until any other thread calls either notify() or notifyAll() on the same object.Once the other thread calls notify() or notifyAll() on the same object, one or all the threads which are WAITING for lock of that object will be notified. All the notified threads will not get the object lock immediately. They will get the object lock on a priority basis once the current thread releases the lock. Until that they will be in BLOCKED state.In simple terms, a thread will be in WAITING state if it is waiting for notification from other threads. A thread will be in BLOCKED state if it is waiting for other thread to release the lock it wants.https://stackoverflow.com/que…The difference is relatively simple.In the BLOCKED state, a thread is about to enter a synchronized block, but there is another thread currently running inside a synchronized block on the same object. The first thread must then wait for the second thread to exit its block.In the WAITING state, a thread is waiting for a signal from another thread. This happens typically by calling Object.wait(), or Thread.join(). The thread will then remain in this state until another thread calls Object.notify(), or dies.https://stackoverflow.com/a/3…wait()wait() method releases the lock.wait() is the method of Object class.wait() is the non-static method - public final void wait() throws InterruptedException { //…} wait() should be notified by notify() or notifyAll() methods.wait() method needs to be called from a loop in order to deal with false alarm.wait() method must be called from synchronized context (i.e. synchronized method or block), otherwise it will throw IllegalMonitorStateExceptionsleep()sleep() method doesn’t release the lock.sleep() is the method of java.lang.Thread class.sleep() is the static method - public static void sleep(long millis, int nanos) throws InterruptedException { //… }after the specified amount of time, sleep() is completed.sleep() better not to call from loop(i.e. see code below).sleep() may be called from anywhere. there is no specific requirement.4.《Effective Java》第10章 ...

March 25, 2019 · 3 min · jiezi

线程的状态

Thread.State首先看JDK中的代码: java.lang.Thread.State /** * A thread state. A thread can be in one of the following states: * 一个线程的状态,一个线程可以处于以下状态中的某一个状态 * <ul> * <li>{@link #NEW}<br> * A thread that has not yet started is in this state. * </li> * <li>{@link #RUNNABLE}<br> * A thread executing in the Java virtual machine is in this state. * </li> * <li>{@link #BLOCKED}<br> * A thread that is blocked waiting for a monitor lock * is in this state. * </li> * <li>{@link #WAITING}<br> * A thread that is waiting indefinitely for another thread to * perform a particular action is in this state. * </li> * <li>{@link #TIMED_WAITING}<br> * A thread that is waiting for another thread to perform an action * for up to a specified waiting time is in this state. * </li> * <li>{@link #TERMINATED}<br> * A thread that has exited is in this state. * </li> * </ul> * * <p> * A thread can be in only one state at a given point in time. * These states are virtual machine states which do not reflect * any operating system thread states. * * @since 1.5 * @see #getState / public enum State { /* * Thread state for a thread which has not yet started. / NEW, /* * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. / RUNNABLE, /* * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. / BLOCKED, /* * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called <tt>Object.wait()</tt> * on an object is waiting for another thread to call * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on * that object. A thread that has called <tt>Thread.join()</tt> * is waiting for a specified thread to terminate. / WAITING, /* * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> / TIMED_WAITING, /* * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }1、新建(New)新创建了一个线程对象,还未调用start()方法。2、就绪(Runnable)线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中 获取cpu 的使用权 。3、运行中(Running,线程状态中并没有这一状态,但是实际执行中是有的)可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。4、限期等待(Timed Waiting)也可以称作 TIMED_WAITING(有等待时间的等待状态)。线程主动调用以下方法:Thread.sleep方法;Object的wait方法,带有时间;Thread.join方法,带有时间;LockSupport的parkNanos方法,带有时间。5、无限期等待(Waiting)运行中(Running)的线程执行了以下方法:Object的wait方法,并且没有使用timeout参数;Thread的join方法,没有使用timeout参数;LockSupport的park方法;Conditon的await方法。6、阻塞(Blocked)阻塞状态是指线程因为某种原因放弃了cpu 使用权,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分两种:同步阻塞:运行(running)的线程进入了一个synchronized方法,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。其他阻塞:运行(running)的线程发出了I/O请求时,JVM会把该线程置为阻塞状态。当I/O处理完毕时,线程重新转入可运行(runnable)状态。7、结束(Terminated)线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。线程状态探秘jstack查看线程状态jstack -l <pid> 即可察看线程状态,如何使用呢?随便写一个死循环看一下public class TestThreadState { public static void main(String[] args) { for (; ; ) { } }}ps -ef|grep TestThreadState,找到对应的pid,jstack -l <pid>即可,如果未输出线程信息,可以尝试使用-F参数来强制输出。“main” #1 prio=5 os_prio=31 tid=0x00007f8194801800 nid=0x1603 runnable [0x000070000a9b4000] java.lang.Thread.State: RUNNABLE at org.java.bin.TestThreadState.main(TestThreadState.java:12) ...

March 25, 2019 · 3 min · jiezi

【J2SE】java并发基础

并发简述并发通常是用于提高运行在单处理器上的程序的性能。在单 CPU 机器上使用多任务的程序在任意时刻只在执行一项工作。并发编程使得一个程序可以被划分为多个分离的、独立的任务。一个线程就是在进程中的一个单一的顺序控制流。java的线程机制是抢占式。线程的好处是提供了轻量级的执行上下文切换,只改变了程序的执行序列和局部变量。多线程的主要缺陷:<!– java编程思想 –>等待共享资源的时候性能降低。需要处理线程的额外 CPU 花费。糟糕的程序设计导致不必要的复杂度。有可能产生一些病态行为,若饿死、竞争、死锁和活锁。不同平台导致的不一样。volatile关键字源来:当程序运行,JVM会为每一个线程分配一个独立的缓存用于提高执行效率,每一个线程都在自己独立的缓存中操作各自的数据。一个线程在缓冲中对数据进行修改,写入到主存后,其他线程无法得知数据已被更改,仍在操作缓存中已过时的数据,为了解决这个问题,提供了volatile关键字,实现内存可见,一旦主存数据被修改,便致使其他线程缓存数据行无效,强制前往主存获取新数据。Example:内存不可见,导致主线程无法结束。class ThreadDemo implements Runnable { //添加volatile关键字可实现内存可见性 public volatile boolean flag = false; public boolean flag = Boolean.false; @Override public void run() { try { Thread.sleep(200); } catch (InterruptedException e) { } flag = Boolean.true; System.out.println(“ThreadDemo over”); } public boolean isFlag() { return flag; }}public class TestVolatile { public static void main(String[] args) { ThreadDemo demo = new ThreadDemo(); new Thread(demo).start(); while (true) { if (demo.flag || demo.isFlag()) { System.out.println(“Main over”); break; } } }}/output:打印ThreadDemo over,主线程持续循环/作用:当多个线程操作共享数据时,保证内存中的数据可见性。采用底层的内存栅栏,及时的将缓存中修改的数据刷新到主存中,并导致其他线程所缓存的数据无效,使得这些线程必须去主存中获取修改的数据。优缺点:保证内存可见性,让各个线程能够彼此获取最新的内存数据。较传统synchronized加锁操作提高了效率,若有线程正在操作被synchronized修饰的代码块数据时,其他线程试图进行操作,发现已被其他线程占用,试图操作的线程必须挂起,等到下一次继续尝试操作。对volatile修饰的数据被修改后,其他线程必须前往主存中读取,若修改频繁,需要不断读取主存数据,效率将会降低。使用volatile,底层采用内存栅栏,JVM将不会对其提供指令重排序及其优化。不具备互斥性。多个线程可以同时对数据进行操作,只是由原来的在缓存操作转变成了直接在主存中操作。(synchronized是互斥的,一个线程正在执行,其他线程必须挂起等待)不保证变量的原子性。使用volatile仅仅是一个能保证可见性的轻量级同步策略。原子变量与 CAS 算法Example:使用volatile修饰,number自增问题。class ThreadDemo implements Runnable { public volatile int number = 0; @Override public void run() { try { Thread.sleep(200); } catch (Exception e) { } System.out.print(getIncrementNumber() + " “); } public int getIncrementNumber() { return ++number; }}public class TestAtomic { public static void main(String[] args) { ThreadDemo demo = new ThreadDemo(); for (int i = 0; i < 10; i++) { new Thread(demo).start(); } }}/*output: 1 5 4 7 3 9 2 1 8 6 /// ++number底层原理思想int temp = number; // ①number = number + 1; // ②temp = number; // ③return temp; // ④由 ++number 可知,返回的是 temp 中存储的值,且自增是一个多步操作,当多个线程调用 incrementNumber方法时,方法去主存中获取 number 值放入 temp 中,根据 CPU 时间片切换,当 A 线程完成了 ③ 操作时,时间片到了被中断,A 线程开始执行 ① 时不幸被中断,接着 A 获取到了CPU执行权,继续执行完成 ④ 操作更新了主存中的值,紧接着 B 线程开始执行,但是 B 线程 temp中存储的值已经过时了。注意:自增操作为四步,只有在第四步的时候才会刷新主存的值,而不是number = number + 1 操作就反映到主存中去。如图所示:源来:volatile只能保证内存可见性,对多步操作的变量,无法保证其原子性,为了解决这个问题,提供了原子变量。作用:原子变量既含有volatile的内存可见性,又提供了对变量原子性操作的支持,采用底层硬件对并发操作共享数据的 CAS(Compare-And-Swap)算法,保证数据的原子性。提供的原子类:类描述AtomicBoolean一个 boolean值可以用原子更新。AtomicInteger可能原子更新的 int值。AtomicIntegerArray一个 int数组,其中元素可以原子更新。AtomicIntegerFieldUpdater<T>基于反射的实用程序,可以对指定类的指定的 volatile int字段进行原子更新。AtomicLong一个 long值可以用原子更新。AtomicLongArray可以 long地更新元素的 long数组。AtomicLongFieldUpdater<T>基于反射的实用程序,可以对指定类的指定的 volatile long字段进行原子更新。AtomicMarkableReference<V>AtomicMarkableReference维护一个对象引用以及可以原子更新的标记位。AtomicReference<V>可以原子更新的对象引用。AtomicReferenceArray<E>可以以原子方式更新元素的对象引用数组。AtomicReferenceFieldUpdater<T,V>一种基于反射的实用程序,可以对指定类的指定的 volatile volatile引用原子更新。AtomicStampedReference<V>AtomicStampedReference维护对象引用以及可以原子更新的整数“印记”。DoubleAccumulator一个或多个变量一起维护使用提供的功能更新的运行的值 double 。DoubleAdder一个或多个变量一起保持初始为零 double和。LongAccumulator一个或多个变量,它们一起保持运行 long使用所提供的功能更新值。LongAdder一个或多个变量一起保持初始为零 long总和。CAS算法:CAS(Compare-And-Swap)是底层硬件对于原子操作的一种算法,其包含了三个操作数:内存值(V),预估值(A),更新值(B)。当且仅当 V == A 时, 执行 V = B 操作;否则不执行任何结果。这里需要注意,A 和 B 两个操作数是原子性的,同一时刻只能有一个线程进行AB操作。优缺点:操作失败时,直接放弃结果,并不释放对CPU的控制权,进而可以继续尝试操作,不必挂起等待。(synchronized会让出CPU)当多个线程并发的对主存中的数据进行操作时,有且只有一个会成功,其余均失败。原子变量中封装了用于对数据的原子操作,简化了代码的编写。Collection并发类HashMap 与 HashTable简述HashMap是线程不安全的,而HashTable是线程安全的,因为HashTable所维护的Hash表存在着独占锁,当多个线程并发访问时,只能有一个线程可进行操作,但是对于复合操作时,HashTable仍然存在线程安全问题,不使用HashTable的主要原因还是效率低下。// 功能:不包含obj,则添加if (!hashTable.contains(obj)) { // 复合操作,执行此处时线程中断,obj被其他线程添加至容器中,此处继续执行将导致重复添加 hashTable.put(obj);}可知上述两个操作需要 “原子性”,为了达到效果,还不是得对代码块进行同步ConcurrentHashMap采用锁分段机制,分为 16 个段(并发级别),每一个段下有一张表,该表采用链表结构链接着各个元素,每个段都使用独立的锁。当多个线程并发操作的时候,根据各自的级别不同,操作不同的段,多个线程并行操作,明显提高了效率,其次还提供了复合操作的诸多方法。注:jdk1.8由原来的数组+单向链表结构转换成数据+单向链表+红黑树结构。ConcurrentSkipListMap和ConcurrentSkipListSet有序的哈希表,通过跳表实现,不允许null作为键或值。ConcurrentSkipListMap详解CopyOnWriteArrayList 和 CopyOnWriteArraySet对collection进行写入操作时,将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行。当修改完成时,一个原子性的操作将把心的数组换人,使得新的读取操作可以看到新的修改。<!–Java编程思想–>好处之一是当多个迭代器同时遍历和修改列表时,不会抛出ConcurrentModificationException。小结:当期望许多线程访问一个给定 collection 时,ConcurrentHashMap 通常优于同步的 HashMapConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。并发迭代操作多时,可选择CopyOnWriteArrayList 和 CopyOnWriteArraySet。高并发情况下,可选择ConcurrentSkipListMap和ConcurrentSkipListSetCountDownLatch闭锁源由:当一个修房子的 A 线程正在执行,需要砖头时,开启了一个线程 B 去拉砖头,此时 A 线程需要等待 B 线程的结果后才能继续执行时,但是线程之间都是并行操作的,为了解决这个问题,提供了CountDownLatch。作用:一个同步辅助类,为了保证执行某些操作时,“所有准备事项都已就绪”,仅当某些操作执行完毕后,才能执行后续的代码块,否则一直等待。CountDownLatch中存在一个锁计数器,如果锁计数器不为 0 的话,它会阻塞任何一个调用 await() 方法的线程。也就是说,当一个线程调用 await() 方法时,如果锁计数器不等于 0,那么就会一直等待锁计数器为 0 的那一刻,这样就解决了需要等待其他线程执行完毕才执行的需求。Example:class ThreadDemo implements Runnable { private CountDownLatch latch = null; public ThreadDemo(CountDownLatch latch) { this.latch = latch; } @Override public void run() { try { System.out.println(“execute over”); } finally { latch.countDown(); // 必须保证计数器减一 } }}public class TestCountDownLatch { public static void main(String[] args) { final int count = 10; final CountDownLatch latch = new CountDownLatch(count); ThreadDemo demo = new ThreadDemo(latch); for (int i = 0; i < count; ++i) { new Thread(demo).start(); } try { latch.await(); // 等待计数器为 0 System.out.println(“其他线程结束,继续往下执行…”); } catch (InterruptedException e) { e.printStackTrace(); } }}/**output: execute over … 其他线程结束,继续往下执行…/细节:子线程完毕后,必须调用 countDown() 方法使得 锁计数器减一,否则将会导致调用 await() 方法的线程持续等待,尽可能的放置在 finally 中。锁计数器的个数与子线程数最好相等,只要计数器等于 0,不论是否还存在子线程,await() 方法将得到响应,继续执行后续代码。Callable接口源由:当开启一个线程执行运算时,可能会需要该线程的计算结果,之前的 implements Runnable 和 extends Thread 的 run() 方法并没有提供可以返回的功能,因此提供了 Callable接口。 Callable 的运行结果, 需要使用 FutureTask 类来接受。Example:class ThreadDemo implements Callable<Integer> { private Integer cycleValue; public ThreadDemo(Integer cycleValue) { this.cycleValue = cycleValue; } @Override public Integer call() throws Exception { int result = 0; for (int i=0; i<cycleValue; ++i) { result += i; } return result; } }public class TestCallable { public static void main(String[] args) throws Exception { ThreadDemo demo = new ThreadDemo(Integer.MAX_VALUE); // 使用FutureTask接受结果 FutureTask<Integer> task = new FutureTask<>(demo); new Thread(task).start(); Integer result = task.get(); // 等待计算结果返回, 闭锁 System.out.println(result); }}/output:1073741825 /Lock同步锁和Condition线程通信控制对象Lock:在进行性能测试时,使用Lock通常会比使用synchronized要高效许多,并且synchronized的开销变化范围很大,而Lock相对稳定。只有在性能调优时才使用Lock对象。<!–Java编程思想–>Condition: 替代了 Object 监视器方法的使用,描述了可能会与锁有关的条件标量,相比 Object 的 notifyAll() ,Condition 的 signalAll() 更安全。Condition 实质上被绑定到一个锁上,使用newCondition() 方法为 Lock 实例获取 Condition。Lock和Condition对象只有在困难的多线程问题中才是必须的。<!–Java编程思想–>synchonized与Lock的区别:synchonizedLock隐式锁显示锁JVM底层实现,由JVM维护由程序员手动维护 灵活控制(也有风险)“虚假唤醒”:当一个线程A在等待时,被另一个线程唤醒,被唤醒的线程不一定满足了可继续向下执行的条件,如果被唤醒的线程未满足条件,而又向下执行了,那么称这个现象为 “虚假唤醒”。// 安全的方式,保证退出等待循环前,一定能满足条件while (条件) { wait();}Example:生产消费者<!–参考Java编程思想 P712–>// 产品carclass Car { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private boolean available = false; // false:无货;true有货 public void put(){ lock.lock(); try { while (available) { // 有货等待 condition.await(); } System.out.println(Thread.currentThread().getName() + “put(): 进货”); available = true; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void get() { lock.lock(); try { while (!available) { // 无货等待 condition.await(); } System.out.println(Thread.currentThread().getName() + “get():出货”); available = false; condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }}// 消费者class Consume implements Runnable { private Car car; public Consume(Car car) { this.car = car; } @Override public void run() { for (int i=0; i<TestProduceAndConsume.LOOP_SIZE; ++i) { car.get(); try { Thread.sleep(100); } catch (InterruptedException e) { } } } }// 生产者class Produce implements Runnable { private Car car; public Produce(Car car) { this.car = car; } @Override public void run() { for (int i=0; i<TestProduceAndConsume.LOOP_SIZE; i++) { car.put(); } } }public class TestProduceAndConsume { public static final int LOOP_SIZE = 10; public static void main(String[] args) { Car car = new Car(); for (int i=0; i<5; ++i) { Consume consume = new Consume(car); Produce produce = new Produce(car); new Thread(consume, i + “–”).start(); new Thread(produce, i + “–”).start(); } } }每一个 对lock()的调用都必须紧跟着一个 try-finally 子句,用以保证可以在任何情况下都能释放锁,任务在调用 await()、signal()、signalAll()之前,必须拥有锁。lock.lock();try { … // 业务代码} finally { lock.unlock();}ReadWriteLock读写锁源由:上述讲解的锁都是读写一把锁,不论是读或写,都是一把锁解决,当多线程访问数据时,若发生了一千次操作,其中的写操作只执行了一次,数据的更新率非常低,那么每次进行读操作时,都要加锁读取”不会更改的“数据,显然是不必要的开销,因此出现了 ReadWriteLock 读写锁,该对象提供读锁和写锁。作用:ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 write写入操作,那么多个线程可以同时进行持有读锁。而写入锁是独占的,当执行写操作时,其他线程不可写,也不可读。性能的提升取决于读写操作期间读取数据相对于修改数据的频率,如果读取操作远远大于写入操作时,便能增强并发性。Example:class Demo { private int value = 0; private ReadWriteLock lock = new ReentrantReadWriteLock(); public void read() { lock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " : " + value); } finally { lock.readLock().unlock(); } } public void write(int value) { lock.writeLock().lock(); try { this.value = value; System.out.println(“write(” + value + “)”); } finally { lock.writeLock().unlock(); } }}class ReadLock implements Runnable { private Demo demo = null; public ReadLock(Demo demo) { this.demo = demo; } @Override public void run() { for (int i=0; i<20; ++i) { demo.read(); try { Thread.sleep(320); } catch (InterruptedException e) { } } } }class WriteLock implements Runnable { private Demo demo = null; public WriteLock(Demo demo) { this.demo = demo; } @Override public void run() { for (int i=0; i<10; ++i) { demo.write(i); try { Thread.sleep(200); } catch (InterruptedException e) { } } } }public class TestReadWriteLock { public static void main(String[] args) { Demo demo = new Demo(); ReadLock readLock = new ReadLock(demo); WriteLock writeLock = new WriteLock(demo); for (int i=0; i<3; ++i) { new Thread(readLock, i + “–”).start(); } new Thread(writeLock).start(); }}/**output: 0– : 0 1– : 0 2– : 0 write(0) write(1) 1– : 1 2– : 1 0– : 1 write(2) write(3) 1– : 3 0– : 3 …/线程池与线程调度源来:在传统操作中(如连接数据库),当我们需要使用一个线程的时候,就 直接创建一个线程,线程完毕后被垃圾收集器回收。每一次需要线程的时候,不断的创建与销毁,大大增加了资源的开销。作用:线程池维护着一个线程队列,该队列中保存着所有等待着的线程,避免了重复的创建与销毁而带来的开销。体系结构:Execuotr:负责线程的使用与调度的根接口。 |- ExecutorService:线程池的主要接口。 |- ForkJoinPool:采用分而治之技术将任务分解。 |- ThreadPoolExecutor:线程池的实现类。 |- ScheduledExecutorService:负责线程调度的子接口。 |- ScheduledThreadPoolExecutor:负责线程池的调度。继承ThreadPoolExecutor并实现ScheduledExecutorService接口Executors 工具类API描述:方法描述ExecutorService newFixedThreadPool(int nThreads)创建一个可重用固定数量的无界队列线程池。使用了有限的线程集来执行所提交的所有任务。创建的时候可以一次性预先进行代价高昂的线程分配。ExecutorService newWorkStealingPool(int parallelism)创建一个维护足够的线程以支持给定的parallelism并行级别的线程池。ExecutorService newSingleThreadExecutor()创建一个使用单个线程运行的无界队列的执行程序。ExecutorService newCachedThreadPool()创建一个根据需要创建新线程的线程池,当有可用线程时将重新使用以前构造的线程。ScheduledExecutorService newSingleThreadScheduledExecutor()创建一个单线程执行器,可以调度命令在给定的延迟之后运行,或定期执行。ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。ThreadFactory privilegedThreadFactory()返回一个用于创建与当前线程具有相同权限的新线程的线程工厂。补充:ExecutorService.shutdown():防止新任务被提交,并继续运行被调用之前所提交的所有任务,待任务都完成后退出。CachedThreadPoo在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,是Executor的首选。仅当这个出现问题时,才需切换 FixedThreadPool。SingleThreadExecutor: 类似于线程数量为 1 的FixedThreadPool,但它提供了不会存在两个及以上的线程被并发调用的并发。Example:线程池public class TestThreadPool { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(2); for (int i = 0; i < 10; ++i) { Future<String> future = pool.submit(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName(); } }); String threadName = future.get(); System.out.println(threadName); } pool.shutdown(); // 拒绝新任务并等待正在执行的线程完成当前任务后关闭。 }}/**output: pool-1-thread-1 pool-1-thread-2 pool-1-thread-1 pool-1-thread-2 …/Example:线程调度public class TestThreadPool { public static void main(String[] args) throws Exception { ScheduledExecutorService pool = Executors.newScheduledThreadPool(2); for (int i = 0; i < 5; ++i) { ScheduledFuture<String> future = pool.schedule(new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName() + " : " + Instant.now(); } }, 1, TimeUnit.SECONDS); // 延迟执行单位为 1秒的任务 String result = future.get(); System.out.println(result); } pool.shutdown(); }}/*output: pool-1-thread-1 : 2019-03-18T12:10:31.260Z pool-1-thread-1 : 2019-03-18T12:10:32.381Z pool-1-thread-2 : 2019-03-18T12:10:33.382Z pool-1-thread-1 : 2019-03-18T12:10:34.383Z pool-1-thread-2 : 2019-03-18T12:10:35.387Z/<span style=“color: red”>注意:若没有执行 shutdown()方法,则线程会一直等待而不停止。</span>ForkJoinPool分支/合并框架源由:在一个线程队列中,假如队头的线程由于某种原因导致了阻塞,那么在该队列中的后继线程需要等待队头线程结束,只要队头一直阻塞,这个队列中的所有线程都将等待。此时,可能其他线程队列都已经完成了任务而空闲,这种情况下,就大大减少了吞吐量。ForkJoin的“工作窃取”模式:当执行一个新任务时,采用分而治之的思想,将其分解成更小的任务执行,并将分解的任务加入到线程队列中,当某一个线程队列没有任务时,会随机从其他线程队列中“偷取”一个任务,放入自己的队列中执行。Example:// 求次方: value为底,size为次方数class CountPower extends RecursiveTask<Long> { private static final long serialVersionUID = 1L; public Long value = 0L; public int size = 0; public static final Long CRITICAL = 10L; // 阈值 public CountPower(Long value, int size) { this.value = value; this.size = size; } @Override protected Long compute() { // 当要开方的此时 小于 阈值,则计算 (视为最小的任务单元) if(size <= CRITICAL) { Long sum = 1L; for (int i=0; i<size; ++i) { sum *= value; } return sum; } else { int mid = size / 2; // 拆分任务,并压入线程队列 CountPower leftPower = new CountPower(value, mid); leftPower.fork(); CountPower rightPower = new CountPower(value, size - mid); rightPower.fork(); // 将当前两个任务返回的执行结果再相乘 return leftPower.join() * rightPower.join(); } } }public class TestForkJoinPool { public static void main(String[] args) throws Exception { ForkJoinPool pool = new ForkJoinPool(); CountPower task = new CountPower(2L, 11); Long result = pool.invoke(task); System.out.println(result); }}/*output: 2048/根据分而治之的思想进行分解,需要一个结束递归的条件,该条件内的代码就是被分解的最小单元。使用fork()在当前任务正在运行的池中异步执行此任务,即将该任务压入线程队列。调用join()`返回计算结果。RecursiveTask是有返回值的task,RecursiveAction则是没有返回值的。参考尚硅谷JUC视频教程《java编程思想》第 21 章 并发 ...

March 19, 2019 · 6 min · jiezi

刚刚,阿里开源 iOS 协程开发框架 coobjc!

阿里妹导读:刚刚,阿里巴巴正式对外开源了基于 Apache 2.0 协议的协程开发框架 coobjc,开发者们可以在 Github 上自主下载。coobjc是为iOS平台打造的开源协程开发框架,支持Objective-C和Swift,同时提供了cokit库为Foundation和UIKit中的部分API提供了协程化支持,本文将为大家详细介绍coobjc的设计理念及核心优势。开源地址https://github.com/alibaba/coobjciOS异步编程问题从2008年第一个iOS版本发布至今的11年时间里,iOS的异步编程方式发展缓慢。基于 Block 的异步编程回调是目前 iOS 使用最广泛的异步编程方式,iOS 系统提供的 GCD 库让异步开发变得很简单方便,但是基于这种编程方式的缺点也有很多,主要有以下几点:容易进入"嵌套地狱"错误处理复杂和冗长容易忘记调用 completion handler条件执行变得很困难从互相独立的调用中组合返回结果变得极其困难在错误的线程中继续执行(如子线程操作UI)难以定位原因的多线程崩溃(手淘中多线程crash已占比60%以上)锁和信号量滥用带来的卡顿、卡死针对多线程以及尤其引发的各种崩溃和性能问题,我们制定了很多编程规范、进行了各种新人培训,尝试降低问题发生的概率,但是问题依然很严峻,多线程引发的问题占比并没有明显的下降,异步编程本来就是很复杂的事情,单靠规范和培训是难以从根本上解决问题的,需要有更加好的编程方式来解决。解决方案上述问题在很多系统和语言开发中都可能会碰到,解决问题的标准方式就是使用协程,C#、Kotlin、Python、Javascript 等热门语言均支持协程极其相关语法,使用这些语言的开发者可以很方便的使用协程及相关功能进行异步编程。2017 年的 C++ 标准开始支持协程,Swift5 中也包含了协程相关的标准,从现在的发展趋势看基于协程的全新的异步编程方式,是我们解决现有异步编程问题的有效的方式,但是苹果基本已经不会升级 Objective-C 了,因此使用Objective-C的开发者是无法使用官方的协程能力的,而最新 Swift 的发布和推广也还需要时日,为了让广大iOS开发者能快速享受到协程带来的编程方式上的改变,手机淘宝架构团队基于长期对系统底层库和汇编的研究,通过汇编和C语言实现了支持 Objective-C 和 Swift 协程的完美解决方案 —— coobjc。核心能力提供了类似C#和Javascript语言中的Async/Await编程方式支持,在协程中通过调用await方法即可同步得到异步方法的执行结果,非常适合IO、网络等异步耗时调用的同步顺序执行改造。提供了类似Kotlin中的Generator功能,用于懒计算生成序列化数据,非常适合多线程可中断的序列化数据生成和访问。提供了Actor Model的实现,基于Actor Model,开发者可以开发出更加线程安全的模块,避免由于直接函数调用引发的各种多线程崩溃问题。提供了元组的支持,通过元组Objective-C开发者可以享受到类似Python语言中多值返回的好处。内置系统扩展库提供了对NSArray、NSDictionary等容器库的协程化扩展,用于解决序列化和反序列化过程中的异步调用问题。提供了对NSData、NSString、UIImage等数据对象的协程化扩展,用于解决读写IO过程中的异步调用问题。提供了对NSURLConnection和NSURLSession的协程化扩展,用于解决网络异步请求过程中的异步调用问题。提供了对NSKeyedArchieve、NSJSONSerialization等解析库的扩展,用于解决解析过程中的异步调用问题。coobjc设计最底层是协程内核,包含了栈切换的管理、协程调度器的实现、协程间通信channel的实现等。中间层是基于协程的操作符的包装,目前支持async/await、Generator、Actor等编程模型。最上层是对系统库的协程化扩展,目前基本上覆盖了Foundation和UIKit的所有IO和耗时方法。核心实现原理协程的核心思想是控制调用栈的主动让出和恢复。一般的协程实现都会提供两个重要的操作:Yield:是让出cpu的意思,它会中断当前的执行,回到上一次Resume的地方。Resume:继续协程的运行。执行Resume后,回到上一次协程Yield的地方。我们基于线程的代码执行时候,是没法做出暂停操作的,我们现在要做的事情就是要代码执行能够暂停,还能够再恢复。 基本上代码执行都是一种基于调用栈的模型,所以如果我们能把当前调用栈上的状态都保存下来,然后再能从缓存中恢复,那我们就能够实现yield和 resume。实现这样操作有几种方法呢?第一种:利用glibc 的 ucontext组件(云风的库)。第二种:使用汇编代码来切换上下文(实现c协程),原理同ucontext。第三种:利用C语言语法switch-case的奇淫技巧来实现(Protothreads)。第四种:利用了 C 语言的 setjmp 和 longjmp。第五种:利用编译器支持语法糖。上述第三种和第四种只是能过做到跳转,但是没法保存调用栈上的状态,看起来基本上不能算是实现了协程,只能算做做demo,第五种除非官方支持,否则自行改写编译器通用性很差。而第一种方案的 ucontext 在iOS上是废弃了的,不能使用。那么我们使用的是第二种方案,自己用汇编模拟一下 ucontext。模拟ucontext的核心是通过getContext和setContext实现保存和恢复调用栈。需要熟悉不同CPU架构下的调用约定(Calling Convention). 汇编实现就是要针对不同cpu实现一套,我们目前实现了 armv7、arm64、i386、x86_64,支持iPhone真机和模拟器。Show me the code说了这么多,还是看看代码吧,我们从一个简单的网络请求加载图片功能来看看coobjc到底是如何使用的。下面是最普通的网络请求的写法:下面是使用coobjc库协程化改造后的代码:原本需要20行的代码,通过coobjc协程化改造后,减少了一半,整个代码逻辑和可读性都更加好,这就是coobjc强大的能力,能把原本很复杂的异步代码,通过协程化改造,转变成逻辑简洁的顺序调用。coobjc还有很多其他强大的能力,本文对于coobjc的实际使用就不过多介绍了,感兴趣的朋友可以去官方github仓库自行下载查看。性能提升我们在iPhone7 iOS11.4.1的设备上使用协程和传统多线程方式分别模拟高并发读取数据的场景,下面是两种方式得到的压测数据。测试机器:iPhone7 iOS11.4.1数据文件大小:20M协程最多使用线程数:4数据测试结果(统计的是所有并发访问结束的总耗时):从上面的表格我们可以看到使用在并发量很小的场景,由于多线程可以完全使用设备的计算核心,因此coobjc总耗时要比传统多线程略高,但是由于整体耗时都很小,因此差异并不明显,但是随着并发量的增大,coobjc的优势开始逐渐体现出来,当并发量超过1000以后,传统多线程开始出现线程分配异常,而导致很多并发任务并没有执行,因此在上表中显示的是大于20秒,实际是任务已经无法正常执行了,但是coobjc仍然可以正常运行。我们在手机淘宝这种超级App中尝试了协程化改造,针对部分性能差的页面,我们发现在滑动过程中存在很多主线程IO调用、数据解析,导致帧率下降严重,通过引入coobjc,在不改变原有业务代码的基础上,通过全局hook部分IO、数据解析方法,即可让原来在主线程中同步执行的IO方法异步执行,并且不影响原有的业务逻辑,通过测试验证,这样的改造在低端机(iPhone6及以下的机器)上的帧率有20%左右的提升。优势简明概念少:只有很少的几个操作符,相比响应式几十个操作符,简直不能再简单了。原理简单:协程的实现原理很简单,整个协程库只有几千行代码。易用使用简单:它的使用方式比GCD还要简单,接口很少。改造方便:现有代码只需要进行很少的改动就可以协程化,同时我们针对系统库提供了大量协程化接口。清晰同步写异步逻辑:同步顺序方式写代码是人类最容易接受的方式,这可以极大的减少出错的概率。可读性高:使用协程方式编写的代码比block嵌套写出来的代码可读性要高很多。性能调度性能更快:协程本身不需要进行内核级线程的切换,调度性能快,即使创建上万个协程也毫无压力。减少卡顿卡死: 协程的使用以帮助开发减少锁、信号量的滥用,通过封装会引起阻塞的IO等协程接口,可以从根源上减少卡顿、卡死,提升应用整体的性能。总结程序是写来给人读的,只会偶尔让机器执行一下。——Abelson and Sussman基于协程实现的编程范式能够帮助开发者编写出更加优美、健壮、可读性更强的代码。协程可以帮助我们在编写并发代码的过程中减少线程和锁的使用,提升应用的性能和稳定性。本文作者:淘宝技术阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

March 1, 2019 · 1 min · jiezi

Spring Cloud Alibaba迁移指南(一):一行代码从 Hystrix 迁移到 Sentinel

摘要: 本文对Hystrix、Resilience4j、Sentinel进行对比,并探讨如何使用一行代码这种极简的方式,将Hystrix迁移到Sentinel。 Hystrix 自从前段时间 宣布停止维护之后,社区推荐了 resilience4j。自 Spring Cloud 官方宣布 Spring Cloud Netflix 进入维护状态后,我们开始制作《Spring Cloud Alibaba迁移指南》系列文章,向开发者提供更多的技术选型方案,并降低迁移过程中的技术难度。第一篇,我们对Hystrix、Resilience4j 和 Sentinel 三个开源项目进行对比,并探讨如何使用一行代码这种极简的方式,将Hystrix迁移到Sentinel。Hystrix 自从前段时间 宣布停止维护之后,社区推荐了 resilience4j。这 3 款产品各有优劣势,具体的功能差异参考下表(该表来源 Sentinel Wiki): SentinelHystrixresilience4j隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间实时统计实现滑动窗口(LeapArray)滑动窗口(基于 RxJava)Ring Bit Buffer动态规则配置支持多种数据源支持多种数据源有限支持扩展性多个扩展点插件的形式接口的形式基于注解的支持支持支持支持限流基于 QPS,支持基于调用关系的限流有限的支持Rate Limiter流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的 Rate Limiter 模式系统自适应保护支持不支持不支持控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其它监控系统目前 Sentinel 在 Spring Cloud Alibaba 项目中已经适配了 Spring Cloud 体系,可以用来完全替代 Hystrix 的功能了。Spring Cloud Alibaba Sentinel 功能介绍Spring Cloud Alibaba Sentinel 主要是为了整合 Sentinel 和 Spring Boot/Cloud 技术栈。目前完成了如下功能:自动适配 Servlet 容器。只需要配置 url-pattern(默认拦截所有请求),即可对拦截的这些 url 进行限流降级操作整合了 RestTemplate,使用 RestTemplate 进行操作的时候会被限流降级整合了 Feign,兼容了原先的 Hystrix 支持 Feign 的编程模型数据源可配置化,只需在配置文件中配置数据源信息,即可动态加载规则Endpoint 暴露各种元数据,比如配置信息,规则数据HealthIndicator 检查 Sentinel 健康状态 (整合中)支持 Spring Cloud Gateway 和 Zuul (整合中)Spring Cloud Alibaba Sentinel 代替 Hystrix想要使用 Spring Cloud Alibaba Sentinel,需要加上依赖信息:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel</artifactId> <version>0.2.1.RELEASE</version></dependency>0代码修改兼容 Feign加上 Feign 的依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>${latest.version}</version></dependency>把 hystrix 的配置改成 sentinel 的即可使用 Sentinel 的限流降级功能:# feign.hystrix.enabled: truefeign.sentinel.enabled: true@FeignClient(name = “service-provider”)public interface EchoService { @RequestMapping(value = “/echo/{str}”, method = RequestMethod.GET) String echo(@PathVariable(“str”) String str); @RequestMapping(value = “/echo/save”, method = RequestMethod.POST) String save(Foo foo);}对于这个 EchoService,echo 方法对应的资源名是 GET:http://service-provider/echo/{str}, save 方法对应的资源名是 POST:http://service-provider/echo/save。只需配置这些规则,限流降级操作即可立即生效。一行代码支持 RestTemplateSentinel 还跟 Spring 生态的 RestTemplate 做了整合,可以对 RestTemplate 请求过程进行限流和降级操作,只需要在构造 RestTemplate 的时候加上 @SentinelRestTemplate 注解即可:@Bean@SentinelRestTemplatepublic RestTemplate restTemplate() { return new RestTemplate();}@SentinelRestTemplate 注解还暴露出了对应的属性可进行限流降级后的自定义错误,默认的行为是返回 “RestTemplate request block by sentinel” 信息。关于 @SentinelRestTemplate 的详细信息可以参考 Wiki。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 26, 2019 · 1 min · jiezi

Tensorflow源码解析3 -- TensorFlow核心对象 - Graph

1 Graph概述计算图Graph是TensorFlow的核心对象,TensorFlow的运行流程基本都是围绕它进行的。包括图的构建、传递、剪枝、按worker分裂、按设备二次分裂、执行、注销等。因此理解计算图Graph对掌握TensorFlow运行尤为关键。2 默认Graph默认图替换之前讲解Session的时候就说过,一个Session只能run一个Graph,但一个Graph可以运行在多个Session中。常见情况是,session会运行全局唯一的隐式的默认的Graph,operation也是注册到这个Graph中。也可以显示创建Graph,并调用as_default()使他替换默认Graph。在该上下文管理器中创建的op都会注册到这个graph中。退出上下文管理器后,则恢复原来的默认graph。一般情况下,我们不用显式创建Graph,使用系统创建的那个默认Graph即可。print tf.get_default_graph()with tf.Graph().as_default() as g: print tf.get_default_graph() is g print tf.get_default_graph()print tf.get_default_graph()输出如下<tensorflow.python.framework.ops.Graph object at 0x106329fd0>True<tensorflow.python.framework.ops.Graph object at 0x18205cc0d0><tensorflow.python.framework.ops.Graph object at 0x10d025fd0>由此可见,在上下文管理器中,当前线程的默认图被替换了,而退出上下文管理后,则恢复为了原来的默认图。默认图管理默认graph和默认session一样,也是线程作用域的。当前线程中,永远都有且仅有一个graph为默认图。TensorFlow同样通过栈来管理线程的默认graph。@tf_export(“Graph”)class Graph(object): # 替换线程默认图 def as_default(self): return _default_graph_stack.get_controller(self) # 栈式管理,push pop @tf_contextlib.contextmanager def get_controller(self, default): try: context.context_stack.push(default.building_function, default.as_default) finally: context.context_stack.pop()替换默认图采用了堆栈的管理方式,通过push pop操作进行管理。获取默认图的操作如下,通过默认graph栈_default_graph_stack来获取。@tf_export(“get_default_graph”)def get_default_graph(): return _default_graph_stack.get_default()下面来看_default_graph_stack的创建_default_graph_stack = _DefaultGraphStack()class _DefaultGraphStack(_DefaultStack): def init(self): # 调用父类来创建 super(_DefaultGraphStack, self).init() self._global_default_graph = None class _DefaultStack(threading.local): def init(self): super(_DefaultStack, self).init() self._enforce_nesting = True # 和默认session栈一样,本质上也是一个list self.stack = []_default_graph_stack的创建如上所示,最终和默认session栈一样,本质上也是一个list。3 前端Graph数据结构Graph数据结构理解一个对象,先从它的数据结构开始。我们先来看Python前端中,Graph的数据结构。Graph主要的成员变量是Operation和Tensor。Operation是Graph的节点,它代表了运算算子。Tensor是Graph的边,它代表了运算数据。@tf_export(“Graph”)class Graph(object): def init(self): # 加线程锁,使得注册op时,不会有其他线程注册op到graph中,从而保证共享graph是线程安全的 self._lock = threading.Lock() # op相关数据。 # 为graph的每个op分配一个id,通过id可以快速索引到相关op。故创建了_nodes_by_id字典 self._nodes_by_id = dict() # GUARDED_BY(self._lock) self._next_id_counter = 0 # GUARDED_BY(self._lock) # 同时也可以通过name来快速索引op,故创建了_nodes_by_name字典 self._nodes_by_name = dict() # GUARDED_BY(self.lock) self.version = 0 # GUARDED_BY(self.lock) # tensor相关数据。 # 处理tensor的placeholder self.handle_feeders = {} # 处理tensor的read操作 self.handle_readers = {} # 处理tensor的move操作 self.handle_movers = {} # 处理tensor的delete操作 self.handle_deleters = {}下面看graph如何添加op的,以及保证线程安全的。 def add_op(self, op): # graph被设置为final后,就是只读的了,不能添加op了。 self.check_not_finalized() # 保证共享graph的线程安全 with self.lock: # 将op以id和name分别构建字典,添加到_nodes_by_id和_nodes_by_name字典中,方便后续快速索引 self.nodes_by_id[op.id] = op self.nodes_by_name[op.name] = op self.version = max(self.version, op.id)GraphKeys 图分组每个Operation节点都有一个特定的标签,从而实现节点的分类。相同标签的节点归为一类,放到同一个Collection中。标签是一个唯一的GraphKey,GraphKey被定义在类GraphKeys中,如下@tf_export(“GraphKeys”)class GraphKeys(object): GLOBAL_VARIABLES = “variables” QUEUE_RUNNERS = “queue_runners” SAVERS = “savers” WEIGHTS = “weights” BIASES = “biases” ACTIVATIONS = “activations” UPDATE_OPS = “update_ops” LOSSES = “losses” TRAIN_OP = “train_op” # 省略其他name_scope 节点命名空间使用name_scope对graph中的节点进行层次化管理,上下层之间通过斜杠分隔。# graph节点命名空间g = tf.get_default_graph()with g.name_scope(“scope1”): c = tf.constant(“hello, world”, name=“c”) print c.op.name with g.name_scope(“scope2”): c = tf.constant(“hello, world”, name=“c”) print c.op.name输出如下scope1/cscope1/scope2/c # 内层的scope会继承外层的,类似于栈,形成层次化管理4 后端Graph数据结构Graph先来看graph.h文件中的Graph类的定义,只看关键代码 class Graph { private: // 所有已知的op计算函数的注册表 FunctionLibraryDefinition ops; // GraphDef版本号 const std::unique_ptr<VersionDef> versions; // 节点node列表,通过id来访问 std::vector<Node*> nodes; // node个数 int64 num_nodes = 0; // 边edge列表,通过id来访问 std::vector<Edge*> edges; // graph中非空edge的数目 int num_edges = 0; // 已分配了内存,但还没使用的node和edge std::vector<Node*> free_nodes; std::vector<Edge*> free_edges; }后端中的Graph主要成员也是节点node和边edge。节点node为计算算子Operation,边为算子所需要的数据,或者代表节点间的依赖关系。这一点和Python中的定义相似。边Edge的持有它的源节点和目标节点的指针,从而将两个节点连接起来。下面看Edge类的定义。Edgeclass Edge { private: Edge() {} friend class EdgeSetTest; friend class Graph; // 源节点, 边的数据就来源于源节点的计算。源节点是边的生产者 Node* src; // 目标节点,边的数据提供给目标节点进行计算。目标节点是边的消费者 Node* dst; // 边id,也就是边的标识符 int id; // 表示当前边为源节点的第src_output_条边。源节点可能会有多条输出边 int src_output; // 表示当前边为目标节点的第dst_input_条边。目标节点可能会有多条输入边。 int dst_input;};Edge既可以承载tensor数据,提供给节点Operation进行运算,也可以用来表示节点之间有依赖关系。对于表示节点依赖的边,其src_output, dst_input_均为-1,此时边不承载任何数据。下面来看Node类的定义。Nodeclass Node { public: // NodeDef,节点算子Operation的信息,比如op分配到哪个设备上了,op的名字等,运行时有可能变化。 const NodeDef& def() const; // OpDef, 节点算子Operation的元数据,不会变的。比如Operation的入参列表,出参列表等 const OpDef& op_def() const; private: // 输入边,传递数据给节点。可能有多条 EdgeSet in_edges; // 输出边,节点计算后得到的数据。可能有多条 EdgeSet out_edges;}节点Node中包含的主要数据有输入边和输出边的集合,从而能够由Node找到跟他关联的所有边。Node中还包含NodeDef和OpDef两个成员。NodeDef表示节点算子的信息,运行时可能会变,创建Node时会new一个NodeDef对象。OpDef表示节点算子的元信息,运行时不会变,创建Node时不需要new OpDef,只需要从OpDef仓库中取出即可。因为元信息是确定的,比如Operation的入参个数等。由Node和Edge,即可以组成图Graph,通过任何节点和任何边,都可以遍历完整图。Graph执行计算时,按照拓扑结构,依次执行每个Node的op计算,最终即可得到输出结果。入度为0的节点,也就是依赖数据已经准备好的节点,可以并发执行,从而提高运行效率。系统中存在默认的Graph,初始化Graph时,会添加一个Source节点和Sink节点。Source表示Graph的起始节点,Sink为终止节点。Source的id为0,Sink的id为1,其他节点id均大于1.5 Graph运行时生命周期Graph是TensorFlow的核心对象,TensorFlow的运行均是围绕Graph进行的。运行时Graph大致经过了以下阶段图构建:client端用户将创建的节点注册到Graph中,一般不需要显示创建Graph,使用系统创建的默认的即可。图发送:client通过session.run()执行运行时,将构建好的整图序列化为GraphDef后,传递给master图剪枝:master先反序列化拿到Graph,然后根据session.run()传递的fetches和feeds列表,反向遍历全图full graph,实施剪枝,得到最小依赖子图。图分裂:master将最小子图分裂为多个Graph Partition,并注册到多个worker上。一个worker对应一个Graph Partition。图二次分裂:worker根据当前可用硬件资源,如CPU GPU,将Graph Partition按照op算子设备约束规范(例如tf.device(’/cpu:0’),二次分裂到不同设备上。每个计算设备对应一个Graph Partition。图运行:对于每一个计算设备,worker依照op在kernel中的实现,完成op的运算。设备间数据通信可以使用send/recv节点,而worker间通信,则使用GRPC或RDMA协议。这些阶段根据TensorFlow运行时的不同,会进行不同的处理。运行时有两种,本地运行时和分布式运行时。故Graph生命周期到后面分析本地运行时和分布式运行时的时候,再详细讲解。本文作者:扬易阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 22, 2019 · 2 min · jiezi

Java多线程001——一图读懂线程与进程

本博客 猫叔的博客,转载请申明出处前言本系列将由浅入深,学习Java并发多线程。一图读懂线程与进程1、一个进程可以包含一个或多个线程。(其实你经常听到“多线程”,没有听过“多进程”嘛)2、进程存在堆和方法区3、线程存在程序计数器和栈4、堆占最大内存,其为创建时分配的,是多线程共享的,主要存放new创建的对象5、方法区也是多线程共享的,主要存放类、常量、静态变量6、CPU的基本执行单位是线程(注意!不是进程)7、由此,线程需要一个程序计数器记录当前线程要执行的指令地址8、当CPU的时间片用完,让出后记录当前执行地址,下次继续执行(时间片轮询)9、只有执行Java代码时pc技数器记录的才是下一条指令的地址,执行native方法,则记录的是undefined地址10、线程中的栈,只要存储线程局部变量、调用栈帧栈帧:C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

February 19, 2019 · 1 min · jiezi

Java的Interrupt与线程中断

中断状态每一个线程都有一个boolean属性,表示中断状态,初始值为false。中断线程:Thread.interrupt()正常情况下,只是将线程的中断状态变为true。线程中可以通过轮询中断状态,做出相应的处理。如果线程在阻塞状态下,线程将退出阻塞且中断状态将被清除(即为false),且会抛出InterruptException。(IO操作忽略)查询中断状态(1)isInterrupted(),返回当前的中断状态,不会改变中断状态。(2)static interrupted(),返回当前中断状态,且会清除中断状态。(即第二次调用将返回 false)处理InterruptedException(1)继续抛出。如果抛出InterruptedException意味着是一个阻塞方法,那么调用一个阻塞方法则意味着调用者也是一个阻塞方法,应该有某种策略来处理InterruptedException。(2)捕获InterruptedException,执行清理,再重新抛出InterruptedException。(3)再次调用interrupt()。当由Runnable定义的任务调用一个可中断的方法时,在这种情况下,不能重新抛出InterruptedException,因为Runnable接口的run方法不允许抛出异常。当一个阻塞方法检测到中断并抛出InterruptedException但是不能重新抛出它,那么应该保留中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断做出响应,该任务可以通过调用interrupt()以重新中断当前线程来完成。public void run() { try { while (true) { Task task = queue.take(10, TimeUnit.SECONDS); task.execute(); } } catch (InterruptedException e) { // Restore the interrupted status Thread.currentThread().interrupt(); } }中断一个不支持中断的类如一个IO类会阻塞线程,但不支持中断。则可以写一个新IO类,继承Thread类,重写interrupt方法,在interrupt中关闭IO,最后调用super.interrupt();待决中断在进入阻塞前被中断,被称为待决中断。在调用阻塞方法时,会立刻抛出InterruptException。参考文章:多线程-interrupt(),isInterrupted(),interrupted() - 小路不懂2

February 18, 2019 · 1 min · jiezi

MySQL主从同步机制和同步延时问题追查

今天遇到一个问题,Mysql持续报错,主从同步延时数过大或错误。所以这篇文章给大家分享下主从同步的机制原理以及问题排查思路。故障表现最直观的表现为:mysql> show slave status\G; // 状态一 Seconds_Behind_Master: NULL // 状态二 Seconds_Behind_Master: 0 // 状态三 Seconds_Behind_Master: 79连续查询,大部分时间该属性值=0,偶发性出现Null或者79等延时值。导致观察主从同步延时的监控持续报警。故障原因及解决方案多台备机的server-id一致,导致主机无法长时间同某一台备机连接,进而无法正常同步。修改server-id后,重启数据库恢复。主从同步机制MySQL的主从同步,又称为复制(replication),是一种内置的高可用高性能集群解决方案,主要功能有:数据分布:同步不需要很大带宽,可以实现多数据中心复制数据。读取的负载均衡:通过服务器集群,可以通过DNS轮询、Linux LVS等GSLB(全局负载均衡)方式,降低主服务器的读压力。数据库备份:复制是备份的一部分,但并不能代替备份。还需要与快照相结合。高可用性和故障转移:从服务器可以快速切换为主服务器,减少故障的停机时间和恢复时间。主从同步分为3步:主服务器(master)把数据更改记录到二进制日志(binlog)中。从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。从服务器重做中继日志中的日志,把更改应用到自己的数据库上,达到数据的一致性。主从同步是一个异步实时的同步,会实时的传输,但存在执行上的延时,如果主服务器压力很大,延时也会相应扩大。通过上面的图,可以看到一共需要3个线程:主服务器的日志传送线程:负责将二进制日志增量传送到备机从服务器的I/O线程:负责读取主服务器的二进制日志,并保存为中继日志从服务器的SQL线程,负责执行中继日志查看MySQL线程我们可以使用show full processlist;命令来查看MySQL的状态:主机的状态:备机的状态:可以看到,我的集群架构为1台主机、4台备机,所以在主机中有4个同步线程(已经发送所有的binlog数据到备机,等待binlog日志更新),1个查看命令线程(show full processlist)。在备机中有1个查看命令线程,1个I/O线程(等待主机发送同步数据事件),1个SQL线程(已经读取了所有中继日志,等待I/O线程来更新它)。查看同步状态因为主从同步是异步实时的,也就是会存在延时的情况,我们可以通过show slave status;来查看备机上的同步延时:在主从同步中我们需要关注的一些属性,已经给大家标红了:Slave_IO_State: 当前I/O线程的状态Master_Log_File: 当前同步的主服务器的二进制文件Read_Master_Log_Pos: 当前同步的主服务器的二进制文件的偏移量,单位为字节,如图中为已经同步了12.9M(13630580/1024/1024)的内容Relay_Master_Log_File: 当前中继日志同步的二进制文件Slave_IO_Running: 从服务器中I/O线程的运行状态,YES为运行正常Slave_SQL_Running: 从服务器中SQL线程的运行状态,YES为运行正常Exec_Master_Log_Pos: 表示同步完成的主服务器的二进制日志偏移量Seconds_Behind_Master: 表示从服务器数据比主服务器落后的持续时长同样可以通过show master status;命令来查看主服务器的运行状态:正常运行的主从同步状态:Slave_IO_Running: YESSlave_SQL_Running: YESSeconds_Behind_Master: 0问题排查在理解了主从同步的机制后,再来看今天遇到的问题,通过查看备机状态,我们观察在三种状态下的几个关键属性值:mysql> show slave status\G;#状态一: Slave_IO_State: Reconnecting after a failed master event read Slave_IO_Running: No Slave_SQL_Running: Yes Seconds_Behind_Master: NULL#状态二: Slave_IO_State: Waiting for master to send event Slave_IO_Running: Yes Slave_SQL_Running: Yes Seconds_Behind_Master: 0#状态三: Slave_IO_State: Queueing master event to the relay log Slave_IO_Running: Yes Slave_SQL_Running: Yes Seconds_Behind_Master: 636通过MySQL主从复制线程状态转变,我们可以看到三种状态的不同含义:# 状态一# 线程正尝试重新连接主服务器,当连接重新建立后,状态变为Waiting for master to send event。Reconnecting after a failed master event read# 状态二# 线程已经连接上主服务器,正等待二进制日志事件到达。如果主服务器正空闲,会持续较长的时间。如果等待持续slave_read_timeout秒,则发生超时。此时,线程认为连接被中断并企图重新连接。Waiting for master to send event# 状态三# 线程已经读取一个事件,正将它复制到中继日志供SQL线程来处理。Queueing master event to the relay log在这里,我们可以猜测,由于某些原因,从服务器不断的和主服务器进行断开并尝试重连,重连成功后又再次断开。我们再看看主机的运行情况:发现问题出在10.144.63.*和10.144.68.*两台机器上,我们查看其中一台的错误日志:190214 11:33:20 [Note] Slave: received end packet from server, apparent master shutdown: 190214 11:33:20 [Note] Slave I/O thread: Failed reading log event, reconnecting to retry, log ‘mysql-bin.005682’ at postion 13628070拿到关键字Slave: received end packet from server, apparent master shutdown: Google搜索一下,在文章Confusing MySQL Replication Error Message中可以看到原因为两台备机的server-id重复。One day it happen to me, and took me almost an hour to find that out.Moving foward I always use a base my.cnf to I copy to any other server and the first thing is to increase the server-id.Could MySQL just use the servername intead of a numeric value?问题修复定位了问题,我们确认下是否重复,发现两台备机的该字段确实相同:vim my.cnf#replicationlog-bin=mysql-bin# 这个随机数字相同导致的server-id=177230069sync_binlog=1更改一个其他不同的数字,保存,重启MySQL进程,报警恢复。总结最终来看,这个问题的解决非常简单,但从刚开始的迷茫到最后的思路清晰,都是我们排查问题所常见的,这篇文章的主要收获是让你明白主从同步的机制和追查问题的思路,希望下次我们都能很快的解决主从同步带给我们的问题。参考资料《MySQL基础内幕 InnoDB存储引擎 第2版》P8.7 复制MySQL主从复制线程状态转变: http://www.ywnds.com/?p=3821Confusing MySQL Replication Error Message: https://www.percona.com/blog/… ...

February 14, 2019 · 1 min · jiezi

在Java中怎样实现多线程?Java线程的四种状态

一、在java中怎样实现多线程?extends Threadimplement Runnable方法一:继承 Thread 类,覆盖方法 run(),我们在创建的 Thread 类的子类中重写 run() ,加入线程所要执行的代码即可。下面是一个例子:public class MyThread extends Thread { int count= 1, number; public MyThread(int num) { number = num; System.out.println (“创建线程 " + number); } public void run() { while(true) { System.out.println (“线程 " + number + “:计数 " + count); if(++count== 6) return; } } public static void main(String args[]) { for(int i = 0;i 〈 5; i++) new MyThread(i+1).start(); } }*上海尚学堂Java培训 shsxt.com 这种方法简单明了,符合大家的习惯,但是,它也有一个很大的缺点,那就是如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类,这时如果我们又不想建立一个新的类,应该怎么办呢? 我们不妨来探索一种新的方法:我们不创建Thread类的子类,而是直接使用它,那么我们只能将我们的方法作为参数传递给 Thread 类的实例,有点类似回调函数。但是 Java 没有指针,我们只能传递一个包含这个方法的类的实例。 那么如何限制这个类必须包含这一方法呢?当然是使用接口!(虽然抽象类也可满足,但是需要继承,而我们之所以要采用这种新方法,不就是为了避免继承带来的限制吗?) Java 提供了接口 java.lang.Runnable 来支持这种方法。方法二:实现 Runnable 接口 Runnable接口只有一个方法run(),我们声明自己的类实现Runnable接口并提供这一方法,将我们的线程代码写入其中,就完成了这一部分的任务。但是Runnable接口并没有任何对线程的支持,我们还必须创建Thread类的实例,这一点通过Thread类的构造函数 public Thread(Runnable target);来实现。下面是一个例子:public class MyThread implements Runnable { int count= 1, number; public MyThread(int num) { number = num; System.out.println(“创建线程 " + number); } public void run() { while(true) { System.out.println (“线程 " + number + “:计数 " + count); if(++count== 6) return; } } public static void main(String args[]) { for(int i = 0; i 〈 5;i++) new Thread(new MyThread(i+1)).start(); } }**上海尚学堂Java培训 shsxt.com 严格地说,创建Thread子类的实例也是可行的,但是必须注意的是,该子类必须没有覆盖 Thread 类的 run 方法,否则该线程执行的将是子类的 run 方法,而不是我们用以实现Runnable 接口的类的 run 方法,对此大家不妨试验一下。 使用 Runnable 接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装,它的缺点在于,我们只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承 Thread 来得紧凑。 综上所述,两种方法各有千秋,大家可以灵活运用。 下面让我们一起来研究一下多线程使用中的一些问题。 二、线程的四种状态 1. 新状态:线程已被创建但尚未执行(start() 尚未被调用)。 2. 可执行状态:线程可以执行,虽然不一定正在执行。CPU 时间随时可能被分配给该线程,从而使得它执行。 3. 死亡状态:正常情况下 run() 返回使得线程死亡。调用 stop()或 destroy() 亦有同样效果,但是不被推荐,前者会产生异常,后者是强制终止,不会释放锁。 4. 阻塞状态:线程不会被分配 CPU 时间,无法执行。 三、线程的优先级 线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得 CPU 时间时,线程调度系统根据各个线程的优先级来决定给谁分配 CPU 时间,优先级高的线程有更大的机会获得 CPU 时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。关于Java线程就介绍到这,更多Java学习资料教程请点 上海尚学堂java学习视频 ...

January 24, 2019 · 1 min · jiezi

这一次,真正明白进程与线程

引言作为软件工程师,进程与线程应该是我们必备的知识了,从年年各大企业的面试题就能看出来!必考题:进程与线程的区别小生本学期学习了操作系统这门课,最大的收获就是学会了这道“必考题”。最开始觉得自己学明白了,自己写本文的时候,才觉得这里牵扯到好多东西。操作系统什么是操作系统?说到操作系统,大家一定都不会陌生,我们用的手机内置了Android/iOS操作系统,我的开发用的电脑,Mac OS、Linux、Windows等操作系统。操作系统的非官方描述:操作系统是管理计算机硬件与软件资源的系统软件。发展史想真正明白进程与线程,我们需要了解一下计算机操作系统的历史。手工操作阶段最原始的时候,计算机没有操作系统,只有单独的机器,像下面这样:以一个现代人的眼光来看:没有操作系统,这个机器怎么用啊?反正给我一堆硬件,我是不能让这些硬件跑起来。前人们也是非常厉害的。他们使用记录有程序和数据的卡片(前期用卡片,后来逐渐发展为使用打孔纸带)去操作机器。程序读入机器后,机器就开始工作直到程序停止。据说图灵能非常熟练地用这种方法操作Manchester Mark I机器。缺点用户独占全机CPU速度快,需等待人工操作,资源利用率低批处理操作系统为了解决人工操作速度远慢于CPU的问题,引入了脱机输入/输出系统。计算机能够自动地、成批地处理一个或多个用户的作业,这就不需要手工输入了,这就快了。单道批处理操作系统每次只加载一道作业到内存中执行。多道批处理操作系统每次加载多道作业到内存中并发执行,各个作业轮流使用处理机和其他系统资源,最终依次完成。在内存中同时存放多道相互独立的程序,这些程序共享系统资源,在操作系统的控制下交替在CPU上执行。以后发展的操作系统,我们就不需要知道了,了解到这就足够了。进程与线程程序并发执行程序并发执行时,多道程序共享系统资源,此时,程序的运行失去了封闭性。程序在并发执行时,是多个程序共享系统中的各种资源,因而这些资源的状态将由多个程序来改变,致使程序的运行失去了封闭性。这样,某程序在执行时,必然会受到其它程序的影响。当多个程序并发执行时,我们期望的是运行变快,增加系统吞吐量。但是,就如该图一样,某些程序必须在某些程序执行之后才可继续执行,不能让这个程序想执行就执行,我们要对其进行控制。为了更方便地进行控制,我们就引入了进程的概念。进程在早期,进程是资源分配与调度的基本单位。引入进程,我们发现解决了上述的问题,可以对I1、I2等等都建立进程,并按我们想法的前趋图对进程执行进行约束,达到我们预期的效果。然后程序对我们的进行进行调度,以达到并发执行的效果。然后怎么控制就是PV操作,信号量,进程同步与互斥,这就不属于本文的范围了,大家可以自行学习。进程不足又过了好多年,人们发现最初的进程设计得有些缺点。原始进程两特点:资源分配的基本单位。调度的基本单位。所以,在仅有进程的系统中,想实现并发,需要进行下列操作:创建进程:创建进程PCB,并为它分配资源。撤销进程:回收系统资源,撤销PCB。进程切换:从一个进程环境切换到另一个进程环境。还是这个图为例,我需要创建12个进程,然后进行进程控制,让它们并发执行。创建进程12次,分配12次资源,分配资源消耗了大量的资源。因为进程间的资源相互独立,所以进程间切换是资源的切换,开销相当大。程序结束时,撤销12次资源,回收资源时也有很大的开销。我们有没有一种方法,可以减少创建、撤销以及切换的开销呢?然后,引入了线程的概念。线程进程是携带资源的,切换时开销太大,所以我们需要将资源分配与调度分开来。在现代操作系统中,进程是资源分配的基本单位,线程是调度的基本单位。一个进程中有多个线程,多个线程共享该进程的资源,CPU直接调度进程。还是这张图,如果我们创建1个进程,12个线程会怎么样?程序运行开始,创建一个进程,分配资源1次。创建12个线程,注意,这里是在进程内创建线程,所以,线程无需分配资源,直接使用其进程的资源即可,这开销就非常少了。12个线程并发执行,这12个线程是共享一块系统资源,所以在线程切换时,是不需要切换资源的,开销大大减少。撤销时呢?撤销线程12次,撤销进程,回收资源1次。总结所以,这就是进程与线程的发展,了解了这些历史,我想那些答案再也不用背了吧?CPU的一个核心只能执行一个线程,多个核心可以并发执行,但线程并行数最多不超过核心数。4核的,在不添加新技术的情况下,最多并行4个线程。嗯?Core i7不是4核8线程吗?为什么英特尔的4个核心能跑8个线程?这就是英特尔大名鼎鼎的超线程技术,它将处理机核心中未利用的资源利用起来,再模拟出一个核心,虽然是4核心的处理机,所以在用户看来,该CPU有8个逻辑核心。在桌面端很有用,但是到了服务器端,超线程技术会有些问题。这只是我看了很多篇关于服务器端批判超线程的运维博客之后的看法,待日后,我们再对该技术进行讨论。

January 22, 2019 · 1 min · jiezi

JUC包中的分而治之策略-为提高性能而生

一、前言本次分享我们来共同探讨JUC包中一些有意思的类,包含AtomicLong & LongAdder,ThreadLocalRandom原理。二、AtomicLong & LongAdder2.1 AtomicLong 类AtomicLong是JUC包提供的原子性操作类,其内部通过CAS保证了对计数的原子性更新操作。大家可以翻看源码发现内部是通过UnSafe(rt.jar)这个类的CAs操作来保证对内部的计数器变量 long value进行原子性更新的,比如JDK8中: public final long incrementAndGet() { return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L; }其中unsafe.getAndAddLong的代码如下: public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2) { long l; do { l = getLongVolatile(paramObject, paramLong1);//(1) } while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));//(2) return l; }可知最终调用的是native 方法compareAndSwapLong原子性操作。当多个线程调用同一个AtomicLong实例的incrementAndGet方法后,多个线程都会执行到unsafe.getAndAddLong方法,然后多个线程都会执行到代码(1)处获取计数器的值,然后都会去执行代码(2),如果多个线程同时执行了代码(2),由于CAS具有原子性,所以只有一个线程会更新成功,然后返回true从而退出循环,整个更新操作就OK了。其他线程则CAS失败返回false,则循环一次在次从(1)处获取当前计数器的值,然后在尝试执行(2),这叫做CAS的自旋操作,本质是使用Cpu 资源换取使用锁带来的上下文切换等开销。2.2 LongAdder类AtomicLong类为开发人员使用线程安全的计数器提供了方便,但是AtomicLong在高并发下存在一些问题,如上所述,当大量线程调用同一个AtomicLong的实例的方法时候,同时只有一个线程会CAS计数器的值成功,失败的线程则会原地占用cpu进行自旋转重试,这回造成大量线程白白浪费cpu原地自旋转。在JDK8中新增了一个LongAdder类,其采用分而治之的策略来减少同一个变量的并发竞争度,LongAdder的核心思想是把一个原子变量分解为多个变量,让同样多的线程去竞争多个资源,这样竞争每个资源的线程数就被分担了下来,下面通过图形来理解下两者设计的不同之处:如上图AtomicLong是多个线程同时竞争同一个原子变量。如上图LongAdder内部维护多个Cell变量,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这是变相的减少了争夺共享资源的并发量。下面我们首先看下Cell的结构: @sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } final boolean cas(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); } // Unsafe 机制 private static final sun.misc.Unsafe UNSAFE; private static final long valueOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> ak = Cell.class; valueOffset = UNSAFE.objectFieldOffset (ak.getDeclaredField(“value”)); } catch (Exception e) { throw new Error(e); } } }LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base,由于Cells占用内存是相对比较大的,所以一开始并不创建,而是在需要时候在创建,也就是惰性 创建。当一开始判断cell数组是null并且并发线程较少时候所有的累加操作都是对base变量进行的,这时候就退化为了AtomicLong。cell数组的大小保持是2的N次方大小,初始化时候Cell数组的中Cell的元素个数为2,数组里面的变量实体是Cell类型。当多个线程在争夺同一个Cell原子变量时候如果失败并不是在当前cell变量上一直自旋CAS重试,而是会尝试在其它Cell的变量上进行CAS尝试,这个改变增加了当前线程重试时候CAS成功的可能性。最后获取LongAdder当前值的时候是把所有Cell变量的value值累加后在加上base返回的,如下代码: public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }如上代码可知首先把base的值赋值给sum变量,然后通过循环把每个cell元素的value值累加到sum变量上,最后返回sum.其实这是一种分而治之的策略,先把并发量分担到多个原子变量上,让多个线程并发的对不同的原子变量进行操作,然后获取计数时候在把所有原子变量的计数和累加。思考问题:何时初始化cell数组当前线程如何选择cell中的元素进行访问如果保证cell中元素更新的线程安全cell数组何时进行扩容,cell元素个数可以无限扩张?性能对比,这里有一个文章 http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/三、 Random & ThreadLocalRandom3.1 Random类原理及其局限性在JDK7之前包括现在java.util.Random应该是使用比较广泛的随机数生成工具类,下面先通过简单的代码看看java.util.Random是如何使用的:public class RandomTest { public static void main(String[] args) { //(1)创建一个默认种子的随机数生成器 Random random = new Random(); //(2)输出10个在0-5(包含0,不包含5)之间的随机数 for (int i = 0; i < 10; ++i) { System.out.println(random.nextInt(5)); } }}代码(1)创建一个默认随机数生成器,使用默认的种子。代码(2)输出输出10个在0-5(包含0,不包含5)之间的随机数。 public int nextInt(int bound) { //(3)参数检查 if (bound <= 0) throw new IllegalArgumentException(BadBound); //(4)根据老的种子生成新的种子 int r = next(31); //(5)根据新的种子计算随机数 … return r; } 如上代码可知新的随机数的生成需要两个步骤首先需要根据老的种子计算生成新的种子。然后根据新的种子和bound变量通过一定的算法来计算新的随机数。下面看下next()代码: protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { //(6)获取当前原子变量种子的值 oldseed = seed.get(); //(7)根据当前种子值计算新的种子 nextseed = (oldseed * multiplier + addend) & mask; //(8)使用新种子替换老的种子 } while (!seed.compareAndSet(oldseed, nextseed)); //(9) return (int)(nextseed >>> (48 - bits)); }代码(6)使用原子变量的get方法获取当前原子变量种子的值代码(7)根据具体的算法使用当前种子值计算新的种子代码(8)使用CAS操作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6)那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环从新获取更新后的种子作为当前种子去计算老的种子,这就保证了随机数的随机性。代码(9)则使用固定算法根据新的种子计算随机数,并返回。3.2 ThreadLocalRandomRandom类生成随机数原理以及不足:每个Random实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。多线程下使用单个Random实例生成随机数时候,多个线程同时计算随机数计算新的种子时候多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,那么CAS操作失败的大量线程进行自旋重试,而大量线程的自旋重试是会降低并发性能和消耗CPU资源的,为了解决这个问题,ThreadLocalRandom类应运而生。public class RandomTest { public static void main(String[] args) { //(10)获取一个随机数生成器 ThreadLocalRandom random = ThreadLocalRandom.current(); //(11)输出10个在0-5(包含0,不包含5)之间的随机数 for (int i = 0; i < 10; ++i) { System.out.println(random.nextInt(5)); } }}如上代码(10)调用ThreadLocalRandom.current()来获取当前线程的随机数生成器。下面来分析下ThreadLocalRandom的实现原理。从名字看会让我们联想到ThreadLocal类。ThreadLocal通过让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个原理。Random的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,这个原理可以通过下面图来表达:那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己本地内存中的老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能,如下图ThreadLocalRandom原理可以使用下图表达:Thread类里面有几个变量: /** The current seed for a ThreadLocalRandom / @sun.misc.Contended(“tlr”) long threadLocalRandomSeed; /* Probe hash value; nonzero if threadLocalRandomSeed initialized */ @sun.misc.Contended(“tlr”) int threadLocalRandomProbe;思考问题:每个线程的初始种子怎么生成的如果保障多个线程产生的种子不一样四、总结本文是对拙作 java并发编程之美 一书中有关章节的提炼。本次分享首先讲解了AtomicLong的内部实现,以及存在的缺点,然后讲解了 LongAdder采用分而治之的策略通过使用多个原子变量减小单个原子变量竞争的并发度。然后简单介绍了Random,和其缺点,最后介绍了ThreadLocalRandom借用ThreadLocal的思想解决了多线程对同一个原子变量竞争锁带来的性能损耗。其实JUC包中还有其他一些经典的组件,比如fork-join框架等。本文作者:加多阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 15, 2019 · 2 min · jiezi

MongoDB 如何使用内存?为什么内存满了?

最近接到多个MongoDB内存方面的线上case及社区问题咨询,主要集中在:为什么我的 MongoDB 使用了 XX GB 内存?一个机器上部署多个 Mongod 实例/进程,WiredTiger cache 应该如何配置?MongoDB 是否应该使用 SWAP 空间来降低内存压力?MongoDB 内存用在哪?Mongod 进程启动后,除了跟普通进程一样,加载 binary、依赖的各种library 到内存,其作为一个DBMS,还需要负责客户端连接管理,请求处理,数据库元数据、存储引擎等很多工作,这些工作都涉及内存的分配与释放,默认情况下,MongoDB 使用 Google tcmalloc 作为内存分配器,内存占用的大头主要是「存储引擎」与 「客户端连接及请求的处理」。存储引擎 CacheMongoDB 3.2 及以后,默认使用 WiredTiger 存储引擎,可通过 cacheSizeGB 选项配置 WiredTiger 引擎使用内存的上限,一般建议配置在系统可用内存的60%左右(默认配置)。举个例子,如果 cacheSizeGB 配置为 10GB,可以认为 WiredTiger 引擎通过tcmalloc分配的内存总量不会超过10GB。为了控制内存的使用,WiredTiger 在内存使用接近一定阈值就会开始做淘汰,避免内存使用满了阻塞用户请求。目前有4个可配置的参数来支持 wiredtiger 存储引擎的 eviction 策略(一般不需要修改),其含义是:参数默认值含义eviction_target80当 cache used 超过 eviction_target,后台evict线程开始淘汰 CLEAN PAGEeviction_trigger95当 cache used 超过 eviction_trigger,用户线程也开始淘汰 CLEAN PAGEeviction_dirty_target5当 cache dirty 超过 eviction_dirty_target,后台evict线程开始淘汰 DIRTY PAGEeviction_dirty_trigger20当 cache dirty 超过 eviction_dirty_trigger, 用户线程也开始淘汰 DIRTY PAGE在这个规则下,一个正常运行的 MongoDB 实例,cache used 一般会在 0.8 * cacheSizeGB 及以下,偶尔超出问题不大;如果出现 used>=95% 或者 dirty>=20%,并一直持续,说明内存淘汰压力很大,用户的请求线程会阻塞参与page淘汰,请求延时就会增加,这时可以考虑「扩大内存」或者 「换更快的磁盘提升IO能力」。TCP 连接及请求处理MongoDB Driver 会跟 mongod 进程建立 tcp 连接,并在连接上发送数据库请求,接受应答,tcp 协议栈除了为连接维护socket元数据为,每个连接会有一个read buffer及write buffer,用户收发网络包,buffer的大小通过如下sysctl系统参数配置,分别是buffer的最小值、默认值以及最大值,详细解读可以google。net.ipv4.tcp_wmem = 8192 65536 16777216net.ipv4.tcp_rmem = 8192 87380 16777216redhat7(redhat6上并没有导出这么详细的信息) 上通过 ss -m 可以查看每个连接的buffer的信息,如下是一个示例,读写 buffer 分别占了 2357478bytes、2626560bytes,即均在2MB左右;500个类似的连接就会占用掉 1GB 的内存;buffer 占到多大,取决于连接上发送/应答的数据包的大小、网络质量等,如果请求应答包都很小,这个buffer也不会涨到很大;如果包比较大,这个buffer就更容易涨的很大。tcp ESTAB 0 0 127.0.0.1:51601 127.0.0.1:personal-agent skmem:(r0,rb2357478,t0,tb2626560,f0,w0,o0,bl0)除了协议栈上的内存开销,针对每个连接,Mongod 会起一个单独的线程,专门负责处理这条连接上的请求,mongod 为处理连接请求的线程配置了最大1MB的线程栈,通常实际使用在几十KB左右,通过 proc 文件系统看到这些线程栈的实际开销。 除了处理请求的线程,mongod 还有一系列的后台线程,比如主备同步、定期刷新 Journal、TTL、evict 等线程,默认每个线程最大ulimit -s(一般10MB)的线程栈,由于这批线程数量比较固定,占的内存也比较可控。# cat /proc/$pid/smaps7f563a6b2000-7f563b0b2000 rw-p 00000000 00:00 0Size: 10240 kBRss: 12 kBPss: 12 kBShared_Clean: 0 kBShared_Dirty: 0 kBPrivate_Clean: 0 kBPrivate_Dirty: 12 kBReferenced: 12 kBAnonymous: 12 kBAnonHugePages: 0 kBSwap: 0 kBKernelPageSize: 4 kBMMUPageSize: 4 kB线程在处理请求时,需要分配临时buffer存储接受到的数据包,为请求建立上下文(OperationContext),存储中间的处理结果(如排序、aggration等)以及最终的应答结果等。当有大量请求并发时,可能会观察到 mongod 使用内存上涨,等请求降下来后又慢慢释放的行为,这个主要是 tcmalloc 内存管理策略导致的,tcmalloc 为性能考虑,每个线程会有自己的 local free page cache,还有 central free page cache;内存申请时,按 local thread free page cache ==> central free page cache 查找可用内存,找不到可用内存时才会从堆上申请;当释放内存时,也会归还到 cache 里,tcmalloc 后台慢慢再归还给 OS, 默认情况下,tcmalloc 最多会 cache min(1GB,1/8 * system_memory) 的内存, 通过 setParameter.tcmallocMaxTotalThreadCacheBytesParameter 参数可以配置这个值,不过一般不建议修改,尽量在访问层面做调优)tcmalloc cache的管理策略,MongoDB 层暴露了几个参数来调整,一般不需要调整,如果能清楚的理解tcmalloc原理及参数含义,可做针对性的调优;MongoDB tcmalloc 的内存状态可以通过 db.serverStatus().tcmalloc 查看,具体含义可以看 tcmalloc 的文档。重点可以关注下 total_free_bytes ,这个值告诉你有多少内存是 tcmalloc 自己缓存着,没有归还给 OS 的。mymongo:PRIMARY&gt; db.serverStatus().tcmalloc{ “generic” : { “current_allocated_bytes” : NumberLong(“2545084352”), “heap_size” : NumberLong(“2687029248”) }, “tcmalloc” : { “pageheap_free_bytes” : 34529280, “pageheap_unmapped_bytes” : 21135360, “max_total_thread_cache_bytes” : NumberLong(1073741824), “current_total_thread_cache_bytes” : 1057800, “total_free_bytes” : 86280256, “central_cache_free_bytes” : 84363448, “transfer_cache_free_bytes” : 859008, “thread_cache_free_bytes” : 1057800, “aggressive_memory_decommit” : 0, … }}如何控制内存使用?合理配置 WiredTiger cacheSizeGB如果一个机器上只部署 Mongod,mongod 可以使用所有可用内存,则是用默认配置即可。如果机器上多个mongod混部,或者mongod跟其他的一些进程一起部署,则需要根据分给mongod的内存配额来配置 cacheSizeGB,按配额的60%左右配置即可。控制并发连接数TCP连接对 mongod 的内存开销上面已经详细分析了,很多同学对并发有一定误解,认为「并发连接数越高,数据库的QPS就越高」,实际上在大部分数据库的网络模型里,连接数过高都会使得后端内存压力变大、上下文切换开销变大,从而导致性能下降。MongoDB driver 在连接 mongod 时,会维护一个连接池(通常默认100),当有大量的客户端同时访问同一个mongod时,就需要考虑减小每个客户端连接池的大小。mongod 可以通过配置 net.maxIncomingConnections 配置项来限制最大的并发连接数量,防止数据库压力过载。是否应该配置 SWAP官方文档上的建议如下,意思是配置一下swap,避免mongod因为内存使用太多而OOM。For the WiredTiger storage engine, given sufficient memory pressure, WiredTiger may store data in swap space.Assign swap space for your systems. Allocating swap space can avoid issues with memory contention and can prevent the OOM Killer on Linux systems from killing mongod. 开启 SWAP 与否各有优劣,SWAP开启,在内存压力大的时候,会利用SWAP磁盘空间来缓解内存压力,此时整个数据库服务会变慢,但具体变慢到什么程度是不可控的。不开启SWAP,当整体内存超过机器内存上线时就会触发OOM killer把进程干掉,实际上是在告诉你,可能需要扩展一下内存资源或是优化对数据库的访问了。是否开启SWAP,实际上是在「好死」与「赖活着」的选择,个人觉得,对于一些重要的业务场景来说,首先应该为数据库规划足够的内存,当内存不足时,「及时调整扩容」比「不可控的慢」更好。其他尽量减少内存排序的场景,内存排序一般需要更多的临时内存主备节点配置差距不要过大,备节点会维护一个buffer(默认最大256MB)用于存储拉取到oplog,后台从buffer里取oplog不断重放,当备同步慢的时候,这个buffer会持续使用最大内存。控制集合及索引的数量,减少databse管理元数据的内存开销;集合、索引太多,元数据内存开销是一方面的影响,更多的会影响启动加载的效率、以及运行时的性能。本文作者:张友东阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 10, 2019 · 2 min · jiezi

性能诊断利器 JProfiler 快速入门和最佳实践

背景性能诊断是软件工程师在日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。Java 作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成 Java 应用出现性能问题的因素非常多,例如线程控制、磁盘读写、数据库访问、网络I/O、垃圾收集等。想要了定位这些问题,一款优秀的性能诊断工具必不可少。本文将介绍 Java 性能诊断过程中的常用工具,并重点介绍其中的优秀代表 JProfiler 的基本原理和最佳实践(本文所做的调研基于jprofiler10.1.4)。Java 性能诊断工具简介在 Java 的世界里,有许多诊断工具可供选择,既包括像 jmap、jstat 这样的简单命令行工具,又包括 JVisualvm、JProfiler 等图形化综合诊断工具,同时还有 SkyWalking、ARMS 这样的针对分布式应用的性能监控系统。下面分别对其进行介绍。简单命令行工具JDK 内置了许多命令行工具,它们可用来获取目标 JVM 不同方面、不同层次的信息。jinfo - 用于实时查看和调整目标 JVM 的各项参数。jstack - 用于获取目标 Java 进程内的线程堆栈信息,可用来检测死锁、定位死循环等。jmap - 用于获取目标 Java 进程的内存相关信息,包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等。jstat - 一款轻量级多功能监控工具,可用于获取目标 Java 进程的类加载、JIT 编译、垃圾收集、内存使用等信息。jcmd - 相比 jstat 功能更为全面的工具,可用于获取目标 Java 进程的性能统计、JFR、内存使用、垃圾收集、线程堆栈、JVM 运行时间等信息。图形化综合诊断工具使用上述命令行工具或组合能帮您获取目标 Java 应用性能相关的基础信息,但它们存在下列局限:无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便。分析数据通过终端输出,结果展示不够直观。下面介绍几款图形化的综合性能诊断工具。JVisualvmJVisualvm 是 JDK 内置的可视化性能诊断工具,它通过 JMX、jstatd、Attach API 等方式获取目标 JVM 的分析数据,包括 CPU 使用率、内存使用量、线程堆栈信息等。此外,它还能直观地展示 Java 堆中各对象的数量和大小、各 Java 方法的调用次数和执行时间等。JProfilerJProfiler 是由 ej-technologies 公司开发的一款 Java 应用性能诊断工具。它聚焦于四个重要主题上。方法调用 - 对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法。内存分配 - 通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄漏问题,优化内存使用。线程和锁 - JProfiler 提供多种针对线程和锁的分析视图助您发现多线程问题。高级子系统 - 许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的 SQL 语句。JProfiler 支持对这些子系统进行集成分析。分布式应用性能诊断如果只需要诊断单机 Java 应用的性能瓶颈,上面介绍的诊断工具就已经够用了。但随着现代系统架构逐渐从单体转变为分布式、微服务,单纯使用上述工具往往无法满足需求,这时就需要借助 Jaeger、ARMS、SkyWalking 这些分布式追踪系统提供的全链路追踪功能。分布式追踪系统种类繁多,但实现原理都大同小异,它们通过代码埋点的方式记录 tracing 信息,通过 SDK 或 agent 将记录的数据传输至中央处理系统,最后提供 query 接口对结果进行展示和分析,想了解更多分布式追踪系统的原理可参考文章开放分布式追踪(OpenTracing)入门与 Jaeger 实现。JProfiler 简介核心组件JProfiler 包含用于采集目标 JVM 分析数据的 JProfiler agent、用于可视化分析数据的 JProfiler UI、提供各种功能的命令行工具,它们之间的关系如下图所示。JProfiler agentJProfiler agent 是一个本地库,它可以在 JVM 启动时通过参数-agentpath:<path to native library>进行加载或者在程序运行时通过 JVM Attach 机制进行加载。Agent 被成功加载后,会设置 JVMTI 环境,监听虚拟机产生的事件,如类加载、线程创建等。例如,当它监听到类加载事件后,会给这些类注入用于执行度量操作的字节码。JProfiler UIJProfiler UI 是一个可独立部署的组件,它通过 socket 和 agent 建立连接。这意味着不论目标 JVM 运行在本地还是远端,JProfiler UI 和 agent 间的通信机制都是一样的。JProfiler UI 的主要功能是展示通过 agent 采集上来的分析数据,此外还可以通过它控制 agent 的采集行为,将快照保存至磁盘,展示保存的快照。命令行工具JProfiler 提供了一系列命令行工具以实现不同的功能。jpcontroller - 用于控制 agent 的采集行为。它通过 agent 注册的 JProfiler MBean 向 agent 传递命令。jpenable - 用于将 agent 加载到一个正在运行的 JVM 上。jpdump - 用于获取正在运行的 JVM 的堆快照。jpexport & jpcompare - 用于从保存的快照中提取数据并创建 HTML 报告。安装配置JProfiler 同时支持诊断本地和远程 Java 应用的性能。如果您需要实时采集并展示远程 JVM 的分析数据,需要完成以步骤:在本地安装 JProfiler UI。在远程宿主机上安装 JProfiler agent 并让其被目标 JVM 加载。配置 UI 到 agent 的连接。具体步骤可参考文档 Installing JProfiler 和 Profiling A JVM。最佳实践本章将以高性能写 LogHub 类库 Aliyun LOG Java Producer 为原型,带您了解如何使用 JProfiler 剖析它的性能。如果您的应用或者您在使用 producer 的过程中遇到了性能问题,也可以用类似的方式定位问题根因。如果您还不了解 producer 的功能,建议先阅读文章日志上云利器 - Aliyun LOG Java Producer。本章使用的样例代码参见 SamplePerformance.java。JProfiler 设置数据采集模式JProfier 提供两种数据采集模式 Sampling 和 Instrumentation。Sampling - 适合于不要求数据完全精确的场景。优点是对系统性能的影响较小,缺点是某些特性不支持(如方法级别的统计信息)。Instrumentation - 完整功能模式,统计信息也是精确的。缺点是如果需要分析的类比较多,对应用性能影响较大。为了降低影响,往往需要和 Filter 一起使用。由于我们需要获取方法级别的统计信息,这里选择了 Instrumentation 模式。同时配置了 Filter,让 agent 只记录位于 Java 包com.aliyun.openservices.aliyun.log.producer下的类和类com.aliyun.openservices.log.Client的 CPU 分析数据。应用启动模式通过为 JProfiler agent 指定不同的参数可以控制应用的启动模式。等待模式 - 只有在 Jprofiler GUI 和 agent 建立连接并完成分析配置设置后,应用才会真正启动。在这种模式下,您能够获取应用启动时期的分析数据。对应的命令为-agentpath:<path to native library>=port=8849。立即启动模式 - 应用会立即启动,Jprofiler GUI 会在需要时和 agent 建立连接并设置分析配置。这种模式相对灵活,但会丢失应用启动初期的分析数据。对应的命令为-agentpath:<path to native library>=port=8849,nowait。离线模式 - 通过触发器记录数据、保存快照供事后分析。对应的命令为-agentpath:<path to native library>=offline,id=xxx,config=/config.xml。因为是在测试环境,同时对应用启动初期的性能也比较关注,这里选择了默认的等待模式。使用 JProfiler 诊断性能在完成 JProfiler 的设置后,便可以对 Producer 的性能进行诊断。Overview在概览页我们可以清晰的看到内存使用量、垃圾收集活动、类加载数量、线程个数和状态、CPU 使用率等指标随时间变化的趋势。通过此图,我们可以作出如下基本判断:程序在运行过程中会产生大量对象,但这些对象生命周期极短,大部分都能被垃圾收集器及时回收,不会造成内存无限增长。加载类的数量在程序初始时增长较快,随后保持平稳,符合预期。在程序运行过程中,有大量线程处于阻塞状态,需要重点关注。在程序刚启动时,CPU 使用率较高,需要进一步探究其原因。CPU viewsCPU views 下的各个子视图展示了应用中各方法的执行次数、执行时间、调用关系等信息,能帮我们定位对应用性能影响最大的方法。Call TreeCall tree 通过树形图清晰地展现了方法间的层次调用关系。同时,JProfiler 将子方法按照它们的执行总时间由大到小排序,这能让您快速定位关键方法。对于 Producer 而言,方法SendProducerBatchTask.run()耗时最多,继续向下查看会发现该方法的主要时间消耗在了执行方法Client.PutLogs()上。Hot Spots如果您的应用方法很多,且很多子方法的执行时间比较接近,使用 hot spots 视图往往能助您更快地定位问题。该视图能根据方法的单独执行时间、总执行时间、平均执行时间、调用次数等属性对它们排序。其中,单独执行时间等于该方法的总执行时间减去所有子方法的总执行时间。在该视图下,可以看到Client.PutLogs(),LogGroup.toByteArray(),SamplePerformance$1.run()是单独执行时间耗时最多的三个方法。Call Graph找到了关键方法后,call graph 视图能为您呈现与该方法直接关联的所有方法。这有助于我们对症下药,制定合适的性能优化策略。这里,我们观察到方法Client.PutLogs()执行的主要时间花费在了对象序列化上,因此性能优化的关键是提供执行效率更高的序列化方法。Live memoryLive memory 下的各个子视图能让您掌握内存的具体分配和使用情况,助您判断是否存在内存泄漏问题。All ObjectsAll Objects 视图展示了当前堆中各种对象的数量和总大小。由图可知,程序在运行过程中构造出了大量 LogContent 对象。Allocation Call TreeAllocation Call Tree 以树形图的形式展示了各方法分配的内存大小。可以看到,SamplePerformance$1.run()和SendProducerBatchTask.run()是内存分配大户。Allocation Hot Spots如果方法比较多,您还可以通过 Allocation Hot Spots 视图快速找出分配对象最多的方法。Thread History线程历史记录视图直观地展示了各线程在不同时间点的状态。不同线程执行的任务不同,所展现的状态特征也不同。线程pool-1-thread-<M>会循环调用producer.send()方法异步发送数据,它们在程序刚启动时一直处于运行状态,但随后在大部分时间里处于阻塞状态。这是因为 producer 发送数据的速率低于数据的产生速率,且单个 producer 实例能缓存的数据大小有限。在程序运行初始,producer 有足够空间缓存待发送数据,所以pool-1-thread-<M>一直处于运行状态,这也就解释了为何程序在刚启动时 CPU 使用率较高。随着时间的推移,producer 的缓存被逐渐耗尽,pool-1-thread-<M>必须等到 producer “释放”出足够的空间才有机会继续运行,这也是为什么我们会观察到大量线程处于阻塞状态。aliyun-log-producer-0-mover负责将超时 batch 投递到发送线程池中。由于发送速率较快,batch 会因缓存的数据达到了上限被pool-1-thread-<M>直接投递到发送线程池中,因此 mover 线程在大部分时间里都处于等待状态。aliyun-log-producer-0-io-thread-<N>作为真正执行数据发送任务的线程有一部分时间花在了网络 I/O 状态。aliyun-log-producer-0-success-batch-handler用于处理发送成功的 batch。由于回调函数比较简单,执行时间短,它在大部分时间里都处于等待状态。aliyun-log-producer-0-failure-batch-handler用于处理发送失败的 batch。由于没有数据发送失败,它一直处于等待状态。通过上述分析可知,这些线程的状态特征都是符合预期的。Overhead Hot Spots Detected当程序运行结束后,JProfiler 会弹出一个对话框展示那些频繁被调用,但执行时间又很短的方法。在下次诊断时,您可以让 JProfiler agent 在分析过程中忽略掉这些方法以减轻对应用性能的影响。小结通过 JProfiler 的诊断可知应用不存在大的性能问题,也不存在内存泄漏。下一步的优化方向是提升对象的序列化效率。参考资料深入浅出JProfilerJprofiler Introduction本文作者:吴波bruce_wu阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 7, 2019 · 2 min · jiezi

2019 PHP程序员发展路线

我一生的文章都会放在这里,我的博客,我希望每一行代码,每一段文字都能帮助你。https://github.com/CrazyCodes…前言新的一年,新气象。在2012年我参加LAMP兄弟连的培训,成为一名PHP程序员。那个时候PHP盛行,简直有称霸世界的迹象。当然现在我大PHP也不差。我认为成为PHP程序员的有以下三种途径。科班出身,找了一份PHP开发相关的工作自学成才 (毕竟在当时PHP入门是所有语言里最简单的)培训班出身 (就像我,不是科班,自学能力也不强,所以参加的培训学校 注:2012年的LAMP兄弟连其实不错的,现在的就不评论了)就我这近六年的职业生涯。为大家准备了一份2019年程序员发展路线。跟上潮流2012年的PHP是web开发的强者,我记得当年的PHP微信开发简直是火到不行,经过这几年的不断发展,PHP实际更偏向后端了。我已经很久没有动过前端的东西了,当年都是混合开发乱的不行,所以作为一个PHP程序员不要太计较前端的那些技能,注重后端该会的东西。框架Laravel 一款过度设计的,优雅的,复杂的 PHP开发框架 , 这个框架在我多年实践中证明只适合写后台,如果用他写接口你会发现性能与原生PHP差距很大,具体比对数据可自行Google。建议把Laravel的设计方式认真学习一下,并非必须去学习使用这款框架Lumen 这是一款Laravel的Api框架,其速度要比Laravel快很多,是一款精简的LaravelSymfony 没怎么看过这款框架,Symfony即是一款框架,也是一组PHP组件库,要知道Laravel的DB,Dump,Route,Response其实都是在Symfony组件基础上做的。可见laravel composer.json https://github.com/laravel/fr...CodeIgniter 也可以关注下上个时代框架霸主,CodeIgniter 他的新版本可能会有奇迹发生扩展swoole PHP异步编程框架,这个就不必多说了。自4.1.0 Swoole加入Coroutine,使并发开发更简单。语法非常类似Goroutinerabbitmq 消息队列,数据过多的时候就知道有什么用了docker 不要告诉我2019年你还不听过docker,容器技术泛滥,该看下了老铁,https://segmentfault.com/a/11… , https://segmentfault.com/a/11...DevOps这是一个看起来高端但很切合实际的话题。如何做到DevOps? 可以先了解下下面的知识travisCi 一款基于Github的自动发布,自动集成,自动测试的平台 ,https://segmentfault.com/a/11…teamcity jetbrains推出的一款自动发布、集成、测试的平台,https://segmentfault.com/a/11…phpunit 当然做前面两个之前你必须学会如何有效的写测试composer 学习强有力的搬砖技巧,板巧砖,要学会找各种组件包去实现自己的应用算法算法是程序开发的基础,(大厂更看重基础),可以适当在下方平台去联系LintCode力扣什么?上面的题根本做不出来?没思路?乱七八糟的一些算法书我就不推荐的,首先判定你与我当年一样 (我们数学就没学好),虽然计算机算法与数学有些许出入,不过还是建议继续看我下面的建议。基础从小就不爱学习的我,选择了这个职业,无奈基础没打牢(实际就是没打),我选择这样强补知识。作为山东人(北方人),我选择了人教版《数学》,如果你有这样的勇气,那么跟我一起来补基础吧。我是从初中数学开始到高中数学。在学习的过程中买很多试卷做,巩固练习。在这之后再考虑大学期间学习的知识吧。其他相关阅读书籍可参考下方https://segmentfault.com/a/11…除了数学外,则应该是计算机相关的线程,通信协议等等….语言之所以把学习其他语言放到这里,是感觉并不是太重要,如果是一位长期战斗的程序员,我相信他的学习另外一门语言是手到擒来的。不过你也可以选择几门当做业余爱好。GoJavaC+Python随意选择,学什么语言都一样,不过只是学学语法而已(至少大多人都是这样)致谢感谢你看到这里,希望2019年的你比2018年更上一层楼,希望我的文章可以从根本上帮助到你。谢谢

January 6, 2019 · 1 min · jiezi

简说Java线程的那几个启动方式

本文首发于 猫叔的博客,转载请申明出处前言并发是一件很美妙的事情,线程的调度与使用会让你除了业务代码外,有新的世界观,无论你是否参与但是这对于你未来的成长帮助很大。所以,让我们来好好看看在Java中启动线程的那几个方式与介绍。Thread对于 Thread 我想这个基本上大家都认识的,在Java源码是这样说: java 虚拟机允许应用程序同时运行多个执行线程。 而这个的 Thread 就是程序的执行线程。如何使用它呢,其实在这个类中的源码已经给我们写好了,甚至是下面的 Runnable 的使用方式。(如下是Thread源码)/** * A <i>thread</i> is a thread of execution in a program. The Java * Virtual Machine allows an application to have multiple threads of * execution running concurrently. * <hr><blockquote><pre> * class PrimeThread extends Thread { * long minPrime; * PrimeThread(long minPrime) { * this.minPrime = minPrime; * } * * public void run() { * // compute primes larger than minPrime * &nbsp;.&nbsp;.&nbsp;. * } * } * </pre></blockquote><hr> * <p> * The following code would then create a thread and start it running: * <blockquote><pre> * PrimeThread p = new PrimeThread(143); * p.start(); * </pre></blockquote> * <p> * <hr><blockquote><pre> * class PrimeRun implements Runnable { * long minPrime; * PrimeRun(long minPrime) { * this.minPrime = minPrime; * } * * public void run() { * // compute primes larger than minPrime * &nbsp;.&nbsp;.&nbsp;. * } * } * </pre></blockquote><hr> * <p> * The following code would then create a thread and start it running: * <blockquote><pre> * PrimeRun p = new PrimeRun(143); * new Thread(p).start(); * </pre></blockquote> * <p> /public class Thread implements Runnable { //…}阅读源码的信息其实是最全的 ,我截取了部分的注释信息,起码我们现在可以无压力的使用这个两个方式来启动自己的线程。如果我们还要传递参数的话,那么我们设定一个自己的构造函数也是可以,如下方式:public class MyThread extends Thread { public MyThread(String name) { super(name); } @Override public void run() { System.out.println(“一个子线程 BY " + getName()); }}这时读者应该发现,这个构造函数中的 name ,居然在 Thread 中也是有的,其实在Java中的线程都会自己的名称,如果我们不给其定义名称的话,java也会自己给其命名。/** Allocates a new {@code Thread} object. This constructor has the same* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}* {@code (null, null, name)}.** @param name* the name of the new thread*/public Thread(String name) { init(null, null, name, 0);}而我们最核心,也是大家最在意的应该就是如何启动并执行我们的线程了,是的,这个大家都知道的,就是这个 run 方法了。同时大家如果了解过了 Runnable ,我想大家都会知道这个 run 方法,其实是 Runnable 的方法,而我们本节的 Thread 也是实现了这个接口。这里,大家可能会好奇,不是应该是 start 这个方法吗?那么让我们看看 start 的源码。/** * Causes this thread to begin execution; the Java Virtual Machin * calls the <code>run</code> method of this thread. /public synchronized void start() { //…}通过 start 方法,我们可以了解到,就如同源码的启动模板中那样,官网希望,对于线程的启动,使用者是通过 start 的方式来启动线程,因为这个方法会让Java虚拟机会调用这个线程的 run 方法。其结果就是,一个线程去运行 start 方法,而另一个线程则取运行 run 方法。同时对于这样线程,Java官方也说了,线程是不允许多次启动的,这是不合法的。所以如果我们执行下面的代码,就会报 java.lang.IllegalThreadStateException 异常。MyThread myThread = new MyThread(“Thread”);myThread.start();myThread.start();但是,如果是这样的代码呢?MyThread myThread = new MyThread(“Thread”);myThread.run();myThread.run();myThread.start();//运行结果一个子线程 BY Thread一个子线程 BY Thread一个子线程 BY Thread这是不合理的,如果大家有兴趣,可以去试试并动手测试下,最好开调试模式。下面我们再看看,连 Thread 都要实现,且核心的 run 方法出处的 Runnable 。Runnable比起 Thread 我希望大家跟多的使用 Runnable 这个接口实现的方式,对于好坏对比会在总结篇说下。我想大家看 Runnable 的源码会更加容易与容易接受,毕竟它有一个 run 方法。(如下为其源码)/* * The <code>Runnable</code> interface should be implemented by any * class whose instances are intended to be executed by a thread. The * class must define a method of no arguments called <code>run</code>. /@FunctionalInterfacepublic interface Runnable { /* * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object’s * <code>run</code> method to be called in that separately executing * thread. / public abstract void run();}首先,所有打算执行线程的类均可实现这个 Runnable 接口,且必须实现 run 方法。它将为各个类提供一个协议,就像 Thread 一样,其实当我们的类实现了 Runnable 的接口后,我们的类与 Thread 是同级,只是可能仅有 run 方法,而没有 Thread 提供的跟丰富的功能方法。而对于 run 方法,则是所有实现了 Runnable 接口的类,在调用 start 后,将使其单独执行 run 方法。那么我们可以写出这样的测试代码。MyThreadRunnable myThreadRunnable = new MyThreadRunnable(“Runnabel”);myThreadRunnable.run();new Thread(myThreadRunnable).start();Thread thread = new Thread(myThreadRunnable);thread.start();thread.start();//运行效果Exception in thread “main” java.lang.IllegalThreadStateException at java.lang.Thread.start(Thread.java:705) at com.github.myself.runner.RunnableApplication.main(RunnableApplication.java:14)这是一个子线程 BY Runnabel这是一个子线程 BY Runnabel这是一个子线程 BY Runnabel同样的,线程是不允许多次启动的,这是不合法的。同时,这时我们也看出了使用 Thread 与 Runnable 的区别,当我们要多次启用一个相同的功能时。我想 Runnable 更适合你。但是,用了这两个方式,我们要如何知道线程的运行结果呢???FutureTask这个可能很少人(初学者)用到,不过这个现在是我最感兴趣的。它很有趣。其实还有一个小兄弟,那就是 Callable。 它们是一对搭档。如果上面的内容,你已经细细品味过,那么你应该已经发现 Callable 了。没错,他就在 Runnable 的源码中出现过。/* * @author Arthur van Hoff * @see java.lang.Thread * @see java.util.concurrent.Callable * @since JDK1.0 / @FunctionalInterfacepublic interface Runnable {}那么我们先去看看这个 Callable 吧。(如下为其源码)/* * A task that returns a result and may throw an exception. * Implementors define a single method with no arguments called * {@code call}. /@FunctionalInterfacepublic interface Callable<V> { /* * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result / V call() throws Exception;}其实,这是一个与 Runnable 基本相同的接口,当时它可以返回执行结果与检查异常,其计算结果将由 call() 方法返回。那么其实我们现在可以写出一个实现的类。public class MyCallable implements Callable { private String name; public MyCallable(String name) { this.name = name; } @Override public Object call() throws Exception { System.out.println(“这是一个子线程 BY " + name); return “successs”; }}关于更深入的探讨,我将留到下一篇文章中。好了,我想我们应该来看看 FutureTask 这个类的相关信息了。/* * A cancellable asynchronous computation. This class provides a base * implementation of {@link Future}, with methods to start and cancel * a computation, query to see if the computation is complete, and * retrieve the result of the computation. The result can only be * retrieved when the computation has completed; the {@code get} * methods will block if the computation has not yet completed. Once * the computation has completed, the computation cannot be restarted * or cancelled (unless the computation is invoked using * {@link #runAndReset}). / public class FutureTask<V> implements RunnableFuture<V> { //… }源码写的很清楚,这是一个可以取消的异步计算,提供了查询、计算、查看结果等的方法,同时我们还可以使用 runAndRest 来让我们可以重新启动计算。在查看其构造函数的时候,很高兴,我们看到了我们的 Callable 接口。/* * Creates a {@code FutureTask} that will, upon running, execute the * given {@code Callable}. * * @param callable the callable task * @throws NullPointerException if the callable is null */public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable}即我们将创建一个未来任务,来执行 Callable 的实现类。那么我们现在可以写出这样的代码了。final FutureTask fun = new FutureTask(new MyCallable(“Future”));那么接下来我们就可以运行我们的任务了吗?是的,我知道了 run() 方法,但是却没有 start 方法。官方既然说有结果,那么我找到了 get 方法。同时我尝试着写了一下测试代码。public static void main(String[] args) { MyCallable myCallable = new MyCallable(“Callable”); final FutureTask fun = new FutureTask(myCallable); fun.run(); try { Object result = fun.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }}运行效果,是正常的,这好像是那么回事。//运行效果这是一个子线程 BY Callablesuccesss可是,在我尝试着加多一些代码的时候,却发现了一些奇妙的东西 。我加多了一行 fun.run(); 代码,同时在 MyCallable 类中,将方法加一个时间线程去等待3s。结果是: 结果只输出了一次,同时 get 方法需要等运行3s后才有返回。这并不是我希望看到的。但是,起码我们可以知道,这次即使我们多次运行使用 run 方法,但是这个线程也只运行了一次。这是一个好消息。同时,我们也拿到了任务的结果,当时我们的进程被阻塞了,我们需要去等我们的任务执行完成。最后,在一番小研究后,以下的代码终于完成了我们预期的期望。public static void main(String[] args) { MyCallable myCallable = new MyCallable(“Callable”); ExecutorService executorService = Executors.newCachedThreadPool(); final FutureTask fun = new FutureTask(myCallable); executorService.execute(fun);// fun.run(); //阻塞进程 System.out.println(”–继续执行”); try { Object result = fun.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }}我们使用线程池去运行我们的 FutureTask 同时使用 get 方法去获取运行后的结果。结果是友好的,进程并不会被阻塞。关于更深入的探讨,我将留到下一篇文章中。总结一波好了,现在应该来整理以下了。Thread 需要我们继承实现,这是比较局限的,因为Java的 继承资源 是有限的,同时如果多次执行任务,还需要 多次创建任务类。Runnable 以接口的形式让我们实现,较为方便,同时多次执行任务也无需创建多个任务类,当时仅有一个 run 方法。以上两个方法都 无法获取任务执行结果 ,FutureTask可以获取任务结果。同时还有更多的新特性方便我们使用···公众号:Java猫说现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。 ...

December 30, 2018 · 5 min · jiezi

短视频宝贝=慢?阿里巴巴工程师这样秒开短视频

前言随着短视频兴起,各大APP中短视频随处可见,feeds流、详情页等等。怎样让用户有一个好的视频观看体验显得越来越重要了。大部分feeds里面滑动观看视频的时候,有明显的等待感,体验不是很好。针对这个问题我们展开了一波优化,目标是:视频播放秒开,视频播放体验良好。无图无真相,上个对比图,左边是优化之前的,右边是优化之后的:问题分析视频格式的选择在正式分析问题之前有必要说明下:我们现在首页的视频,都是320p H.264编码的mp4视频。H.264 & H.265H.264也称作MPEG-4AVC(Advanced Video Codec,高级视频编码),是一种视频压缩标准,同时也是一种被广泛使用的高精度视频的录制、压缩和发布格式。H.264因其是蓝光光盘的一种编解码标准而著名,所有蓝光播放器都必须能解码H.264。H.264相较于以前的编码标准有着一些新特性,如多参考帧的运动补偿、变块尺寸运动补偿、帧内预测编码等,通过利用这些新特性,H.264比其他编码标准有着更高的视频质量和更低的码率.H.265/HEVC的编码架构大致上和H.264/AVC的架构相似,也主要包含:帧内预测(intra prediction)、帧间预测(inter prediction)、转换 (transform)、量化 (quantization)、去区块滤波器(deblocking filter)、熵编码(entropy coding)等模块。但在HEVC编码架构中,整体被分为了三个基本单位,分别是:编码单位(coding unit,CU)、预测单位(predict unit,PU) 和转换单位(transform unit,TU )。总的来说H.265压缩效率更高,传输码率更低,视频画质更优。看起来使用H.265似乎是很明智的选择,但我们这里选择的是H.264。原因是:H.264支持的机型范围更为广泛。 PS:闲鱼H.265视频在宝贝详情页会在近期上线,敬请关注体验!TS & FLV & MP4TS是日本高清摄像机拍摄下进行的封装格式,全称为MPEG2-TS。TS即"Transport Stream"的缩写。MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。下述命令可以把mp4转换成ts格式,从结果来看ts文件(4.3MB)比mp4文件(3.9MB)大10%左右。ffmpeg -i input.mp4 -c copy output.tsFLV是FLASH VIDEO的简称,FLV流媒体格式是随着Flash MX的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等问题。FLV只支持一个音频流、一个视频流,不能在一个文件里包含多路音频流。音频采样率不支持48k,视频编码不支持H.265。相同编码格式下,文件大小和mp4几乎没有区别。 ffmpeg -i input.mp4 -c copy output.flv MP4是为大家所熟知的一种视频封装格式,MP4或称MPEG-4第14部分是一种标准的数字多媒体容器格式。MPEG-4第14部分的扩充名为.mp4,以存储数字音频及数字视频为主,但也可以存储字幕和静止图像。因其可容纳支持比特流的视频流,MP4可以在网络传输时使用流式传输。其兼容性很好,几乎所有的移动设备都支持,而且还能在浏览器、桌面系统进行播放。综合上面几个封装格式的特点,我们的最终选择是MP4.播放流程一个视频在客户端的播放流程是怎么样的?播放首开慢耗时在什么地方?耗时点是否能够快速低成本的解决?了解视频的播放流程有助于找到问题的突破口。视频从加载到播放可以分为三个阶段:读取(IO):“获取” 内容 -> 从 “本地” or “服务器” 上获取解析(Parser):“理解” 内容 -> 参考 “格式&协议” 来 “理解” 内容渲染(Render):“展示” 内容 -> 通过扬声器/屏幕来 “展示” 内容可以看出内容获取从“服务器”改为“本地”,这样会节省很大一部分时间,而且成本很低,是一个很好的切入点。事实也是如此,我们的优化正是围绕此点展开。PS: 我们使用的网络库,播放器都是集团内部的,本身做了很多优化。本文不涉及网络协议,播放器方面的优化讨论。技术方案鉴于上面的分析,我们要做的工作是:把mp4文件提前缓存一部分,到feeds滑动要播放的时候,播放本地的mp4文件。由于用户可能继续观看视频,所以本地的数据播放完后,需要从网络下载数据进行播放。这里需要解决两个问题:应该提前下载多少数据缓存数据播放完成后该怎么切换到网络数据MOOV BOX的位置对于第一个问题,我们不得不分析一下mp4的文件结构,看看我们应该下载多少数据量合适。MP4是由很多Box 组成的,Box里面可以嵌套Box:这里不详细介绍MP4的格式信息。但是可以看出moov box对播放很关键,它提供的信息如:宽高、时长、码率、编码格式、帧列表、关键帧列表等等。播放器没有获取到moov box是没办法进行播放的。所以下载的数据应该要包含moov box再加上几十帧的数据。做了一个简单的计算:闲鱼短视频一般最长是30s,feeds里面的分辨率是320p,码率是1141kb/s,ftyp+moov这个视频的数据量在31kb左右(打开文件可以看出mdat是从31754byte的位置开始的),所以,头部信息+10帧的数据大约是:(31kb + 1141kb/3)/8 = 51KBProxy第二个问题:缓存数据播放完成后该怎么切换到网络数据呢?在本地数据播放完成之后,设置一个网络地址给播放器,告诉播放器下载的offset是多少,然后继续从网络下载数据播放。这样看起来可行,但是需要播放器提供支持:本地数据播放完成的回调;设置网络url并支持offset。另外,服务端需要支持range参数,而且切换到网络播放的时候需要新建立网络连接,很可能会造成卡顿。最终,我们选择了proxy的方式,把proxy作为中间人,负责预加载数据、给播放器提供数据,切换逻辑在proxy里面来完成。未加入proxy之前流程是这样的:加入了proxy之后流程是这样的:这样做的好处很明显,我们可以在proxy里面做很多事情:例如本地文件缓存数据和网络数据的切换工作。甚至是和CDN使用其它的协议进行通信。我们这里假定预加载工作已经完成,看看播放器是怎么和proxy进行交互的。播放的时候会用Proxy提供的一个localhost的url进行播放,这样代理服务器会收到网络请求,把本地预加载的数据返回给播放器。播放器完全感知不到proxy模块、预加载模块的存在。播放器、预加载模块都是Proxy的client,调用逻辑都是一样。图示说明如下:下面逐步解释一下,数据的加载过程:Client发起http请求获取数据,箭头1所示文件缓存如果存在所请求的数据则直接返回数据,箭头2所示若本地文件缓存数据不够,则发起网络请求,向CDN请求数据,箭头3所示获取网络数据,写入文件缓存,箭头4所示返回请求的数据给Client,箭头2所示实现模块预加载模块确定了技术方案后,预加载模块还是有很多工作要做的。在列表网络数据解析完成后会触发视频预加载,首先会根据url生成md5值,然后去查看这个md5值对应的任务是否存在,如果存在则不会重复提交。生成任务后会提交到线程池,在后台线程进行处理。网络从Wifi切换到3G的时候,会把任务取消,防止消耗用户的数据流量。预加载任务在线程池执行的时候,其流程是这样的:首先会获取一个本地代理的url。然后发起http请求。Proxy会收到http请求进行处理,开始做真正的数据预加载工作。预加载模块读取到指定的数据量后终止。到此,预加载的任务就已完成。流程图如下所示:在用户快速滑动的时候,怎么能保证视频还能继续秒开呢?预加载模块对于每一个任务都会维护一个状态机,在Fling的时候会把划过的任务暂停下,把最新要显示的任务优先级提高,让其优先执行。Proxy模块Proxy内部有个local的httpServer负责拦截播放器和预加载模块的http请求。client在请求时会带入CDN的url,在本地缓存数据没有的时候会去CDN获取新鲜数据。因为有多个地方向Proxy请求数据,所以用线程池来处理多个client的连接很有必要,这样多个client可以并行,不会因为前面有client在请求而阻塞。文件缓存使用LruDiskCache,在超过指定文件大小后,老的缓存文件会删除,这是一个在使用文件缓存时很容易忽视的问题。由于我们的场景视频是连续播放的,不存在seek的情况,所以文件缓存相对比较简单,不用考虑文件分段的情况。Proxy内部对于同一个url会映射到一个client,如果预加载和播放同时进行,数据只会有一份,不会去重复下载数据。再来一个Proxy内部构造示意图:遇到的问题在测试中发现,有的视频还是会播放很慢,仔细查看本地的确缓存了期望的数据大小,但是播放的时候还是有较长的等待时间,这种视频有个特点:moov box在尾部。对于moov在尾部的视频,是整个文件都下载完成后才进行播放的,原因是moov box里面存了很多关键信息,前面分析mp4格式的时候有提到。对于这个问题有两个解法:解法一:服务端在进行转码的时候保证moov的头部在前面,发现moov位置不正确的视频服务端进行订正。PS:查看moov在文件中的位置可以用hex文本编辑器打开,按字符搜索moov所在的位置即可,MAC上面还可以使用MediaParser , 另外还可以用ffmpeg命令生成moov在头部或者尾部的mp4文件。例如: 从1.mp4 copy一个文件,使其moov头在尾部ffmpeg -i 1.mp4 -c copy -f mp4 output.mp4 从1.mp4 copy一个文件,使其moov头在头部:ffmpeg -i 1.mp4 -c copy -f mp4 -movflags faststart output2.mp4解法二不用修改moov box的位置,而是在播放端进行处理,播放端需要检测流信息,如果moov前面没有,就去请求文件的尾部信息。具体就是:发起 HTTP MP4 请求,读取响应 body 的开头,如果发现 moov 在开头,就接着往下读mdat。如果发现开头没有,先读到 mdat,马上 RESET 这个连接,然后通过 Range 头读取文件末尾数据,因为前面一个 HTTP 请求已经获取到了 Content-Length ,知道了 MP4 文件的整个大小,通过 Range 头读取部分文件尾部数据也是可以的。示意图如下这个方案的缺点是:对于moov box在尾部的视频会多两次http connection。结语本文介绍了常见的视频编码格式,视频封装格式,介绍了moov头信息对于视频播放的影响。随着对于播放流程的分析,我们找到了问题的切入点。简单说就是围绕着数据预加载展开,把网络请求数据的工作提前完成,播放的时候直接从缓存读取,而且后续的视频回看都是从缓存读取,不仅解决了视频初始化播放慢的问题,还解决了播放缓存问题,可以说是一箭双雕。Proxy是这个方案的核心思想,本地localhost的url是一个关键纽带,视频预加载模块和播放器模块解耦彻底,换了播放器照样可以使用。到此为止,视频feeds秒开优化就已完成。上线后的数据来看视频打开速度在800ms左右。回过头来,或许我们还可以更进一步,可以对预加载收到的数据进行验证,确保缓存了准确的信息,而不是固定的数值。还可以进行更加深度的优化,让用户观看视频的体验更加顺滑。参考文献* AndroidVideoCache* 视频的封装格式和编码格式* 播放器技术分享(1):架构设计* MP4文件格式的解析,以及MP4文件的分割算法* 从天猫某活动视频3次请求说起* [视音频编解码学习工程:FLV封装格式分析器]https://blog.csdn.net/leixiaohua1020/article/details/17934487* https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10_1.pdf* https://baike.baidu.com/item/flv* https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html本文作者:闲鱼技术-邻云阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 21, 2018 · 1 min · jiezi

分析core,是从案发现场,推导案发经过

分析core不是一件容易的事情。试想,一个系统运行了很长一段时间,在这段时间里,系统会积累大量正常、甚至不正常的状态。这个时候如果系统突然出现了一个问题,那这个问题十有八九跟长时间积累下来的状态有关系。分析core,就是分析出问题时,系统产生的“快照”,追溯历史,找出问题发生源头。这有点像是从案发现场,推导案发经过一样。soft lockup!今天这个“案件”,我们从soft lockup说起。soft lockup是内核实现的夯机自我诊断功能。这个功能的实现,和线程的优先级有关系。这里我们假设有三个线程A、B、和C。他们的优先级关系是A<B<C。这意味着C优先于B执行,B优先于A执行。这个优先级关系,如果倒过来叙述,就会产生一个规则:如果C不能执行,那么B也没有办法执行,如果B不能执行,那基本上A也没法执行。soft lockup实际上就是对这个规则的实现:soft lockup使用一个内核定时器(C线程),周期性地检查,watchdog(B线程)有没有正常运行。如果没有,那就意味着普通线程(A线程)也没有办法正常运行。这时内核定时器(C线程)会输出类似上图中的soft lockup记录,来告诉用户,卡在cpu上的,有问题的线程的信息。具体到这个“案件”,卡在cpu上的线程是python,这个线程正在刷新tlb缓存。老搭档ipi和tlb如果我们对所有夯机问题的调用栈做一个统计的话,我们肯定会发现,tlb和ipi是一对形影不离的老搭档。其实这不是偶然的。系统中,相对于内存,tlb是处理器本地的cache。这样的共享内存和本地cache的架构,必然会提出一致性的要求。如果每个处理器的tlb“各自为政”的话,那系统肯定会乱套。满足tlb一致性的要求,本质上来说只需要一种操作,就是刷新本地tlb的同时,同步地刷新其他处理器的tlb。系统正是靠tlb和ipi这对老搭档的完美配合来完成这个操作的。这个操作本身的代价是比较大的。一方面,为了避免产生竞争,线程在刷新本地tlb的时候,会停掉抢占。这就导致一个结果:其他的线程,当然包括watchdog线程,没有办法被调度执行(soft lockup)。另外一方面,为了要求其他cpu同步地刷新tlb,当前线程会使用ipi和其他cpu同步进展,直到其他cpu也完成刷新为止。其他cpu如果迟迟不配合,那么当前线程就会死等。不配合的cpu为什么其他cpu不配合去刷新tlb呢?理论上来说,ipi是中断,中断的优先级是很高的。如果有cpu不配合去刷新tlb,基本上有两种可能:一种是这个cpu刷新了tlb,但是做到一半也卡住了;另外一种是,它根本没有办法响应ipi中断。通过查看系统中所有占用cpu的线程,可以看到cpu基本上在做三件事情:idle,正在刷新tlb,和正在运行java程序。其中idle的cpu,肯定能在需要的时候,响应ipi并刷新tlb。而正在刷新tlb的cpu,因为停掉了抢占,且在等待其他cpu完成tlb刷新,所以在重复输出soft lockup记录。这里问题的关键,是运行java的cpu,这个我们在下一节讲。java不是问题,踩到的坑才是问题java线程运行在0号cpu上,这个线程的调用栈,满满的都是故事。我们可以简单地把线程调用栈分为上下两部分。下边的是system call调用栈,是java从系统调用进入内核的执行记录。上边的是中断栈,java在执行系统调用的时候,正好有一个中断进来,所以这个cpu临时去处理了中断。在linux内核中,中断和系统调用使用的是不同的内核栈,所以我们可以看到第二列,上下两部分地址是不连续的。netoops持有等待分析中断处理这部分调用栈,从下往上,我们首先会发现,netoops函数触发了缺页异常。缺页异常其实就是给系统一个机会,把指令踩到的虚拟地址,和真正想要访问的物理机之间的映射关系给建立起来。但是有些虚拟地址,这种映射根本就是不存在的,这些地址就是非法地址(坑)。如果指令踩到这样的地址,会有两种后果,segment fault(进程)和oops(内核)。很显然netoops踩到了非法地址,使得系统进入了oops逻辑。系统进入oops逻辑,做的第一件事情就是禁用中断。这个非常好理解。oops逻辑要做的事情是保存现场,它当然不希望,中断在这个时候破坏问题现场。接下来,为了保存现场的需要,netoops再一次被调用,然后这个函数在几条指令之后,等在了spinlock上。要拿到这个spinlock,netoops必须要等它当前的owner线程释放它。这个spinlock的owner是谁呢?其实就是当前线程。换句话说,netoops拿了spinlock,回过头来又去要这个spinlock,导致当前线程死锁了自己。验证上边的结论,我们当然可以去读代码。但是有另外一个技巧。我们可以看到netoops函数在踩到非法地址的时候,指令rip地址是ffffffff8137ca64,而在尝试拿spinlock的时候,rip是ffffffff8137c99f。很显然拿spinlock在踩到非法地址之前。虽然代码里的跳转指令,让这种判断不是那么的准确,但是大部分情况下,这个技巧是很有用的。缺页异常,错误的时间,错误的地点这个线程进入死锁的根本原因是,缺页异常在错误的时间发生在了错误的地点。对netoops函数的汇编和源代码进行分析,我们会发现,缺页发生在ffffffff8137ca64这条指令,而这条指令是inline函数utsname的指令。下图中框出来的四条指令,就是编译后的utsname函数。而utsname函数的源代码其实就一行。return &current->nsproxy->uts_ns->name;这行代码通过当前进程的task_struct指针current,访问了uts namespace相关的内容。这一行代码,之所以会编译成截图中的四条汇编指令,是因为gs寄存器的0xcbc0项,保存的就是current指针。这四条汇编指令做的事情分别是,取current指针,读nsproxy项,读uts_ns项,以及计算name的地址。第三条指令踩到非法地址,是因为nsproxy这个值为空值。空值nsproxy我们可以在两个地方验证nsproxy为空这个结论。第一个地方是读取当前进程task_sturct的nsproxy项。另外一个是看缺页异常的时候,保存下来的rax寄存器的值。保存下来的rax寄存器值可以在图三中看到,下边是从task_struct里读出来的nsproxy值。正在退出的线程那么,为什么当前进程task_struct这个结构的nsproxy这一项为空呢?我们可以回头看一下,java线程调用栈的下半部分内容。这部分调用栈实际上是在执行exit系统调用,也就是说进程正在退出。实际上参考代码,我们可以确定,这个进程已经处于僵尸(zombie)状态了。因而nsproxy相关的资源,已经被释放了。namespace访问规则最后我们简单看一下nsproxy的访问规则。规则一共有三条,netoops踩到空指针的原因,某种意义上来说,是因为它间接地违背了第三条规则。netoops通过utsname访问进程的namespace,因为它在中断上下文,所以并不算是访问当前的进程,也就是说它应该查空。另外我加亮的部分,进一步佐证了上一小节的结论。/*``* the namespaces access rules are:``*``* 1\. only current task is allowed to change tsk-&gt;nsproxy pointer or``* any pointer on the nsproxy itself``*``* 2\. when accessing (i.e. reading) current task's namespaces - no``* precautions should be taken - just dereference the pointers``*``* 3\. the access to other task namespaces is performed like this``* rcu_read_lock();``* nsproxy = task_nsproxy(tsk);``* if (nsproxy != NULL) {``* / *``* * work with the namespaces here``* * e.g. get the reference on one of them``* * /``* } / *``* * NULL task_nsproxy() means that this task is``* * almost dead (zombie)``* * /``* rcu_read_unlock();``*``*/回顾最后我们复原一下案发经过。开始的时候,是java进程退出。java退出需要完成很多步骤。当它马上就要完成自己使命的时候,一个中断打断了它。这个中断做了一系列的动作,之后调用了netoops函数。netoops函数拿了一个锁,然后回头去访问java的一个被释放掉的资源,这触发了一个缺页。因为访问的是非法地址,所以这个缺页导致了oops。oops过程禁用了中断,然后调用netoops函数,netoops需要再次拿锁,但是这个锁已经被自己拿了,这是典型的死锁。再后来其他cpu尝试同步刷新tlb,因为java进程关闭了中断而且死锁了,它根本收不到其他cpu发来的ipi消息,所以其他cpu只能不断的报告soft lockup错误。本文作者:声东阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 3, 2018 · 1 min · jiezi

关于Flutter初始化流程,我必须告诉你的是...

引言最近在做性能优化的时候发现,在混合栈开发中,第一次启动Flutter页面的耗时总会是第二次启动Flutter页面耗时的两倍左右,这样给人感觉很不好。分析发现第一次启动Flutter页面会做一些初始化工作,借此,我梳理了下Flutter的初始化流程。2. Flutter初始化时序Flutter初始化主要分四部分,FlutterMain初始化、FlutterNativeView初始化、FlutterView初始化和Flutter Bundle初始化。我们先看下Flutter初始化的时序图,来整体把握下Flutter初始化的一般流程: Flutter初始化时序3. 具体分析3.1 FlutterMain初始化这部分初始化工作是由Application.onCreate方法中调用开始的,在Application创建的时候就会初始化完成,不会影响Flutter页面的第一次启动,所以这里只是做一个简单分析。 从FlutterMain.startInitialization方法代码中可以轻易看出来,初始化主要分四部分。 前面三部分比较类似,分别是初始化配置信息、初始化AOT编译和初始化资源,最后一部分则是加载Flutter的Native环境。 这部分感兴趣的同学可以看下FlutterMain.java源码,逻辑还是比较清晰的。public static void startInitialization(Context applicationContext, Settings settings) { // other codes … initConfig(applicationContext); initAot(applicationContext); initResources(applicationContext); System.loadLibrary(“flutter”); // other codes …}3.2 FlutterNativeView初始化先用一个图来展现FlutterNativeView构造函数的调用栈: FlutterNativeView构造函数调用栈从上图的调用栈中我们知道FlutterNativeView的初始化主要做了些什么,我们再从源码角度较为深入的了解下: FlutterNativeView的构造函数最终主要调用了一个nativeAttach方法。到这里就需要分析引擎层代码了,我们可以在JNI文件中找到对应的jni方法调用。(具体文件为platform_view_android_jni.cc)static const JNINativeMethod native_view_methods[] = { { .name = “nativeAttach”, .signature = “(Lio/flutter/view/FlutterNativeView;)J”, .fnPtr = reinterpret_cast<void*>(&shell::Attach), }, // other codes …};从代码中很容易看出FlutterNativeView.attach方法最终调用了shell::Attach方法,而shell::Attach方法主要做了两件事: 1. 创建PlatformViewAndroid。 2. 调用PlatformViewAndroid::Attach。static jlong Attach(JNIEnv* env, jclass clazz, jobject flutterView) { auto view = new PlatformViewAndroid(); // other codes … view->Attach(); // other codes …}那我们再分析下PlatformViewAndroid的构造函数和Attach方法都做了些什么呢?PlatformViewAndroid::PlatformViewAndroid() : PlatformView(std::make_unique<NullRasterizer>()), android_surface_(InitializePlatformSurface()) {}void PlatformViewAndroid::Attach() { CreateEngine(); // Eagerly setup the IO thread context. We have already setup the surface. SetupResourceContextOnIOThread(); UpdateThreadPriorities();}其中: 1. PlatformViewAndroid的构造函数主要是调用了InitializePlatformSurface方法,这个方法主要是初始化了Surface,其中Surface有Vulkan、OpenGL和Software三种类型的区别。 2. PlatformViewAndroid::Attach方法这里主要调用三个方法:CreateEngine、SetupResourceContextOnIOThread和UpdateThreadPriorities。 2.1 CreateEngine比较好理解,创建Engine,这里会重新创建一个Engine对象。 2.2 SetupResourceContextOnIOThread是在IO线程去准备资源的上下文逻辑。 2.3 UpdateThreadPriorities是设置线程优先级,这设置GPU线程优先级为-2,UI线程优先级为-1。3.3 FlutterView初始化FlutterView的初始化就是纯粹的Android层啦,所以相对比较简单。分析FlutterView.java的构造函数就会发现,整个FlutterView的初始化在确保FlutterNativeView的创建成功和一些必要的view设置之外,主要做了两件事: 1. 注册SurfaceHolder监听,其中surfaceCreated回调会作为Flutter的第一帧回调使用。 2. 初始化了Flutter系统需要用到的一系列桥接方法。例如:localization、navigation、keyevent、system、settings、platform、textinput。 FlutterView初始化流程主要如下图所示: FlutterView初始化3.4 Flutter Bundle初始化Flutter Bundle的初始化是由调用FlutterActivityDelegate.runFlutterBundle开始的,先用一张图来说明下runFlutterBundle方法的调用栈: Flutter的Bundle初始化我们再从源码角度较为深入了解下: FlutterActivity的onCreate方法在执行完FlutterActivityDelegate的onCreate方法之后会调用它的runFlutterBundle方法。FlutterActivityDelegate.runFlutterBundle代码如下:public void runFlutterBundle(){ // other codes … String appBundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext()); if (appBundlePath != null) { flutterView.runFromBundle(appBundlePath, null, “main”, reuseIsolate); }}很明显,这个runFlutterBundle并没有做太多事情,而且直接调用了FlutterView.runFromBundle方法。而后兜兜转转最后会调用到PlatformViewAndroid::RunBundleAndSnapshot方法。void PlatformViewAndroid::RunBundleAndSnapshot(JNIEnv* env, std::string bundle_path, std::string snapshot_override, std::string entrypoint, bool reuse_runtime_controller, jobject assetManager) { // other codes … blink::Threads::UI()->PostTask( [engine = engine_->GetWeakPtr(), asset_provider = std::move(asset_provider), bundle_path = std::move(bundle_path), entrypoint = std::move(entrypoint), reuse_runtime_controller = reuse_runtime_controller] { if (engine) engine->RunBundleWithAssets( std::move(asset_provider), std::move(bundle_path), std::move(entrypoint), reuse_runtime_controller); });}PlatformViewAndroid::RunBundleAndSnapshot在UI线程中调用Engine::RunBundleWithAssets,最终调用Engine::DoRunBundle。 DoRunBundle方法最后只会调用RunFromPrecompiledSnapshot、RunFromKernel和RunFromScriptSnapshot三个方法中的一个。而这三个方法最终都会调用SendStartMessage方法。bool DartController::SendStartMessage(Dart_Handle root_library, const std::string& entrypoint) { // other codes … // Get the closure of main(). Dart_Handle main_closure = Dart_GetClosure( root_library, Dart_NewStringFromCString(entrypoint.c_str())); // other codes … // Grab the ‘dart:isolate’ library. Dart_Handle isolate_lib = Dart_LookupLibrary(ToDart(“dart:isolate”)); DART_CHECK_VALID(isolate_lib); // Send the start message containing the entry point by calling // _startMainIsolate in dart:isolate. const intptr_t kNumIsolateArgs = 2; Dart_Handle isolate_args[kNumIsolateArgs]; isolate_args[0] = main_closure; isolate_args[1] = Dart_Null(); Dart_Handle result = Dart_Invoke(isolate_lib, ToDart("_startMainIsolate"), kNumIsolateArgs, isolate_args); return LogIfError(result);}而SendStartMessage方法主要做了三件事: 1. 获取Flutter入口方法(例如main方法)的closure。2. 获取FlutterLibrary。 3. 发送消息来调用Flutter的入口方法。4. 总结一下本次主要分析了下FlutterActivity的onCreate方法中的Flutter初始化部分逻辑,很明显会发现主要耗时在FlutterNativeView、FlutterView和Flutter Bundle的初始化这三块,将这三部分的初始化工作前置就可以比较容易的解决引言中提出的问题。经测试发现,这样改动之后,Flutter页面第一次启动时长和后面几次启动时长差不多一样了。 对于FlutterMain.startInitialization的初始化逻辑、SendStartMessage发送的消息如何最终调用Flutter中的入口方法逻辑没有进一步深入分析,这些内容后续再继续分析撰文分享。本文作者:闲鱼技术-然道阅读原文本文为云栖社区原创内容,未经允许不得转载。

November 26, 2018 · 2 min · jiezi