
暂无个人介绍
能力说明:
了解变量作用域、Java类的结构,能够创建带main方法可执行的java应用,从命令行运行java程序;能够使用Java基本数据类型、运算符和控制结构、数组、循环结构书写和运行简单的Java程序。
阿里云技能认证
详细说明作者:尹吉欢 来源:猿天地 本文摘自于《Spring Cloud微服务:入门、实战与进阶》一书。 01 配置发布后的实时推送设计 配置中心最重要的一个特性就是实时推送了,正因为有这个特性,我们可以依赖配置中心做很多事情。在我自己开发的Smconf这个配置中心,Smconf是依赖于Zookeeper的Watch机制来实现实时推送。 上图简要描述了配置发布的大致过程: 用户在Portal中进行配置的编辑和发布 Portal会调用Admin Service提供的接口进行发布操作 Admin Service收到请求后,发送ReleaseMessage给各个Config Service,通知Config Service配置发生变化 Config Service收到ReleaseMessage后,通知对应的客户端,基于Http长连接实现 02 发送ReleaseMessage的实现方式 ReleaseMessage消息是通过Mysql实现了一个简单的消息队列。之所有没有采用消息中间件,是为了让Apollo在部署的时候尽量简单,尽可能减少外部依赖。 上图简要描述了发送ReleaseMessage的大致过程: Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录 Config Service会启动一个线程定时扫描ReleaseMessage表,去查看是否有新的消息记录 Config Service发现有新的消息记录,那么就会通知到所有的消息监听器 消息监听器得到配置发布的信息后,则会通知对应的客户端 03 Config Service通知客户端的实现方式 通知是采用基于Http长连接实现,主要分为下面几个步骤: 客户端会发起一个Http请求到Config Service的notifications/v2接口 v2接口通过Spring DeferredResult把请求挂起,不会立即返回 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端 如果发现配置有修改,则会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回 客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置 04 源码解析实时推送设计 Apollo推送这块代码比较多,就不在本书中详细分析了,我把推送这块的代码稍微简化了下,给大家进行讲解,这样理解起来会更容易。当然我这边会比较简单,很多细节就不做考虑了,只是为了能够让大家明白Apollo推送的核心原理。 发送ReleaseMessage的逻辑我们就写一个简单的接口,用队列存储,测试的时候就调用这个接口模拟配置有更新,发送ReleaseMessage消息。 消息发送之后,前面我们有讲过Config Service会启动一个线程定时扫描ReleaseMessage表,去查看是否有新的消息记录,然后取通知客户端,这边我们也启动一个线程去扫描: 循环去读取NotificationControllerV2中的队列,如果有消息的话就构造一个ReleaseMessage的对象,然后调用NotificationControllerV2中的handleMessage()方法进行消息的处理。 ReleaseMessage就一个字段,模拟消息内容: 接下来,我们看handleMessage做了什么样的工作 NotificationControllerV2实现了ReleaseMessageListener接口,ReleaseMessageListener中定义了handleMessage()方法。 handleMessage就是当配置发生变化的时候,通知的消息监听器,消息监听器得到配置发布的信息后,则会通知对应的客户端: Apollo的实时推送是基于Spring DeferredResult实现的,在handleMessage()方法中可以看到是通过deferredResults获取DeferredResult,deferredResults就是第一行的Multimap,Key其实就是消息内容,Value就是DeferredResult的业务包装类DeferredResultWrapper,我们来看下DeferredResultWrapper的代码: 通过setResult()方法设置返回结果给客户端,以上就是当配置发生变化,然后通过消息监听器通知客户端的原理,那么客户端是在什么时候接入的呢? NotificationControllerV2中提供了一个/getConfig的接口,客户端在启动的时候会调用这个接口,这个时候会执行getApolloConfigNotifications()方法去获取有没有配置的变更信息,如果有的话证明配置修改过,直接就通过deferredResultWrapper.setResult(newNotifications);返回结果给客户端了,客户端收到结果后重新拉取配置的信息进行覆盖本地的配置。 如果getApolloConfigNotifications()方法没有返回配置修改的信息,证明配置没有发生修改,就将DeferredResultWrapper对象添加到deferredResults中,等待后续配置发生变化时消息监听器进行通知。 同时这个请求就会挂起,不会立即返回,挂起是通过DeferredResultWrapper中的下面的代码实现的: 在创建DeferredResult对象的时候指定了超时的时间和超时后返回的响应码,如果60秒内没有消息监听器进行通知,那么这个请求就会超时,超时后客户端就收到的响应码就是304。 整个Config Service的流程就走完了,接下来我们看客户端是怎么实现的,我们简单的写个测试类模拟客户端注册: 首先启动/getConfig接口所在的服务,然后启动客户端,客户端就会发起注册请求,如果有修改直接获取到结果,进行配置的更新操作。如果无修改,请求会挂起,这边客户端设置的读取超时时间是90秒,大于服务端的60秒超时时间。 每次收到结果后,无论是有修改还是没修改,都必须重新进行注册,通过这样的方式就可以达到配置实时推送的效果。 我们可以调用之前写的/addMsg接口来模拟配置发生变化,调用之后客户端就能马上得到返回结果。 本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。 文章来源:微信公众号 华章计算机
导读:本文主要介绍了机器视觉的主要应用场景,目前绝大部分数字信息都是以图片或视频的形式存在的,若要对这些信息进行有效分析利用,则要依赖于机器视觉技术的发展,虽然目前已有的技术已经能够解决很多问题,但离解决所有问题还很遥远,因此机器视觉的应用前景还是非常广阔的。 我们热切地期盼更多的读者投身到该领域,与我们一起探索图像数据的无尽潜力。 作者:魏溪含 涂铭 张修鹏 如需转载请联系大数据(ID:hzdashuju) ▲图1-1 人工智能相关领域关系图 00 什么是机器视觉? 机器视觉是人工智能的一个重要分支,其核心是使用“机器眼”来代替人眼。机器视觉系统通过图像/视频采集装置,将采集到的图像/视频输入到视觉算法中进行计算,最终得到人类需要的信息。这里提到的视觉算法有很多种,例如,传统的图像处理方法以及近些年的深度学习方法等。 图1-2a展示了一个由彩色图像组成的、分类的数据集Cifar10,其中有飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车10个类别,且每个类别中都有1000张32×32的彩色图片。图1-2b展示的是不同算法在Cifar10数据集上的分类效果。 ▲图1-2a Cifar10数据集展示 ▲图1-2b 传统图像处理方法与深度学习方法在Cifar10数据集上的效果对比 从中我们可以看出,在深度学习出现以前,传统的图像处理和机器学习方法并不能很好地完成这样一个简单的分类任务,而深度学习的出现使得机器有了达到人类水平的可能。事实上,AlphaGo的出现已经证明了在一些领域,机器有了超越人类的能力。 由于深度学习技术的发展、计算能力的提升和视觉数据的增长,视觉智能计算技术在不少应用当中都取得了令人瞩目的成绩。 图像视频的识别、检测、分割、生成、超分辨、captioning、搜索等经典和新生的问题纷纷取得了不小的突破。这些技术正广泛应用于城市治理、金融、工业、互联网等领域。 以下将以9个场景为例,对一些常见的应用场景进行介绍,让读者直观地理解机器视觉都能解决哪些问题。 01 人脸识别 人脸识别(Face Recognition)是基于人的面部特征信息进行身份识别的一种生物识别技术。它通过采集含有人脸的图片或视频流,并在图片中自动检测和跟踪人脸,进而对检测到的人脸进行面部识别。人脸识别可提供图像或视频中的人脸检测定位、人脸属性识别、人脸比对、活体检测等功能。 人脸识别是机器视觉最成熟、最热门的领域,近几年,人脸识别已经逐步超过指纹识别成为生物识别的主导技术。人脸识别分为4个处理过程——人脸图像采集及检测、人脸图像预处理、人脸图像特征提取以及匹配与识别,其主要应用及说明如下: 人脸支付:将人脸与用户的支付渠道绑定,支付阶段即可刷脸付款,无须出示银行卡、手机等,提高支付效率(如图1-3) 人脸开卡:客户在银行等部门开卡时,可通过身份证和人脸识别进行身份校验,以防止借用身份证进行开卡 人脸登录:用户注册阶段录入人脸图片,在安全性要求较高的场景中启动人脸登录验证,以提高安全性 VIP人脸识别:通过人脸识别自动确定客户的身份,提供差异化服务 人脸签到:活动开始前录入人脸图片,活动当天即可通过刷脸进行签到,提高签到效率 人脸考勤:利用高精度的人脸识别、比对能力,搭建考勤系统,提升考勤效率,提高防作弊能力(如图1-3所示) 人脸闸机:在机场、铁路、海关等场合利用人脸识别确定乘客身份 会员识别:会员到店无须出示会员凭证,只要刷脸即可完成会员身份验证,实现无卡化身份确认和人流统计 安防监控:在银行、机场、商场、市场等人流密集的公共场所对人群进行监控,实现人流自动统计、特定人物的自动识别和追踪 相册分类:通过人脸检测,自动识别照片库中的人物角色,并进行分类管理,提升产品的用户体验 人脸美颜:基于人脸检测和关键点识别,实现人脸的特效美颜、特效相机、贴片等互动娱乐功能 ▲图1-3 人脸识别应用场景 由于人脸识别产业的需求旺盛,众多大型科技公司和人工智能创业公司均有涉足该领域,目前该技术已经处于大规模商用阶段,未来3~5年仍将继续保持高速增长。 02 视频监控分析 视频监控分析是利用机器视觉技术对视频中的特定内容信息进行快速检索、查询、分析的技术。由于摄像头的广泛应用,由其产生的视频数据已是一个天文数字,这些数据蕴藏的价值巨大,靠人工根本无法统计,而机器视觉技术的逐步成熟,使得视频分析成为可能。 通过这项技术,公安部门可以在海量的监控视频中搜寻到罪犯;在拥有大量流动人群的交通领域,该技术也被广泛应用于人群分析、防控预警等。 城市治理是视频监控分析应用价值最高的领域之一,以下列举了一些典型的应用场景及说明: 交通拥堵治理:视频分析技术可用于进行车辆检测、车型识别、车牌识别、非机动车检测、行人检测、红绿灯识别、车辆排队长度、车辆通行速度、拥堵程度判断分析。 识别、分析这些信息可用于实现交通态势预测和红绿灯优化配置,从而缓解交通拥堵指数,加快车辆通行速度,提升城市运行效率 异常事件检测与轨迹跟踪:视频分析技术可用于检测拥堵、逆行、违法停车、缓行、抛锚、事故、快速路上的行人和非机动车、路面抛洒物、路口行人大量聚集等异常交通事件的发生(如图1-4)。 根据这些信息,一方面可以实时报警,由交警介入处理;另一方面,视频索引可以实现高效的以图搜图查询,通过车辆轨迹跟踪保留证据,实现非现场执法,可以节省大量警力,并提升交通管理的效率 平安城市情报搜集分析:视频分析技术可用于视频中动态人脸和基础人脸的实时比对,人群密度和不同方向人群流量的分析,智能研判、自动预警重点人员、重点车辆、重点物品在重点时间段出现在重点区域的有效线索,实现基于视频数据的案件串并与动态人员管控,为嫌疑人建立地理画像模型,提高主动防御、精确布控的水平,从海量视频中追踪罪犯成为可能 厂区安全管理:视频分析技术可用于对厂区人员是否戴安全帽,是否在安全区域作业等安全管理问题进行分析,此技术还可应用于其他有安全管控需求的区域,如矿山安全管理、仓库管理等 门店客流分析:在商场或门店部署摄像装置,利用视频分析技术,可实现识别顾客身份、分析顾客行为、指导导购人员进行精准推荐、监控顾客异常行为等功能 ▲图1-4 交通异常事件监测 视频/监控领域盈利空间广阔,商业模式多种多样,将视觉分析技术应用于视频监控领域正在形成一种趋势,目前已率先应用于交通、安防、零售、社区、楼宇、校园、工地等场合。 03 工业瑕疵检测 机器视觉技术可以快速获取大量信息,并进行自动处理。在自动化生产过程中,人们将机器视觉系统广泛应用于工业瑕疵诊断、工况监视和质量控制等领域。 工业瑕疵诊断是指利用传感器(如工业相机、X光等)将工业产品内外部的瑕疵进行成像,通过机器学习技术对这些瑕疵图片进行识别(如图1-5),确定瑕疵的种类、位置,甚至对瑕疵产生的原因进行分析的一项技术。目前,工业瑕疵诊断已成为机器视觉的一个非常重要的应用领域。 ▲图1-5 工业瑕疵诊断应用场景 随着制造业向智能化、无人化方向发展,以及人工成本的逐年上升,广泛存在于制造业的产品外观检测迫切需要通过机器视觉技术替代人工外检人员。 一方面图像外检技术可以运用到一些危险环境和人工视觉难以满足要求的场合;另一方面,更重要的是,人工检测面临检测速度慢、检测准确率不稳定(随着人眼检测时间的增加,检测准确率明显下降)、不同质检员的检测水平不一致的情况,同时,质检员的责任心、状态也会影响检测水平,这些都会直接影响产品的品质。 而图像外检技术可以大大提高生产效率、速度和生产的自动化程度,降低人工成本。 04 图片识别分析 这里所说的图片识别是指人脸识别之外的静态图片识别,图片识别可应用于多种场景,目前应用比较多的是以图搜图、物体/场景识别、车型识别、人物属性、服装、时尚分析、鉴黄、货架扫描识别、农作物病虫害识别等。 这里列举一个图像搜索的例子:拍立淘。拍立淘是手机淘宝的一个应用,主要通过图片来代替文字进行搜索,以帮助用户搜索无法用简单文字描述的需求。 比如,你看到一条裙子很好看,但又很难用简单的语言文字来描述这条裙子的样子,那么这个时候就可以使用拍立淘,通过图片轻松地在淘宝上搜出同款裙子,或者是与它非常接近的款式,如图1-6所示。 ▲图1-6 图片识别应用效果 05 自动驾驶/驾驶辅助 自动驾驶汽车是一种通过计算机实现无人驾驶的智能汽车,它依靠人工智能、机器视觉、雷达、监控装置和全球定位系统协同合作,让计算机可以在没有任何人类主动操作的情况下,自动安全地操作机动车辆(如图1-7)。机器视觉的快速发展促进了自动驾驶技术的成熟,使无人驾驶在未来成为可能。 ▲图1-7 自动驾驶汽车应用场景 自动驾驶技术链比较长,主要包含感知阶段、规划阶段和控制阶段三个部分。机器视觉技术主要应用在无人驾驶的感知阶段,其基本原理可概括如下。 使用机器视觉获取场景中的深度信息,以帮助进行后续的图像语义理解,在自动驾驶中帮助探索可行驶区域和目标障碍物。 通过视频预估每一个像素的运动方向和运动速度。 对物体进行检测与追踪。在无人驾驶中,检测与追踪的目标主要是各种车辆、行人、非机动车。 对于整个场景的理解。最重要的有两点,第一是道路线检测,其次是在道路线检测下更进一步,即将场景中的每一个像素都打成标签,这也称为场景分割或场景解析。 同步地图构建和定位技术。 06 三维图像视觉 三维图像视觉主要是对三维物体进行识别,其主要应用于三维机器视觉、双目立体视觉、三维重建、三维扫描、三维测绘、三维视觉测量、工业仿真等领域。三维信息相比二维信息,能够更全面、真实地反映客观物体,提供更大的信息量。 近年来,三维图像视觉已经成为计算机视觉领域的重要课题,在虚拟现实、文物保护、机械加工、影视特技制作、计算机仿真、服装设计、科研、医学诊断、工程设计、刑事侦查现场痕迹分析、自动在线检测、质量控制、机器人及许多生产过程中得到越来越广泛的应用。 07 医疗影像诊断 医疗数据中有90%以上的数据来自于医疗影像。医疗影像领域拥有孕育深度学习的海量数据,医疗影像诊断可以辅助医生做出判断(如图1-8),提升医生的诊断效率。目前,医疗影像诊断主要应用于如下场景中: 肿瘤探测:通过图像技术,医疗影像诊断可进行如皮肤色素瘤、乳腺癌、肺部癌变的早期识别 肿瘤发展追踪:机器视觉技术可以根据器官组织的分布,预测出肿瘤扩散到不同部位的概率,并能从图片中获取癌变组织的形状、位置、浓度等信息 血液量化与可视化:通过核磁共振图像,医疗影像诊断可以更有效地再现心脏内部血液的流量变化,并可探测心脏是否发生病变 病理解读:不同医生对于同一张图片的理解可能会有不同,机器视觉技术可用于解读图片,并向医生提供较为全面的报告,使医生能够了解到多种不同的病理可能性 糖尿病视网膜病变检测:由糖尿病导致的视网膜病变是失明的一大主因,而早期治疗可以有效减缓这一症状。机器视觉技术可以辨认出患者是否处于糖尿病视网膜病变早期,并能根据图片像素判断病情的发展程度 图1-8是肝脏及结节分割技术的影像分析结果。 ▲图1-8 肝脏及结节分割技术,从左至右:CT原始影像、真实结果、算法结果 08 文字识别 计算机文字识别,俗称光学字符识别(Optical Character Recognition),是利用光学扫描技术将票据、报刊、书籍、文稿及其他印刷品的文字转化为图像信息,再利用文字识别技术将图像信息转化为可以使用的计算机输入技术。该技术可应用于如下场景中: 卡证类识别:如身份证、名片、行驶证、驾驶证、银行卡、营业执照、户口本、签证、房产证等证件类文字识别 票据类识别:定额发票、火车票、飞机票、出租车票等票据类文字识别 出版类识别:书籍、报刊等印刷物的识别 实体标识识别:道路指示牌识别(如图1-9)、广告牌识别等 ▲图1-9 文字识别技术的应用场景 09 图像/视频的生成及设计 人工智能技术不仅可以对现有的图片、视频进行分析、编辑,还可以进行再创造。机器视觉技术可以快速、批量、自动化地进行图片设计,因此其可为企业大幅度节省设计人力成本。 人工智能可以从艺术作品中抽象出视觉模式,然后将这些模式应用于具有该作品的标志性特征的摄影图像的幻想再现。这些算法还可以将任何粗糙的涂鸦转换成令人印象深刻的绘画,看起来就像是由描绘真实世界模型的专家级人类艺术家创建的一样。 人工智能技术可以手绘人脸的草图,并通过算法将其转化为逼真的图像;还可以指导计算机渲染任何图像,使其看起来好像是由特定人类艺术家以特定风格创作的一样;甚至可以对任何图像、图案图形和其他不在源头中的细节化腐朽为神奇。 关于作者:魏溪含 ,爱丁堡大学人工智能硕士,阿里巴巴达摩院算法专家,在计算机视觉、大数据领域有8年以上的算法架构和研发经验。 涂铭,阿里巴巴数据架构师,对大数据、自然语言处理、图像识别、Python、Java相关技术有深入的研究,积累了丰富的实践经验。 张修鹏,毕业于中南大学,阿里巴巴技术发展专家,长期从事云计算、大数据、人工智能与物联网技术的商业化应用,在阿里巴巴首次将图像识别技术引入工业,并推动图像识别产品化、平台化。 本文摘编自《深度学习与图像识别:原理与实践》,经出版方授权发布。 文章来源:微信公众号 大数据
作者:尹吉欢 来源:猿天地 1. /routes 端点 当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露一个路由管理端点/routes。 借助这个端点,可以方便、直观地查看以及管理Zuul的路由。 将所有端点都暴露出来,增加下面的配置: management.endpoints.web.exposure.include=* 访问 http://localhost:2103/actuator/routes 可以显示所有路由信息: { "/cxytiandi/**" : "http://cxytiandi.com", "/hystrix-api/**": "hystrix-feign-demo""/api/**": "forward:/local", "/hystrix-feign-demo/**": "hystrix-feign-demo" } 2. /filters 端点 /fliters端点会返回Zuul中所有过滤器的信息。可以清楚的了解Zuul中目前有哪些过滤器,哪些被禁用了等详细信息。 访问 http://localhost:2103/actuator/filters 可以显示所有过滤器信息: 文件上传 创建一个新的Maven项目zuul-file-demo,编写一个文件上传的接口,如代码清单7-20所示。 代码清单 7-20 文件上传接口 将服务注册到Eureka中,服务名称为zuul-file-demo,通过PostMan来上传文件,如图7-4所示 可以看到接口正常返回了文件上传之后的路径,接下来我们换一个大一点的文件,文件大小为1.7MB。 可以看到报错了(如图7-5所示),通过Zuul上传文件,如果超过1M需要配置上传文件的大小, Zuul和上传的服务都要加上配置: spring.servlet.multipart.max-file-size= 1000Mb spring.servlet.multipart.max-request-size= 1000Mb 配置加完后重新上传就可以成功了,如图7-6所示。 第二种解决办法是在网关的请求地址前面加上/zuul,就可以绕过Spring DispatcherServlet进行上传大文件。 # 正常的地址 http://localhost:2103/zuul-file-demo/file/upload # 绕过的地址 http://localhost:2103/zuul/zuul-file-demo/file/upload 通过加上/zuul前缀可以让Zuul服务不用配置文件上传大小,但是接收文件的服务还是需要配置文件上传大小,否则文件还是会上传失败。 在上传大文件的时候,时间比较会比较长,这个时候需要设置合理的超时时间来避免超时。 在Hystrix隔离模式为线程下zuul.ribbon-isolation-strategy=thread,需要设置Hystrix超时时间。 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000 4. 请求响应信息输出 系统在生产环境出现问题时,排查问题最好的方式就是查看日志了,日志的记录尽量详细,这样你才能快速定位问题。 下面带大家学习如何在Zuul中输出请求响应的信息来辅助我们解决一些问题。 熟悉Zuul的朋友都知道,Zuul中有4种类型过滤器,每种都有特定的使用场景,要想记录响应数据,那么必须是在请求路由到了具体的服务之后,返回了才有数据,这种需求就适合用post过滤器来实现了。如代码清单7-21所示。 代码清单 7-21 Zull获取请求信息 输出效果如下: 获取响应内容第一种方式,如代码清单7-22所示。 代码清单 7-22 获取响应内容(一) 获取响应内容第二种方式,如代码清单7-23所示。 代码清单 7-23 获取响应内容(二) 为什么上面两种方式可以取到响应内容? 在RibbonRoutingFilter或者SimpleHostRoutingFilter中可以看到下面一段代码,如代码清单7-24所示。 代码清单 7-24 响应内容获取源码 forward()方法对服务调用,拿到响应结果,通过setResponse()方法进行响应的设置,如代码清单7-25所示。 代码清单 7-25 setResponse(一) 上面第一行代码就可以解释我们的第一种获取的方法,这边直接把响应内容加到了RequestContext中。 第二种方式的解释就在helper.setResponse的逻辑里面了,如代码清单7-26所示。 代码清单 7-26 setResponse(二) 5. Zuul自带的Debug功能 Zuul中自带了一个DebugFilter,一开始我也没明白这个DebugFilter有什么用,看名称很容易理解,用来调试的,可是你看它源码几乎没什么逻辑,就set了两个值而已,如代码清单7-27所示。 代码清单 7-27 DebugFilter run方法 要想让这个过滤器执行就得研究下它的shouldFilter()方法,如代码清单7-28所示。 代码清单 7-28 DebugFilter shouldFilter 方法 只要满足两个条件中的任何一个就可以开启这个过滤器,第一个条件是请求参数中带了某个参数=true就可以开启,这个参数名是通过下面的代码获取的,如代码清单7-29所示。 代码清单 7-29 DebugFilter启用参数(一) DynamicStringProperty是Netflix的配置管理框架Archaius提供的API,可以从配置中心获取配置,由于Netflix没有开源Archaius的服务端,所以这边用的就是默认值debug,如果大家想动态去获取这个值的话可以用携程开源的Apollo来对接Archaius,这个在后面章节给大家讲解。 可以在请求地址后面追加debug=true来开启这个过滤器,参数名称debug也可以在配置文件中进行覆盖,用zuul.debug.parameter指定,否则就是从Archaius中获取,没有对接Archaius那就是默认值debug。 第二个条件代码,如代码清单7-30所示。 代码清单 7-30 DebugFilter启用参数(二) 是通过配置zuul.debug.request来决定的,可以在配置文件中配置zuul.debug.request=true开启DebugFilter过滤器。 DebugFilter过滤器开启后,并没什么效果,在run方法中只是设置了DebugRouting和DebugRequest两个值为true,于是继续看源码,发现在很多地方有这么一段代码,比如com.netflix.zuul.FilterProcessor.runFilters(String)中,如代码清单7-31所示。 代码清单 7-31 Debug信息添加 当debugRouting为true的时候就会添加一些Debug信息到RequestContext中。现在明白了DebugFilter中为什么要设置DebugRouting和DebugRequest两个值为true。 到这步后发现还是很迷茫,一般我们调试信息的话肯定是用日志输出来的,日志级别就是Debug,但这个Debug信息只是累加起来存储到RequestContext中,没有对使用者展示。 继续看代码吧,功夫不负有心人,在org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.addResponseHeaders()这段代码中看到了希望。如代码清单7-32所示。 代码清单 7-32 Debug信息设置响应 核心代码在于this.zuulProperties.isIncludeDebugHeader(),只有满足这个条件才会把RequestContext中的调试信息作为响应头输出,在配置文件中增加下面的配置即可: zuul.include-debug-header=true 最后在请求的响应头中可以看到调试内容,如图7-7所示。 本文摘自于 《Spring Cloud微服务 入门 实战与进阶》 一书。 文章来源:微信公众号 华章计算机
本文选自 《微服务架构设计模式》 一书。 在微服务架构中编写查询非常具有挑战性。查询通常需要检索分散在多个服务所拥有的数据库中的数据。但是,你不能使用传统的分布式查询处理机制,因为即使技术上可行,它也会打破服务之间的隔离和封装。下面将介绍一种在微服务架构中实现查询操作的最简单方法——API组合模式。 什么是 API 组合模式 这个模式通过调用拥有数据的服务并组合结果来实现查询操作。下图显示了该模式的结构。它有两种类型的参与者: API组合器:它通过查询数据提供方的服务来实现查询操作。 数据提供方服务:拥有查询返回的部分数据的服务。 上图显示了三个提供方服务。API 组合器通过从提供方服务检索数据并组合结果来实现查询。API 组合器可能是需要数据呈现网页的客户端,例如 Web 应用程序。或者,它可能是一个服务,例如 API Gateway 及后端前置模式 ,这个模式将查询操作公开为 API 接口。 是否可以使用此模式实现特定查询操作取决于几个因素,包括数据的分区方式、拥有数据的服务公开的 API 的功能,以及服务使用数据库的功能,等等。例如,即使提供方服务拥有用于检索所需数据的 API,聚合器也可能需要执行大量数据集的低效内存连接。稍后,你将看到使用此模式无法实现查询操作的例子。但幸运的是,有许多场景可以应用这种模式。为了看到它的实际效果,我们来举个例子。 使用 API 组合模式实现 findOrder() 查询操作 findOrder() 查询操作相当于一个简单的基于主键的 equi-join 查询。可以假设每个提供方服务都有一个 API接口,用于通过 orderId 检索所需的数据。因此,采用 API 组合模式来实现 findOrder() 查询操作似乎是合情合理的。API 组合器调用四个服务并将结果组合在一起。下图显示了Find Order Composer的设计。 显而易见,API 组合模式非常简单。让我们看一下在应用此模式时必须解决的几个设计问题。 API 组合模式的设计缺陷 使用此模式时,你必须解决两个设计问题: 确定架构中的哪个组件是查询操作的 API 组合器。 如何编写有效的聚合逻辑。 让我们看看每个问题。 由谁来担任 API 组合器的角色 这是你必须做出的一个决定,选择由谁来扮演查询操作的 API 组合器这个角色。你有三个选择。第一个选择,如下图所示,是由服务的客户端扮演 API 组合器的角色。 实现 Order Status 视图并在同一局域网上运行的前端客户端(如 Web 应用程序)可以使用此模式有效地检索订单详细信息。对于防火墙之外的客户以及通过较慢网络访问的服务,此选择可能不实用。 第二个选择如下图所示,由实现应用程序外部 API 的 API Gateway 来扮演 API 组合器的角色,用来完成查询操作和查询结果的组合。 如果查询操作是应用程序外部 API 的一部分,则此选择有意义。API Gateway 不是将请求路由到另一个服务,而是实现 API 组合逻辑。这种方法使得在防火墙外运行的客户端(例如移动设备)能够通过单个 API 调用有效地从众多服务中检索数据。 第三个选择如下图所示,是将 API 组合器实现为独立的服务。 此选择可以用于由多个服务在内部使用的查询操作。此操作还可用于外部可访问的查询操作,由于它们的聚合逻辑过于复杂,因此无法在 API Gateway 中完成查询,必须使用单独的服务。 API 组合器应该使用响应式编程模型 在开发分布式系统时,我们一直努力降低服务之间的延迟。API 组合器应尽可能地并行调用提供方服务,最大限度地缩短查询操作的响应时间。例如,Find Order Aggregator 应该同时调用这四个服务,因为调用之间没有依赖关系。但有时,API 组合器需要一个提供方服务的结果才能调用另一个服务。在这种情况下,它需要按顺序调用一部分(但希望不是全部)提供方服务。 高效执行顺序和并行服务调用混合的逻辑可能很复杂。为了使 API 组合器达到较高的可维护性、性能和可扩展性,它应该使用基于 Java CompletableFuture、RxJava 可观测或其他类似的响应式设计。我将在后面讲 API Gateway 模式的时候再深入讨论这个主题。 API 组合模式的好处和弊端 此模式是在微服务架构中实现查询操作的简单直观方式。但它也有一些缺点: 增加了额外的开销。 带来可用性降低的风险。 缺乏事务数据一致性。 我们来逐一分析。 增加了额外的开销 这种模式的一个缺点是它需要调用多个服务和查询多个数据库,这带来了额外的开销。在单体应用程序中,客户端可以使用单个请求检索数据,这通常会执行单个数据库查询。相比之下,使用 API 组合模式会涉及多个请求和多个数据库查询。因此,它需要更多计算和网络资源,运行应用程序的成本也相应增加。 带来可用性降低的风险 这种模式的另一个缺点是导致可用性降低。如第 3 章所述,操作的可用性随着所涉及的服务数量而下降。因为查询操作的实现涉及至少三个服务:API 组合器和至少两个提供方服务,其可用性将显著小于单个服务的可用性。例如,如果单个服务的可用性为 99.5%,则调用四个提供方服务的 findOrder() 接口的可用性为 99.5%(4+1)=97.5% ! 你可以使用几种策略来提高可用性。一种策略是 API 组合器在提供方服务不可用时,返回先前缓存的数据。API 组合器有时会缓存提供方服务返回的数据,以提高性能。它还可以使用此缓存来提高可用性。如果提供方服务不可用,则 API 组合器可以从缓存中返回数据,尽管这些缓存数据可能是过时的。 另一种提高可用性的策略是让 API 组合器返回不完整的数据。例如,假设KitchenService 暂时不可用。findOrder() 查询操作的 API 组合器可以从响应中省略该服务的数据,因为用户界面仍然可以显示其他有用的信息。你将在后面一章中看到有关 API 设计、缓存和可靠性的更多详细信息。 缺乏事务数据一致性 API 组合模式的另一个缺点是缺乏数据一致性。单体应用程序通常使用一个数据库事务执行查询操作。ACID 事务受制于隔离级别的约束,可以确保应用程序具有一致的数据视图,即使它执行多个数据库查询。相反,API 组合模式则是针对多个数据库执行查询。这种方式存在一种风险,即查询操作将返回不一致的数据。 例如,从 Order Service 检索的 Order 可能处于 CANCELED 状态,而从 Kitchen Service 检索的相应 Ticket 可能尚未取消。API 组合器必须解决这种差异,因为这会增加代码复杂性。更糟糕的是,API组合器可能无法总是检测到不一致的数据,并将其返回给客户端。尽管存在这些缺点,API 组合模式还是非常有用的。你可以使用它来实现许多查询操作。但是有一些查询操作无法使用此模式有效实现。 以上内容摘自《微服务架构设计模式》一书。 文章来源:微信公众号 江南一点雨
作者:克里斯·理查森 译者:喻勇 来源:《微服务架构设计模式》经出版社授权发布 导读:微服务在最近几年大行其道,很多公司的研发人员都在考虑微服务架构,或者在做微服务的路上,拆分服务是个很热的话题。那么我们应该按照什么原则将现有的业务进行拆分?是否拆分得越细就越好?本文将研究把应用程序分解为服务的策略和指南、分解的障碍以及如何解决它们。 01 服务拆分策略 1. 根据业务能力进行服务拆分和定义 创建微服务架构的策略之一就是采用业务能力进行服务拆分。业务能力是一个来自于业 务架构建模的术语。业务能力是指一些能够为公司(或组织)产生价值的商业活动。特定业务的业务能力取决于这个业务的类型。例如,保险公司业务能力通常包括承保、理赔管理、 账务和合规等。在线商店的业务能力包括:订单管理、库存管理和发货,等等。 识别业务能力 组织的业务能力通常是指这个组织的业务是做什么,它们通常都是稳定的。与之相反,组织采用何种方式来实现它的业务能力,是随着时间不断变化的。这一个组织有哪些业务能力,是通过对组织的目标、结构和商业流程的分析得来的。每一 个业务能力都可以被认为是一个服务,除非它是面向业务的而非面向技术的。业务能力规范 包含多项元素,比如输入和输出、服务等级协议(SLA)。例如,保险承保能力的输入来自客 户的应用程序,这个业务能力的输出是完成核保并报价。 业务能力通常集中在特定的业务对象上。例如,理赔业务对象是理赔管理功能的重点。 能力通常可以分解为子能力。例如,理赔管理能力具有多个子能力,包括理赔信息管理、理赔审核和理赔付款管理。 把 FTGO 的业务能力逐一列出来似乎也并不太困难,如下所示。 供应商管理。 Courier management:送餐员相关信息管理; Restaurant information management:餐馆菜单和其他信息管理,例如营业地址和时间。 用户管理:用户有关信息的管理。 订单获取和履行。 Order management:让用户可以创建和管理订单。 Restaurant order management:让餐馆可以管理订单的生产过程。 物流。 Courier availability management:管理送餐员的实时状态。 Delivery management:把订单送到用户手中。 会计记账。 Consumer accounting:管理跟用户相关的会计记账。 Restaurant accounting:管理跟餐馆相关的会计记账。 Courier accounting:管理跟送餐员相关的会计记账。 其他 顶级能力包括供应商管理、用户(消费者)管理、订单获取和履行以及会计记账。可能 还有许多其他顶级能力,包括与营销相关的能力。大多数顶级能力都会分解为子能力。例 如,订单获取和履行被分解为五个子能力。 这个能力层次的有趣方面是有三个餐馆相关的功力:餐馆信息管理、餐馆订单管理和餐 馆会计记账。那是因为它们代表了餐馆运营的三个截然不同的方面。 接下来,我们将了解如何使用业务能力来定义服务。 从业务能力到服务 一旦确定了业务能力,就可以为每个能力或相关能力组定义服务。图1显示了 FTGO 应用程序从能力到服务的映射。决定将哪个级别的能力层次结构映射到服务是一个非常主观的判断。我对这种特定映射的理由如下: 我将供应商管理的子能力映射到两种服务,因为餐馆和送餐员是非常不同类型的供应商。 我将订单获取和履行能力映射到三个服务,每个服务负责流程的不同阶段。我将送餐员可用性管理(Courier availability management)和交付管理(Delivery management)能力结合起来,并将它们映射到单个服务,因为它们交织在一起。 我将会计记账能力映射到自己的独立服务,因为不同类型的会计记账看起来很相似。 图1 将 FTGO 业务能力映射到服务。能力层次结构各个级别的能力都映射到服务 围绕能力组织服务的一个关键好处是,因为它们是稳定的,所以最终的架构也将相对稳定。架构的各个组件可能会随着业务的具体实现方式的变化而发展,但架构仍保持不变。 话虽如此,重要的是要记住图1 中显示的服务仅仅是定义架构的第一次尝试。随着我 们对应用程序领域的了解越来越多,它们可能会随着时间的推移而变化,特别是架构定义流 程中的一个重要步骤是调查服务如何在每个关键架构服务中协作。例如,你可能会发现由于过多的进程间通信而导致特定的分解效率低下,导致你必须把一些服务组合在一起。相反, 服务可能会在复杂性方面增长到值得将其拆分为多个服务的程度。图2 展示了子域和服务之间的映射,每一个子域都有属于它们自己的领域模型。 图2从子域到服务,FTGO 应用程序域的每个子域都映射为一个服务,该服务有自己的领域模型 DDD 和微服务架构简直就是天生一对。DDD 的子域和限界上下文的概念,可以很好地跟微服务架构中的服务进行匹配。而且,微服务架构中的自治化团队负责服务开发的概念,也跟 DDD 中每个领域模型都由一个独立团队负责开发的概念吻合。更有趣的是,子域用于它自己的领域模型这个概念,为消除上帝类和优化服务拆分提供了好办法。 2. 根据子域进行拆分 领域驱动为每一个子域定义单独的领域模型。子域是领域的一部分,领域是 DDD 中用来描述应用程序问题域的一个术语。识别子域的方式跟识别业务能力一样:分析业务并识别业务的不同专业领域,分析产出的子域定义结果也会跟业务能力非常接近。FTGO 的子域包括:订单获取、订单管理、餐馆管理、送餐和会计。正如你所见:这些子域跟我们之前定义的业务能力非常接近。 DDD 把领域模型的边界称为限界上下文(bounded contest)。限界上下文包括实现这个模型的代码集合。当使用微服务架构时,每一个限界上下文对应一个或者一组服务。换一种说法,我们可以通过 DDD 的方式定义子域,并把子域对应为每一个服务,这样就完成了微服务架构的设计工作。 按子域分解和按业务能力分解是定义应用程序的微服务架构的两种主要模式。但是,也有一些有用的拆分指导原则源于面向对象的设计。我们来详细讨论这些原则。 02 拆分的指导原则 单一职责原则 软件架构和设计的主要目标之一是确定每个软件元素的职责。单一职责原则如下: 改变一个类应该只有一个理由。 —Robert C. Martin 类所承载的每一个职责都是对它进行修改的潜在原因。如果一个类承载了多个职责,并且互相之间的修改是独立的,那么这个类就会变得非常不稳定。遵照 SRP 原则,你所定义的每一个类都应该只有一个职责,因此也就只有一个理由对它进行修改。 我们在设计微服务架构时应该遵循 SRP 原则,设计小的、内聚的、仅仅含有单一职责的服务。这会缩小服务的大小并提升它的稳定性。新的FTGO架构是应用SRP的一个例子。 为客户获取餐食的每一个方面(订单获取、订单准备、送餐等)都由一个单一的服务承载。 闭包原则 另外一个有用的原则是闭包原则(CCP): 在包中包含的所有类应该是对同类的变化的一个集合,也就是说,如果对包做出修改,需要调整的类应该都在这个包之内。 —— Robert C. Martin 这就意味着,如果由于某些原因,两个类的修改必须耦合先后发生,那么就应该把它们 放在同一个包内。也许,这些类实现了一些特定的业务规则的不同方面。这样做的目标是当 业务规则发生变化时,开发者只需要对一个交付包做出修改,而不是大规模地修改(和重新 编译)整个应用采用闭包原则,极大地改善了应用程序的可维护性。 在微服务架构下采用 CCP原则,这样我们就能把根据同样原因进行变化的服务放在一 个组件内。这样做可以控制服务的数量,当需求发生变化时,变更和部署也更加容易。理想 情况下,一个变更只会影响一个团队和一个服务。CCP 是解决分布式单体这种可怕的反模式的法宝。 单一职责原则和闭包原则是 Bob Martin 制定的十一项原则中的两项。它们在开发微服务架构时特别有用。在设计类和包时可以使用其余的九个原则。有关这些原则的更多信息,请参阅 Bob Martin 网站上的文章《面向对象设计的原 则》 (http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod) 03 拆分单体应用为服务的难点及解决方案 从表面上看,通过定义与业务能力或子域相对应的服务来创建微服务架构的策略看起来很简单。但是,你可能会遇到几个障碍: 网络延迟。 同步进程间通信导致可用性降低。 在服务之间维持数据一致性。 获取一致的数据视图。 上帝类阻碍了拆分。 网络延迟 网络延迟是分布式系统中一直存在的问题。你可能会发现,对服务的特定分解会导致两个服务之间的大量往返调用。有时,你可以通过实施批处理API在一次往返中获取多个对象,从而将延迟减少到可接受的数量。但在其他情况下,解决方案是把多个相关的服务组合在一起,用编程语言的函数调用替换昂贵的进程间通信。 同步的进程间通信导致可用性降低 另一个需要考虑的问题是如何处理进程间通信而不降低系统的可用性。例如,实现createOrder() 操作最常见的方式是让Order Service使用REST同步调用其他服务。这样做的弊端是REST这样的协议会降低Order Service的可用性。如果任何一个被调用的服务处在不可用的状态,那么订单就无法创建了。有时候这可能是一个不得已的折中,但是在学习异步消息之后,你就会发现其实有更好的办法来消除这类同步调用产生的紧耦合并提升可用性。 在服务之间维持数据一致性 另一个挑战是如何在某些系统操作需要更新多个服务中的数据时,仍旧维护服务之间的数据一致性。传统的解决方案是使用基于两阶段提交(two phase commit)的分布式事务管理机制。但对于现今的应用程序而言,这不是一个好的选择,你必须使用一种非常不同的方法来处理事务管理,这就是Saga。Saga是一系列使用消息协作的本地事务。Saga 比传统的ACID事务更复杂,但它们在许多情况下都能工作得很好。Saga 的一个限制是它们最终是一致的。如果你需要以原子方式更新某些数据,那么它必须位于单个服务中,这可能是分解的障碍。 获取一致的数据视图 分解的另一个障碍是无法跨多个数据库获得真正一致的数据视图。在单体应用程序中,ACID 事务的属性保证查询将返回数据库的一致视图。相反,在微服务架构中,即使每个服务的数据库是一致的,你也无法获得全局一致的数据视图。如果你需要一些数据的一致视图,那么它必须驻留在单个服务中,这也是服务分解所面临的问题。幸运的是,在实践中这很少带来真正的问题。 上帝类阻碍了拆分 分解的另一个障碍是存在所谓的上帝类。上帝类是在整个应用程序中使用的全局类上帝类通常为应用程序的许多不同方面实现业务逻辑。它有大量字段映射到具有许多列的数据库表。大多数应用程序至少有一个这样的上帝类,每个类代表一个对领域至关重要的概念:银行账户、电子商务订单、保险政策,等等。因为上帝类将应用程序的许多不同方面的状态和行为捆绑在一起,所以将使用它的任何业务逻辑拆分为服务往往都是一个不可逾越的障碍。 Order类是FTGO应用程序中上帝类的一个很好的例子。Order 类具有与订单处理、餐馆订单管理、送餐和付款相对应的字段及方法。由于一个模型必须描述来自应用程序的不同部分的状态转换,因此该类还具有复杂的状态模型。在目前情况下,这个类的存在使得将代码分割成服务变得极其困难。 一种解决方案是将 Order 类打包到库中并创建一个中央Order数据库。处理订单的所有服务都使用此库并访问访问数据库。这种方法的问题在于它违反了微服务架构的一个关键原则,并导致我们特别不愿意看到的紧耦合。例如,对Order模式的任何更改都要求其他开发团队同步更新和重新编译他们的代码。 另一种解决方案是将Order数据库封装在Order Service中,该服务由其他服务调用以检索和更新订单。该设计的问题在于这样的一个Order Service将成为一个纯数据服务,成为包含很少或没有业务逻辑的贫血领域模型(anaemic domain model)。这两种解决方案都没有吸引力,但幸运的是,DDD 提供了一个好的解决方案。 更好的方法是应用DDD并将每个服务视为具有自己的领域模型的单独子域。这意味着FTGO应用程序中与订单有关的每个服务都有自己的领域模型及其对应的 Order 类的版本。Delivery Service是多领域模型的一个很好的例子。如图 3所示为Order,它非常简单:取餐地址、取餐时间、送餐地址和送餐时间。此外,Delivery Service 使用更合适的Delivery名称,而不是称之为Order。Delivery Service 对订单的任何其他属性不感兴趣。 图3 Delivery Service 的领域模型 除了造成一些技术挑战以外,拥有多个领域模型还会影响用户体验。在定义微服务架构时必须识别并消除上帝类。 本文摘自《微服务架构设计模式》,经出版方授权发布。 文章来源:微信公众号 华章计算机
作者 | Chris Richardson 网络安全已成为每个企业都面临的关键问题。几乎每天都有关于黑客如何窃取公司数据的头条新闻。为了开发安全的软件并远离头条新闻,企业需要解决各种安全问题,包括硬件的物理安全性、传输和静态数据加密、身份验证、访问授权以及修补软件漏洞的策略,等等。无论你使用的是单体还是微服务架构,大多数问题都是相同的。本文重点介绍微服务架构如何影响应用程序级别的安全性。应用程序开发人员主要负责实现安全性的四个不同方面: 1.身份验证:验证尝试访问应用程序的应用程序或人员(安全的术语叫主体)的身份。例如,应用程序通常会验证访问的凭据,例如用户的 ID 和密码,或应用程序的 API 密钥。 2.访问授权:验证是否允许访问主体对指定数据完成请求的操作。应用程序通常使用基于角色的安全性和访问控制列表(ACL)的组合。基于角色的安全性为每个用户分配一个或多个角色,授予他们调用特定操作的权限。ACL 授予用户或角色对特定业务对象或聚合执行操作的权限。 3.审计:跟踪用户在应用中执行的所有操作,以便检测安全问题,帮助客户实现并强制执行合规性。 4.安全的进程间通信:理想情况下,所有进出服务的通信都应该采用传输层安全性(TLS)加密。服务间通信甚至可能需要使用身份验证。 下面将重点介绍如何实现身份验证和访问授权。审计和安全的进程间通信的更多详细介绍请参阅 Chris Richardson 的《微服务架构设计模式》。 我首先描述如何在 FTGO 单体应用程序中实现安全性。然后介绍在微服务架构中实现安全性所面临的挑战,以及为何在单体架构中运行良好的技术不能在微服务架构中使用。之后,我将介绍如何在微服务架构中实现安全性。 让我们首先回顾一下 FTGO 单体应用程序如何处理安全性。 传统单体应用程序的安全性 FTGO 应用程序有多种用户,包括消费者、送餐员和餐馆员工。他们使用基于浏览器的 Web 应用程序和移动应用程序访问 FTGO。所有 FTGO 用户都必须登录才能访问该应用程序。图 1 显示了单体 FTGO 应用程序的客户端如何验证和发出请求。 图 1 FTGO 应用程序的客户首先登录以获取会话令牌,该令牌通常是 cookie。客户在向 FTGO 应用程序发出的每个后续请求中都会包括会话令牌 当用户使用其用户 ID 和密码登录时,客户端会向 FTGO 应用程序发出包含用户凭据的 POST 请求。FTGO 应用程序验证凭据并将会话令牌返回给客户端。客户端在 FTGO 应用程序的每个后续请求中包含会话令牌。 图 2 显示了 FTGO 应用程序如何实现安全性。FTGO 应用程序是用 Java 编写的,并使用 Spring Security 框架,但我将使用同样也适用于其他框架(例如 Passport for Node.js)的一般性术语来描述这个设计。 图 2 当 FTGO 应用程序的客户端发出登录请求时,登录处理程序会对用户进行身份验证,初始化会话用户信息,并返回会话令牌 cookie,以便安全地识别会话。接下来,当客户端发出包含会话令牌的请求时,SessionBasedSecurityInterceptor 从指定的会话中检索用户信息并建立安全上下文。请求处理程序(如 OrderDetailsRequestHandler)从安全上下文中检索用户信息 使用安全框架 正确实现身份验证和访问授权具有挑战性。最好使用经过验证的安全框架。使用哪个框架取决于你的应用程序的技术栈。流行的框架包括以下几个: SpringSecurity:适用于 Java 应用程序的流行框架。它是一个复杂的框架,可以处理身份验证和访问授权。 ApacheShiro:另一个 Java 安全框架。 Passport:在 Node.js 应用程序流行的一个专注于身份验证的安全框架。 安全架构的一个关键部分是会话,它存储主体的 ID 和角色。FTGO 应用程序是传统的 Java EE 应用程序,因此会话是 HttpSession 内存中会话。会话令牌代表着每一个具体的会话,客户端在每个请求中包含会话令牌。它通常是一串无法读懂的数字标记,例如经过加密的强随机数。FTGO 应用程序的会话令牌是一个名为 JSESSIONID 的 HTTP cookie。 实现安全性的另一个关键是安全上下文,它存储有关发出当前请求的用户的信息。Spring Security 框架使用标准的 Java EE 方法将安全上下文存储在静态的线程局部变量中,任何被调用以处理请求的代码都可以访问该变量。请求处理程序可以调用 SecurityContextHolder. getContext().getAuthentication() 获取有关当前用户的信息,例如他们的身份和角色。相反,Passport 框架将安全上下文存储为 request 对象的 user 属性。 图 2 中显示的事件序列如下: 1.客户端向 FTGO 应用程序发出登录请求。 2.登录请求由 LoginHandler 处理,LoginHandler 验证凭据,创建会话,并在会话中存储有关主体的信息。 3.Login Handler 将会话令牌返回给客户端。 4.客户端在后续每次调用请求中都包含会话令牌。 5.这些请求首先由 SessionBasedSecurityInterceptor 处理。拦截器通过验证会话令牌来验证每个请求并建立安全上下文。安全上下文描述了主体及其角色。 6.请求处理程序使用安全上下文来获取其身份,并借此确定是否允许用户执行请求的操作。 FTGO 应用程序使用基于角色的授权。它定义了与不同类型用户相对应的几个角色,包括 CONSUMER、RESTAURANT、COURIER 和 ADMIN。它使用 Spring Security 的声明性安全机制来限制对特定角色的 URL 和服务方法的访问。角色也与业务逻辑交织在一起。例如,消费者只能访问自己的订单,而管理员可以访问所有订单。 单体 FTGO 应用程序使用的安全设计只是实现安全性的一种可能方式。例如,使用内存中会话的一个缺点是,它必须把特定会话的所有请求路由到同一个应用程序实例。这个要求使负载均衡和操作变复杂了。例如,你必须实现会话耗尽机制,该机制在关闭应用程序实例之前等待所有会话到期(以免丢失内存中已有的会话)。避免这些问题的另一种方法是将会话存储在数据库中。 开发者可以完全不保存服务器端会话。例如,许多应用程序都有 API 客户端,可以在每个请求中提供其凭据,例如 API 密钥和私钥。因此,无须维护服务器端会话。或者,应用程序可以将会话状态存储在会话令牌中。在本文的后面,我将介绍一种使用会话令牌存储会话状态的方法。但让我们首先看一下在微服务架构中实现安全性的挑战。 在微服务架构中实现安全性 微服务架构是分布式架构。每个外部请求都由 API Gateway 和至少一个服务处理。例如,考虑 getOrderDetails() 查询。API Gateway 通过调用多个服务来处理此查询,包括 Order Service、Kitchen Service 和 Accounting Service。每项服务都必须实现安全性的某些方面。例如,Order Service 必须只允许消费者查看他们自己的订单,这需要结合身份验证和访问授权。为了在微服务架构中实现安全性,我们需要确定谁负责验证用户身份以及谁负责访问授权。 在微服务应用程序中实现安全性的一个挑战是我们不能仅仅从单体应用程序借鉴设计思路。这是因为单体应用程序的安全架构的一些方面对微服务架构来说是不可用的,例如: 1.内存中的安全上下文:使用内存中的安全上下文(如 ThreadLocal)来传递用户身份。服务无法共享内存,因此它们无法使用内存中的安全上下文(如 ThreadLocal)来传递用户身份。在微服务架构中,我们需要一种不同的机制来将用户身份从一个服务传递到另一个服务。 2.集中会话:因为内存中的安全上下文没有意义,内存会话也没有意义。从理论上讲,多种服务可以访问基于数据库的会话,但它会违反松耦合的原则。我们需要在微服务架构中使用不同的会话机制。 让我们通过研究如何处理身份验证来开始探索微服务架构中的安全性。 由 API Gateway 处理身份验证 处理身份验证有两种不同的方法。一种选择是让各个服务分别对用户进行身份验证。这种方法的问题在于它允许未经身份验证的请求进入内部网络。它依赖于每个开发团队在所有服务中正确实现安全性。因此,出现安全漏洞的风险和概率都很大。 在服务中实现身份验证的另一个问题是不同的客户端以不同的方式进行身份验证。纯 API 客户端使用基本身份验证为每个请求提供凭据。其他客户端可能首先登录,然后为每个请求提供会话令牌。但我们要避免在服务中处理多种不同的身份验证机制。 更好的方法是让 API Gateway 在将请求转发给服务之前对其进行身份验证。在 API Gateway 中进行集中 API 身份验证的优势在于只需要确保这里的验证是正确的。因此,出现安全漏洞的可能性要小得多。另一个好处是只有 API Gateway 需要处理各种不同的身份验证机制。这使得其他服务的实现变得简单了。 图 3 显示了这种方法的工作原理。客户端使用 API Gateway 进行身份验证。API 客户端在每个请求中包含凭据。基于登录的客户端将用户的凭据发送到 API Gateway 进行身份验证,并接收会话令牌。一旦 API Gateway 验证了请求,它就会调用一个或多个服务。 图 3 API Gateway 对来自客户端的请求进行身份验证,并在其对服务的请求中包含安全令牌。服务使用令牌获取有关主体的信息。API Gateway 还可以将安全令牌用作会话令牌 模式:访问令牌 API Gateway 将包含用户信息(例如其身份和角色)的令牌传递给它调用的服务。请参阅: http://microservices.io/patterns/security/access-token.html API Gateway 调用的服务需要知道发出请求的主体(用户的身份)。它还必须验证请求是否已经过通过身份验证。解决方案是让 API Gateway 在每个服务请求中包含一个令牌。服务使用令牌验证请求,并获取有关主体的信息。API Gateway 还可以为面向会话的客户端提供相同的令牌,以用作会话令牌。 客户端的事件序列如下: 客户端发出包含凭据的请求给 API Gateway。 API Gateway 对凭据进行身份验证,创建安全令牌,并将其传递给服务。 基于登录的客户端的事件序列如下: 客户端发出包含凭据的登录请求。 API Gateway 返回安全令牌。 客户端在调用操作的请求中包含安全令牌。 API Gateway 验证安全令牌并将其转发给服务。 让我们首先看一下安全性的另一个主要方面:访问授权。 处理访问授权 验证客户端的凭据很重要,但这还不够。应用程序还必须实现访问授权机制,以验证是否允许客户端执行所请求的操作。例如,在 FTGO 应用程序中,getOrderDetails() 查询只能由下此 Order 的消费者(基于实例的安全性的一个示例)和为所有消费者提供服务的客户服务代表调用。 实现访问授权的一个位置是 API Gateway。例如,它可以将对 GET/orders/{orderId}的访问限制为消费者和客户服务代表。如果不允许用户访问特定路径,则 API Gateway 可以在将请求转发到服务之前拒绝该请求。与身份验证一样,在 API Gateway 中集中实现访问授权可降低安全漏洞的风险。你可以使用安全框架(如 Spring Security)在 API Gateway 中实现访问授权。 在 API Gateway 中实现访问授权的一个弊端是,它有可能产生 API Gateway 与服务之间的耦合,要求它们以同步的方式进行代码更新。而且,API Gateway 通常只能实现对 URL 路径的基于角色的访问。由 API Gateway 实现对单个领域对象的访问授权通常是不实际的,因为这需要详细了解服务的领域逻辑。 另一个实现访问授权的位置是服务。服务可以对 URL 和服务方法实现基于角色的访问授权。它还可以实现 ACL 来管理对聚合的访问。例如,在 Order Service 中可以实现基于角色和基于 ACL 的授权机制,以控制对 Order 的访问。FTGO 应用程序中的其他服务也可以实现类似的访问授权逻辑。 使用 JWT 传递用户身份和角色 在微服务架构中实现安全性时,你需要确定 API Gateway 应使用哪种类型的令牌来将用户信息传递给服务。有两种类型的令牌可供选择。一种选择是使用不透明(无可读性)的令牌,它们通常是一串 UUID。不透明令牌的缺点是它们会降低性能和可用性,并增加延迟。因为这种令牌的接收方必须对安全服务发起同步 RPC 调用,以验证令牌并检索用户信息。 另一种消除对安全服务调用的方法是使用包含有关用户信息的透明令牌。透明令牌的一个流行的标准是 JSON Web 令牌(JWT)。JWT 是在访问双方之间安全地传递信息(例如用户身份和角色)的标准方式。JWT 的内容包含一个 JSON 对象,其中有用户的信息,例如其身份和角色,以及其他元数据,如到期日期等。它使用仅为 JWT 的创建者所知的数字签名,例如 API Gateway 和 JWT 的接收者(服务)。该签名确保恶意第三方不能伪造或篡改 JWT。 因为不需要再访问安全服务进行验证,JWT 的一个问题是这个令牌是自包含的,也就是说它是不可撤消的。根据设计,服务将在验证 JWT 的签名和到期日期之后执行请求操作。因此,没有切实可行的方法来撤消落入恶意第三方手中的某个 JWT 令牌。解决方案是发布具有较短到期时间的 JWT,这可以限制恶意方。但是,短期 JWT 的一个缺点是应用程序必须以某种方式不断重新发布 JWT 以保持会话活动。幸运的是,这是 OAuth 2.0 安全标准旨在解决的众多问题之一。让我们来看看它是如何工作的。 在微服务架构中使用 OAuth 2.0 假设你要为 FTGO 应用程序实现一个 User Service,该应用程序管理包含用户信息(如凭据和角色)的数据库。API Gateway 调用 User Service 来验证客户端请求并获取 JWT。你可以设计 User Service 的 API 并使用你喜欢的 Web 框架实现它。但这不是 FTGO 应用程序特有的通用功能,自己开发此类服务往往是得不偿失的。 幸运的是,你不需要开发这种安全基础设施。你可以使用名为 OAuth 2.0 的标准的现成服务或框架。OAuth 2.0 是一种访问授权协议,最初旨在使公共云服务(如 GitHub 或 Google)的用户能够授予第三方应用程序访问其信息的权限,而不必向第三方应用透露他们的密码。例如,OAuth 2.0 使你能够安全地授予第三方基于云的持续集成(CI)服务,访问你的 GitHub 存储库。 虽然 OAuth 2.0 最初的重点是授权访问公共云服务,但你也可以将其用于应用程序中的身份验证和访问授权。让我们快速了解一下微服务架构如何使用 OAuth 2.0。 OAuth 2.0 中的关键概念如下: 授权服务器:提供用于验证用户身份以及获取访问令牌和刷新令牌的 API。Spring OAuth 是一个很好的用来构建 OAuth 2.0 授权服务器的框架。 访问令牌:授予对资源服务器的访问权限的令牌。访问令牌的格式取决于具体的实现技术。Spring OAuth 的实现中采用了 JWT 格式的访问令牌。 刷新令牌:客户端用于获取新的 AccessToken 的长效但同时也可被可撤消的令牌。 资源服务器:使用访问令牌授权访问的服务。在微服务架构中,服务是资源服务器。 客户端:想要访问资源服务器的客户端。在微服务架构中,API Gateway 是 OAuth 2.0 客户端。 首先,我们来谈谈如何验证 API 客户端,然后介绍如何支持基于登录的客户端。 图 4 显示了 API Gateway 如何验证来自 API 客户端的请求。API Gateway 通过向 OAuth 2.0 授权服务器发出请求来验证 API 客户端,该服务器返回访问令牌。然后,API Gateway 将包含访问令牌的一个或多个请求发送到服务。 图 4 API Gateway 通过向 OAuth 2.0 身份验证服务器发出请求来验证 API 客户端。身份验证服务器返回访问令牌,API Gateway 将其传递给服务。服务验证令牌的签名,并提取有关用户的信息,包括其身份和角色 图 4 所示的事件顺序如下: 1.客户端发出请求,使用基本身份验证提供它的凭据。 2.API Gateway 向 OAuth 2.0 身份验证服务器发出 OAuth 2.0 密码授予(Password Grant)请求(www.oauth.com/oauth2-servers/access-tokens/password-grant/)。 3.身份验证服务器验证 API 客户端的凭据,并返回访问令牌和刷新令牌。 4.API Gateway 在其对服务的请求中包含访问令牌。服务验证访问令牌并使用它来授权请求。 基于 OAuth 2.0 的 API Gateway 可以使用 OAuth 2.0 访问令牌作为会话令牌来验证面向会话的客户端。而且,当访问令牌到期时,它可以使用刷新令牌获得新的访问令牌。图 5 显示了 API Gateway 如何使用 OAuth 2.0 来处理面向会话的客户端。API 客户端通过将其凭据(发送 POST)到 API Gateway 的 /login 端点来启动会话。API Gateway 向客户端返回访问令牌和刷新令牌。然后,API 客户端在向 API Gateway 发出请求时提供这两个令牌。 图 5 客户端通过将其凭据发送到 API Gateway 来登录。API Gateway 使用 OAuth 2.0 身份验证服务器对凭据进行身份验证,并将访问令牌和刷新令牌作为 cookie 返回。客户端在其对 API Gateway 的请求中包括这些令牌 事件顺序如下: 基于登录的客户端将其凭据发送到 API Gateway。 API Gateway 的 LoginHandler 向 OAuth 2.0 身份验证服务器发出密码授予请求(www. oauth.com/oauth2-servers/access-tokens/password-grant/)。 身份验证服务器验证客户端的凭据,并返回访问令牌和刷新令牌。 API Gateway 将访问令牌和刷新令牌返回给客户端,通常是采用 cookie 的形式。 客户端在向 API Gateway 发出的请求中包含访问令牌和刷新令牌。 API Gateway 的 Session Authentication Interceptor 验证访问令牌,并将其包含在对服务的请求中。 如果访问令牌已经过期或即将过期,API Gateway 将通过发出 OAuth 2.0 刷新授权请求来获取新的访问令牌(www.oauth.com/oauth2-servers/access-tokens/refresh-access-tokens/),刷新授权请求发送给授权服务器,请求中包含刷新令牌。如果刷新令牌尚未过期或未被撤消,则授权服务器将返回新的访问令牌。API Gateway 将新的访问令牌传递给服务并将其返回给客户端。 使用 OAuth 2.0 的一个重要好处是它是经过验证的安全标准。使用现成的 OAuth 2.0 身份验证服务器意味着你不必浪费时间重新发明轮子或者是没有开发不安全的设计的风险。但 OAuth 2.0 不是在微服务架构中实现安全性的唯一方法。无论你使用哪种方法,三个关键思想如下: 1.API Gateway 负责验证客户端的身份。 2.API Gateway 和服务使用透明令牌(如 JWT)来传递有关主体的信息。 3.服务使用令牌获取主体的身份和角色。 本文摘自《微服务架构设计模式》,经出版方授权发布。 文章来源:微信公众号 IT牧场
导读:相信很多程序员在学习一门新的编程语言或者框架时,都会先了解下该语言或者该框架涉及的数据结构,毕竟当你清晰地了解了数据结构之后才能更加优雅地编写代码,MXNet同样也是如此。 在MXNet框架中你至少需要了解这三驾马车:NDArray、Symbol和Module。这三者将会是你今后在使用MXNet框架时经常用到的接口。那么在搭建或者训练一个深度学习算法时,这三者到底扮演了一个什么样的角色呢? 这里可以做一个简单的比喻,假如将从搭建到训练一个算法的过程比作是一栋房子从建造到装修的过程,那么NDArray就相当于是钢筋水泥这样的零部件,Symbol就相当于是房子每一层的设计,Module就相当于是房子整体框架的搭建。 在本文中你将实际感受命令式编程(imperative programming)和符号式编程(symbolic programming)的区别,因为NDArray接口采用的是命令式编程的方式,而Symbol接口采用的是符号式编程的方式。 作者:迈克尔·贝耶勒(Michael Beyeler) 如需转载请联系大数据(ID:hzdashuju) 01 NDArray NDArray是MXNet框架中数据流的基础结构,NDArray的官方文档地址是: https://mxnet.apache.org/api/python/ndarray/ndarray.html 与NDArray相关的接口都可以在该文档中查询到。在了解NDArray之前,希望你先了解下Python中的NumPy库: http://www.numpy.org/ 因为一方面在大部分深度学习框架的Python接口中,NumPy库的使用频率都非常高;另一方面大部分深度学习框架的基础数据结构设计都借鉴了NumPy。 在NumPy库中,一个最基本的数据结构是array,array表示多维数组,NDArray与NumPy库中的array数据结构的用法非常相似,可以简单地认为NDArray是可以运行在GPU上的NumPy array。 接下来,我会介绍在NDArray中的一些常用操作,并提供其与NumPy array的对比,方便读者了解二者之间的关系。 首先,导入MXNet和NumPy,然后通过NDArray初始化一个二维矩阵,代码如下: import mxnet as mx import numpy as np a = mx.nd.array([[1,2],[3,4]]) print(a) 输出结果如下: [[1. 2.] [3. 4.]] <NDArray 2x2 @cpu(0)> 接着,通过NumPy array初始化一个相同的二维矩阵,代码如下: b = np.array([[1,2],[3,4]]) print(b) 输出结果如下: [[1 2] [3 4]] 实际使用中常用缩写mx代替mxnet,mx.nd代替mxnet.ndarray,np代替numpy,本书后续篇章所涉及的代码默认都采取这样的缩写。 再来看看NumPy array和NDArray常用的几个方法对比,比如打印NDArray的维度信息: print(a.shape) 输出结果如下: (2, 2) 打印NumPy array的维度信息: print(b.shape) 输出结果如下: (2, 2) 打印NDArray的数值类型: print(a.dtype) 输出结果如下: <class 'numpy.float32'> 打印Numpy array的数值类型: print(b.dtype) 输出结果如下: int64 在使用大部分深度学习框架训练模型时默认采用的都是float32数值类型,因此初始化一个NDArray对象时默认的数值类型是float32。 如果你想要初始化指定数值类型的NDArray,那么可以通过dtype参数来指定,代码如下: c=mx.nd.array([[1,2],[3,4]], dtype=np.int8) print(c.dtype) 输出结果如下: <class 'numpy.int8'> 如果你想要初始化指定数值类型的NumPy array,则可以像如下这样输入代码: d = np.array([[1,2],[3,4]], dtype=np.int8) print(d.dtype) 输出结果如下: int8 在NumPy的array结构中有一个非常常用的操作是切片(slice),这种操作在NDArray中同样也可以实现,具体代码如下: c = mx.nd.array([[1,2,3,4],[5,6,7,8]]) print(c[0,1:3]) 输出结果如下: [2. 3.] <NDArray 2 @cpu(0)> 在NumPy array中可以这样实现: d = np.array([[1,2,3,4],[5,6,7,8]]) print(d[0,1:3]) 输出结果如下: [2 3] 在对已有的NumPy array或NDArray进行复制并修改时,为了避免影响到原有的数组,可以采用copy()方法进行数组复制,而不是直接复制,这一点非常重要。下面以NDArray为例来看看采用copy()方法进行数组复制的情况,首先打印出c的内容: print(c) 输出结果如下: [[1. 2. 3. 4.] [5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)> 然后调用c的copy()方法将c的内容复制到f,并打印f的内容: f = c.copy() print(f) 输出结果如下: [[1. 2. 3. 4.] [5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)> 修改f中的一个值,并打印f的内容: f[0,0] = -1 print(f) 输出结果如下,可以看到此时对应位置的值已经被修改了: [[-1. 2. 3. 4.] [ 5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)> 那么c中对应位置的值有没有被修改呢?可以打印此时c的内容: print(c) 输出结果如下,可以看到此时c中对应位置的值并没有被修改: [[1. 2. 3. 4.] [5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)> 接下来看看如果直接将c复制给e,会有什么样的情况发生: e = c print(e) 输出结果如下: [[1. 2. 3. 4.] [5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)> 修改e中的一个值,并打印e的内容: e[0,0] = -1 print(e) 输出内容如下: [[-1. 2. 3. 4.] [ 5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)> 此时再打印c的内容: print(c) 输出结果如下,可以看到对应位置的值也发生了改变: [[-1. 2. 3. 4.] [ 5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)> 实际上,NumPy array和NDArray之间的转换也非常方便,NDArray转NumPy array可以通过调用NDArray对象的asnumpy()方法来实现: g=e.asnumpy() print(g) 输出结果如下: [[-1. 2. 3. 4.] [ 5. 6. 7. 8.]] NumPy array转NDArray可以通过mxnet.ndarray.array()接口来实现: print(mx.nd.array(g)) 输出结果如下: [[-1. 2. 3. 4.] [ 5. 6. 7. 8.]] <NDArray 2x4 @cpu(0)>? 前面曾提到过NDArray和NumPy array最大的区别在于NDArray可以运行在GPU上,从前面打印出来的NDArray对象的内容可以看到,最后都有一个@cpu,这说明该NDArray对象是初始化在CPU上的,那么如何才能将NDArray对象初始化在GPU上呢?首先,调用NDArray对象的context属性可以得到变量所在的环境: print(e.context) 输出结果如下: cpu(0) 然后,调用NDArray对象的as_in_context()方法指定变量的环境,例如这里将环境指定为第0块GPU: e = e.as_in_context(mx.gpu(0)) print(e.context) 输出结果如下: gpu(0) 环境(context)是深度学习算法中比较重要的内容,目前常用的环境是CPU或GPU,在深度学习算法中,数据和模型都要在同一个环境中才能正常进行训练和测试。 MXNet框架中NDArray对象的默认初始化环境是CPU,在不同的环境中,变量初始化其实就是变量的存储位置不同,而且存储在不同环境中的变量是不能进行计算的,比如一个初始化在CPU中的NDArray对象和一个初始化在GPU中的NDArray对象在执行计算时会报错: f = mx.nd.array([[2,3,4,5],[6,7,8,9]]) print(e+f) 显示结果如下,从报错信息可以看出是2个对象的初始化环境不一致导致的: mxnet.base.MXNetError: [11:14:13] src/imperative/./imperative_utils.h:56: Check failed: inputs[i]->ctx().dev_mask() == ctx.dev_mask() (1 vs. 2) Operator broadcast_add require all inputs live on the same context. But the first argument is on gpu(0) while the 2-th argument is on cpu(0) 下面将f的环境也修改成GPU,再执行相加计算: f = f.as_in_context(mx.gpu(0)) print(e+f) 输出结果如下: [[ 1. 5. 7. 9.] [ 11. 13. 15. 17.]] <NDArray 2x4 @gpu(0)> NDArray是MXNet框架中使用最频繁也是最基础的数据结构,是可以在CPU或GPU上执行命令式操作(imperative operation)的多维矩阵,这种命令式操作直观且灵活,是MXNet框架的特色之一。因为在使用MXNet框架训练模型时,几乎所有的数据流都是通过NDArray数据结构实现的,因此熟悉该数据结构非常重要。 02 Symbol Symbol是MXNet框架中用于构建网络层的模块,Symbol的官方文档地址是: https://mxnet.apache.org/api/python/symbol/symbol.html 与Symbol相关的接口都可以在该文档中查询。与NDArray不同的是,Symbol采用的是符号式编程(symbolic programming),其是MXNet框架实现快速训练和节省显存的关键模块。 符号式编程的含义,简单来说就是,符号式编程需要先用Symbol接口定义好计算图,这个计算图同时包含定义好的输入和输出格式,然后将准备好的数据输入该计算图完成计算。 而NDArray采用的是命令式编程(imperative programming),计算过程可以逐步来步实现。其实在你了解了NDArray之后,你完全可以仅仅通过NDArray来定义和使用网络,那么为什么还要提供Symbol呢?主要是为了提高效率。在定义好计算图之后,就可以对整个计算图的显存占用做优化处理,这样就能大大降低训练模型时候的显存占用。 在MXNet中,Symbol接口主要用来构建网络结构层,其次是用来定义输入数据。接下来我们再来列举一个例子,首先定义一个网络结构,具体如下。 用mxnet.symbol.Variable()接口定义输入数据,用该接口定义的输入数据类似于一个占位符。 用mxnet.symbol.Convolution()接口定义一个卷积核尺寸为3*3,卷积核数量为128的卷积层,卷积层是深度学习算法提取特征的主要网络层,该层将是你在深度学习算法(尤其是图像领域)中使用最为频繁的网络层。 用 mxnet.symbol.BatchNorm()接口定义一个批标准化(batch normalization,常用缩写BN表示)层,该层有助于训练算法收敛。 用mxnet.symbol.Activation()接口定义一个ReLU激活层,激活层主要用来增加网络层之间的非线性,激活层包含多种类型,其中以ReLU激活层最为常用。 用mxnet.symbol.Pooling()接口定义一个最大池化层(pooling),池化层的主要作用在于通过缩减维度去除特征图噪声和减少后续计算量,池化层包含多种形式,常用形式有均值池化和最大池化。 用mxnet.symbol.FullyConnected()接口定义一个全连接层,全连接层是深度学习算法中经常用到的层,一般是位于网络的最后几层。需要注意的是,该接口的num_hidden参数表示分类的类别数。 用mxnet.symbol.SoftmaxOutput()接口定义一个损失函数层,该接口定义的损失函数是图像分类算法中常用的交叉熵损失函数(cross entropy loss),该损失函数的输入是通过softmax函数得到的,softmax函数是一个变换函数,表示将一个向量变换成另一个维度相同,但是每个元素范围在[0,1]之间的向量,因此该层用mxnet.symbol.SoftmaxOutput()来命名。这样就得到了一个完整的网络结构了。 网络结构定义代码如下: import mxnet as mx data = mx.sym.Variable('data') conv = mx.sym.Convolution(data=data, num_filter=128, kernel=(3,3), pad=(1,1), name='conv1') bn = mx.sym.BatchNorm(data=conv, name='bn1') relu = mx.sym.Activation(data=bn, act_type='relu', name='relu1') pool = mx.sym.Pooling(data=relu, kernel=(2,2), stride=(2,2), pool_type='max', name='pool1') fc = mx.sym.FullyConnected (data=pool, num_hidden=2, name='fc1') sym = mx.sym.SoftmaxOutput (data=fc, name='softmax') mx.sym是mxnet.symbol常用的缩写形式,后续篇章默认采用这种缩写形式。另外在定义每一个网络层的时候最好都能指定名称(name)参数,这样代码看起来会更加清晰。 定义好网络结构之后,你肯定还想看看这个网络结构到底包含哪些参数,毕竟训练模型的过程就是模型参数更新的过程,在MXNet中,list_arguments()方法可用于查看一个Symbol对象的参数,命令如下: print(sym.list_arguments()) 由下面的输出结果可以看出,第一个和最后一个分别是'data'和'softmax_label',这二者分别代表输入数据和标签;'conv1_weight'和'conv1_bias'是卷积层的参数,具体而言前者是卷积核的权重参数,后者是偏置参数;'bn1_gamma'和'bn1_beta'是BN层的参数;'fc1_weight'和'fc1_bias'是全连接层的参数。 ['data', 'conv1_weight', 'conv1_bias', 'bn1_gamma', 'bn1_beta', 'fc1_weight', 'fc1_bias', 'softmax_label'] 除了查看网络的参数层名称之外,有时候我们还需要查看网络层参数的维度、网络输出维度等信息,这一点对于代码调试而言尤其有帮助。 在MXNet中,可以用infer_shape()方法查看一个Symbol对象的层参数维度、输出维度、辅助层参数维度信息,在调用该方法时需要指定输入数据的维度,这样网络结构就会基于指定的输入维度计算层参数、网络输出等维度信息: arg_shape,out_shape,aux_shape = sym.infer_shape(data=(1,3,10,10)) print(arg_shape) print(out_shape) print(aux_shape) 由下面的输出结果可知,第一行表示网络层参数的维度,与前面list_arguments()方法列出来的层参数名一一对应,例如: 输入数据'data'的维度是(1, 3, 10, 10); 卷积层的权重参数'conv1_weight'的维度是(128, 3, 3, 3); 卷积层的偏置参数'conv1_bias'的维度是(128,),因为每个卷积核对应于一个偏置参数; 全连接层的权重参数'fc1_weight'的维度是(2, 3200),这里的3000是通过计算55128得到的,其中5*5表示全连接层的输入特征图的宽和高。 第二行表示网络输出的维度,因为网络的最后一层是输出节点为2的全连接层,且输入数据的批次维度是1,所以输出维度是[(1, 2)]。 第三行是辅助参数的维度,目前常见的主要是BN层的参数维度。 [(1, 3, 10, 10), (128, 3, 3, 3), (128,), (128,), (128,), (2, 3200), (2,), (1,)] [(1, 2)] [(128,), (128,)] 如果要截取通过Symbol模块定义的网络结构中的某一部分也非常方便,在MXNet中可以通过get_internals()方法得到Symbol对象的所有层信息,然后选择要截取的层即可,比如将sym截取成从输入到池化层为止: sym_mini = sym.get_internals()['pool1_output'] print(sym_mini.list_arguments()) 输出结果如下,可以看到层参数中没有sym原有的全连接层和标签层信息了: ['data', 'conv1_weight', 'conv1_bias', 'bn1_gamma', 'bn1_beta'] 截取之后还可以在截取得到的Symbol对象后继续添加网络层,比如增加一个输出节点为5的全连接层和一个softmax层: fc_new = mx.sym.FullyConnected (data=sym_mini, num_hidden=5, name='fc_new') sym_new = mx.sym.SoftmaxOutput (data=fc_new, name='softmax') print(sym_new.list_arguments()) 输出结果如下,可以看到全连接层已经被替换了: ['data', 'conv1_weight', 'conv1_bias', 'bn1_gamma', 'bn1_beta', 'fc_new_weight', 'fc_new_bias', 'softmax_label'] 除了定义神经网络层之外,Symbol模块还可以实现NDArray的大部分操作,接下来以数组相加和相乘为例介绍通过Symbol模块实现上述操作的方法。首先通过 mxnet.symbol.Variable()接口定义两个输入data_a和data_b;然后定义data_a和data_b相加并与data_c相乘的操作以得到结果s,通过打印s的类型可以看出s的类型是Symbol,代码如下: import mxnet as mx data_a = mx.sym.Variable ('data_a') data_b = mx.sym.Variable ('data_b') data_c = mx.sym.Variable ('data_c') s = data_c*(data_a+data_b) print(type(s)) 输出结果如下: <class 'mxnet.symbol.symbol.Symbol'> 接下来,调用s的bind()方法将具体输入和定义的操作绑定到执行器,同时还需要为bind()方法指定计算是在CPU还是GPU上进行,执行bind操作后就得到了执行器e,最后打印e的类型进行查看,代码如下: e = s.bind(mx.cpu(), {'data_a':mx.nd.array([1,2,3]), 'data_b':mx.nd.array([4,5,6]), 'data_c':mx.nd.array([2,3,4])}) print(type(e)) 输出结果如下: <class 'mxnet.executor.Executor'> 这个执行器就是一个完整的计算图了,因此可以调用执行器的forward()方法进行计算以得到结果: output=e.forward() print(output[0]) 输出结果如下: [ 10. 21. 36.] <NDArray 3 @cpu(0)> 相比之下,通过NDArray模块实现这些操作则要简洁和直观得多,代码如下: import mxnet as mx data_a = mx.nd.array([1,2,3]) data_b = mx.nd.array([4,5,6]) data_c = mx.nd.array([2,3,4]) result = data_c*(data_a+data_b) print(result) 输出结果如下: [ 10. 21. 36.] <NDArray 3 @cpu(0)> 虽然使用Symbol接口的实现看起来有些复杂,但是当你定义好计算图之后,很多显存是可以重复利用或共享的,比如在Symbol模块实现版本中,底层计算得到的data_a+data_b的结果会存储在data_a或data_b所在的空间,因为在该计算图中,data_a和data_b在执行完相加计算后就不会再用到了。 前面介绍的是Symbol模块中Variable接口定义的操作和NDArray模块中对应实现的相似性,除此之外,Symbol模块中关于网络层的操作在NDArray模块中基本上也有对应的操作,这对于静态图的调试来说非常有帮助。 之前提到过,Symbol模块采用的是符号式编程(或者称为静态图),即首先需要定义一个计算图,定义好计算图之后再执行计算,这种方式虽然高效,但是对代码调试其实是不大友好的,因为你很难获取中间变量的值。 现在因为采用命令式编程的NDArray模块中基本上包含了Symbol模块中同名的操作,因此可以在一定程度上帮助调试代码。接下来以卷积层为例看看如何用NDArray模块实现一个卷积层操作,首先用mxnet.ndarray.arange()接口初始化输入数据,这里定义了一个4维数据data,之所以定义为4维是因为模型中的数据流基本上都是4维的。具体代码如下: data = mx.nd.arange(0,28).reshape((1,1,4,7)) print(data) 输出结果如下: [[[[ 0. 1. 2. 3. 4. 5. 6.] [ 7. 8. 9. 10. 11. 12. 13.] [14. 15. 16. 17. 18. 19. 20.] [21. 22. 23. 24. 25. 26. 27.]]]] <NDArray 1x1x4x7 @cpu(0)> 然后,通过mxnet.ndarray.Convolution()接口定义卷积层操作,该接口的输入除了与mxnet.symbol.Convolution()接口相同的data、num_filter、kernel和name之外,还需要直接指定weight和bias。 weight和bias就是卷积层的参数值,为了简单起见,这里将weight初始化成值全为1的4维变量,bias初始化成值全为0的1维变量,这样就能得到最后的卷积结果。具体代码如下: conv1 = mx.nd.Convolution(data=data, weight=mx.nd.ones((10,1,3,3)), bias=mx.nd.zeros((10)), num_filter=10, kernel=(3,3), name='conv1') print(conv1) 输出结果如下: [[[[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]] [[ 72. 81. 90. 99. 108.] [135. 144. 153. 162. 171.]]]] <NDArray 1x10x2x5 @cpu(0)> 总体来看,Symbol和NDArray有很多相似的地方,同时,二者在MXNet中都扮演着重要的角色。采用命令式编程的NDArray其特点是直观,常用来实现底层的计算;采用符号式编程的Symbol其特点是高效,主要用来定义计算图。 03 Module 在MXNet框架中,Module是一个高级的封装模块,可用来执行通过Symbol模块定义的网络模型的训练,与Module相关的接口介绍都可以参考Module的官方文档地址: https://mxnet.apache.org/api/python/module/module.html Module接口提供了许多非常方便的方法用于模型训练,只需要将准备好的数据、超参数等传给对应的方法就能启动训练。 上午中,我们用Symbol接口定义了一个网络结构sym,接下来我们将基于这个网络结构介绍Module模块,首先来看看如何通过Module模块执行模型的预测操作。 通过mxnet.module.Module()接口初始化一个Module对象,在初始化时需要传入定义好的网络结构sym并指定运行环境,这里设置为GPU环境。 然后执行Module对象的bind操作,这个bind操作与Symbol模块中的bind操作类似,目的也是将网络结构添加到执行器,使得定义的静态图能够真正运行起来,因为这个过程涉及显存分配,因此需要提供输入数据和标签的维度信息才能执行bind操作,读者可以在命令行通过“$ watch nvidia-smi”命令查看执行bind前后,显存的变化情况。 bind操作中还存在一个重要的参数是for_training,这个参数默认是True,表示接下来要进行的是训练过程,因为我们这里只需要进行网络的前向计算操作,因此将该参数设置为False。 最后调用Module对象的init_params()方法初始化网络结构的参数,初始化的方式是可以选择的,这里采用默认方式,至此,一个可用的网络结构执行器就初始化完成了。初始化网络结构执行器的代码具体如下: mod = mx.mod.Module(symbol=sym, context=mx.gpu(0)) mod.bind(data_shapes=[('data',(8,3,28,28))], label_shapes=[('softmax_label',(8,))], for_training=False) mod.init_params() 接下来随机初始化一个4维的输入数据,该数据的维度需要与初始化Module对象时设定的数据维度相同,然后通过mxnet.io.DataBatch()接口封装成一个批次数据,之后就可以作为Module对象的forward()方法的输入了,执行完前向计算后,调用Module对象的get_outputs()方法就能得到模型的输出结果,具体代码如下: data = mx.nd.random.uniform(0,1,shape=(8,3,28,28)) mod.forward(mx.io.DataBatch([data])) print(mod.get_outputs()[0]) 输出结果如下,因为输入数据的批次大小是8,网络的全连接层输出节点数是2,因此输出的维度是8*2: [[ 0.50080067 0.4991993 ] [ 0.50148612 0.49851385] [ 0.50103837 0.4989616 ] [ 0.50171131 0.49828872] [ 0.50254387 0.4974561 ] [ 0.50104254 0.49895743] [ 0.50223148 0.49776852] [ 0.49780959 0.50219035]] <NDArray 8x2 @gpu(0)> 接下来介绍如何通过Module模块执行模型的训练操作,代码部分与预测操作有较多地方是相似的,具体代码见下文代码清单3-1,接下来详细介绍代码内容。 本文中的代码清单都可以在本书的项目代码地址中找到: https://github.com/miraclewkf/MXNet-Deep-Learning-in-Action 使用mxnet.io.NDArrayIter()接口初始化得到训练和验证数据迭代器,这里为了演示采用随机初始化的数据,实际应用中要读取有效的数据,不论读取的是什么样的数据,最后都需要封装成数据迭代器才能提供给模型训练。 用mxnet.module.Module()接口初始化得到一个Module对象,这一步至少要输入一个Symbol对象,另外这一步还可以指定训练环境是CPU还是GPU,这里采用GPU。 调用Module对象的bind()方法将准备好的数据和网络结构连接到执行器构成一个完整的计算图。 调用Module对象的init_params()方法初始化网络的参数,因为前面定义的网络结构只是一个架子,里面没有参数,因此需要执行参数初始化。 调用Module对象的init_optimizer()方法初始化优化器,默认采用随机梯度下降法(stochastic gradient descent,SGD)进行优化。 调用mxnet.metric.create()接口创建评价函数,这里采用的是准确率(accuracy)。 执行5次循环训练,每次循环都会将所有数据过一遍模型,因此在循环开始处需要执行评价函数的重置操作、数据的初始读取等操作。 此处的while循环只有在读取完训练数据之后才会退出,该循环首先会调用Module对象的forward()方法执行模型的前向计算,这一步就是输入数据通过每一个网络层的参数进行计算并得到最后结果。 调用Module对象的backward()方法执行模型的反向传播计算,这一步将涉及损失函数的计算和梯度的回传。 调用Module对象的update()方法执行参数更新操作,参数更新的依据就是第9步计算得到的梯度,这样就完成了一个批次(batch)数据对网络参数的更新。 调用Module对象的update_metric()方法更新评价函数的计算结果。 读取下一个批次的数据,这里采用了Python中的try和except语句,表示如果try包含的语句执行出错,则执行except包含的语句,这里用来标识是否读取到了数据集的最后一个批次。 调用评价对象的get_name_value()方法并打印此次计算的结果。 调用Module对象的get_params()方法读取网络参数,并利用这些参数初始化Module对象了。 调用数据对象的reset()方法进行重置,这样在下一次循环中就可以从数据的最初始位置开始读取了。 代码清单3-1 通过Module模块训练模型 import mxnet as mx import logging data = mx.sym.Variable('data') conv = mx.sym.Convolution(data=data, num_filter=128, kernel=(3,3), pad=(1,1), name='conv1') bn = mx.sym.BatchNorm(data=conv, name='bn1') relu = mx.sym.Activation(data=bn, act_type='relu', name='relu1') pool = mx.sym.Pooling(data=relu, kernel=(2,2), stride=(2,2), pool_type='max', name='pool1') fc = mx.sym.FullyConnected(data=pool, num_hidden=2, name='fc1') sym = mx.sym.SoftmaxOutput(data=fc, name='softmax') data = mx.nd.random.uniform(0,1,shape=(1000,3,224,224)) label = mx.nd.round(mx.nd.random.uniform(0,1,shape=(1000))) train_data = mx.io.NDArrayIter(data={'data':data}, label={'softmax_label':label}, batch_size=8, shuffle=True) print(train_data.provide_data) print(train_data.provide_label) mod = mx.mod.Module(symbol=sym,context=mx.gpu(0)) mod.bind(data_shapes=train_data.provide_data, label_shapes=train_data.provide_label) mod.init_params() mod.init_optimizer() eval_metric = mx.metric.create('acc') for epoch in range(5): end_of_batch = False eval_metric.reset() data_iter = iter(train_data) next_data_batch = next(data_iter) while not end_of_batch: data_batch = next_data_batch mod.forward(data_batch) mod.backward() mod.update() mod.update_metric(eval_metric, labels=data_batch.label) try: next_data_batch = next(data_iter) mod.prepare(next_data_batch) except StopIteration: end_of_batch = True eval_name_vals = eval_metric.get_name_value() print("Epoch:{} Train_Acc:{:.4f}".format(epoch, eval_name_vals[0][1])) arg_params, aux_params = mod.get_params() mod.set_params(arg_params, aux_params) train_data.reset() 代码清单3-1中的代码其实从mod.bind()方法这一行到最后都可以用Module模块中的fit()方法来实现。fit()方法不仅封装了上述的bind操作、参数初始化、优化器初始化、模型的前向计算、反向传播、参数更新和计算评价指标等操作,还提供了保存训练结果等其他操作,因此fit()方法将是今后使用MXNet训练模型时经常调用的方法。 下面这段代码就演示了fit()方法的调用,前面两行设置命令行打印训练信息,这三行代码可以直接替换代码清单3-1中从mod.bind()那一行到最后的所有代码。 在fit()方法的输入参数中,train_data参数是训练数据,num_epoch参数是训练时整个训练集的迭代次数(也称epoch数量)。需要注意的是,将所有train_data过一遍模型才算完成一个epoch,因此这里设定为将这个训练集数据过5次模型才完成训练。 logger = logging.getLogger() logger.setLevel(logging.INFO) mod.fit(train_data=train_data, num_epoch=5) 简化版的代码如代码清单3-2所示。 代码清单3-2 通过Module模块训练模型(简化版) import mxnet as mx import logging data = mx.sym.Variable('data') conv = mx.sym.Convolution(data=data, num_filter=128, kernel=(3,3), pad=(1,1), name='conv1') bn = mx.sym.BatchNorm(data=conv, name='bn1') relu = mx.sym.Activation(data=bn, act_type='relu', name='relu1') pool = mx.sym.Pooling(data=relu, kernel=(2,2), stride=(2,2), pool_type='max', name='pool1') fc = mx.sym.FullyConnected(data=pool, num_hidden=2, name='fc1') sym = mx.sym.SoftmaxOutput(data=fc, name='softmax') data = mx.nd.random.uniform(0,1,shape=(1000,3,224,224)) label = mx.nd.round(mx.nd.random.uniform(0,1,shape=(1000))) train_data = mx.io.NDArrayIter(data={'data':data}, label={'softmax_label':label}, batch_size=8, shuffle=True) print(train_data.provide_data) print(train_data.provide_label) mod = mx.mod.Module(symbol=sym,context=mx.gpu(0)) logger = logging.getLogger() logger.setLevel(logging.INFO) mod.fit(train_data=train_data, num_epoch=5) 从下面打印出来的训练结果可以看到,输出结果与代码清单3-1的输出结果基本吻合: INFO:root:Epoch[0] Train-accuracy=0.515000 INFO:root:Epoch[0] Time cost=4.618 INFO:root:Epoch[1] Train-accuracy=0.700000 INFO:root:Epoch[1] Time cost=4.425 INFO:root:Epoch[2] Train-accuracy=0.969000 INFO:root:Epoch[2] Time cost=4.428 INFO:root:Epoch[3] Train-accuracy=0.988000 INFO:root:Epoch[3] Time cost=4.410 INFO:root:Epoch[4] Train-accuracy=0.999000 INFO:root:Epoch[4] Time cost=4.425 上面的演示代码中只设定了fit()方法的几个输入,其实fit()方法的输入还有很多,实际使用中可根据具体要求设定不同的输入参数,本书后面的章节还会进行详细介绍。 得益于MXNet的静态图设计和对计算过程的优化,你会发现MXNet的训练速度相较于大部分深度学习框架要快,而且显存占用非常少!这使得你能够在单卡或单机多卡上使用更大的batch size训练相同的模型,这对于复杂模型的训练非常有利,有时候甚至还会影响训练结果。 04 小结 本文主要介绍了MXNet框架中最常用到的三个模块:NDArray、Symbol和Module,对比了三者之间的联系并通过简单的代码对这三个模块的使用有了大致的认识。 NDArray是MXNet框架中最基础的数据结构,借鉴了NumPy中array的思想且能在GPU上运行,同时采取命令式编程的NDArray在代码调试上非常灵活。NDArray提供了与NumPy array相似的方法及属性,因此熟悉NumPy array的用户应该能够很快上手NDArray的操作,而且二者之间的转换也非常方便。 Symbol是MXNet框架中定义网络结构层的接口,采取符号式编程的Symbol通过构建静态计算图可以大大提高模型训练的效率。Symbol中提供了多种方法用于查看Symbol对象的信息,包括参数层、参数维度等,同时也便于用户在设计网络结构的过程中查漏补缺。 此外,Symbol中的大部分网络层接口在NDArray中都有对应的实现,因此可以通过NDArray中对应名称的网络层查看具体的计算过程。 Module是MXNet框架中封装了训练模型所需的大部分操作的高级接口,用户可以通过Module模块执行bind操作、参数初始化、优化器初始化、模型的前向计算、损失函数的反向传播、网络参数更新、评价指标计算等,同时,Module模块还将常用的训练操作封装在了fit()方法中,通过该方法,用户可以更加方便地训练模型,可以说是既灵活又简便。 关于作者:魏凯峰,资深AI算法工程师和计算机视觉工程师,在MXNet、Pytorch、深度学习相关算法等方面有深入的研究和丰富的实践经验,从事计算机视觉算法相关的工作,主要研究方向包括目标检测、图像分类、图像对抗算法、模型加速和压缩。 本文摘编自《MXNet深度学习实战:计算机视觉算法实现》,经出版方授权发布。 文章来源:微信公众号 大数据
Istio的架构设计在逻辑上分为数据平面和控制平面: 数据平面由一系列称为“边车”(sidecar)的智能代理组成,这些代理通过Mixer来控制所有微服务间的网络通信,Mixer是一个通用的策略和遥测中心。 控制平面负责管理和配置代理来路由流量,另外,控制平面通过配置Mixer来实施策略与遥测数据收集。 Istio的数据平面主要负责流量转发、策略实施与遥测数据上报;Istio的控制平面主要负责接收用户配置生成路由规则、分发路由规则到代理、分发策略与遥测数据收集。 用户通过控制平面提供的API提交路由配置规则、策略配置规则与遥测数据收集的配置规则。Pilot把用户提交的配置规则转换成智能代理需要的配置形式,推送给智能代理。智能代理根据用户的配置来执行服务路由、遥测数据收集与服务访问策略。智能代理拦截服务所有流量,并与其他智能代理通信。 数据平面 Istio在数据平面中使用一个Envoy代理的扩展版本。Envoy是使用C++语言开发的高性能代理,它能拦截服务网络中所有服务的入口和出口流量。Istio利用了众多Envoy内置的功能特性,例如: 动态服务发现 负载均衡 TLS终止 HTTP/2和gRPC代理 熔断器 健康检查 基于百分比流量分隔的灰度发布 故障注入 丰富的度量指标 Envoy作为一个边车与对应的服务部署在同一个Kubernetes Pod中。这种部署方式使得Istio能提取丰富的流量行为信号作为属性。Istio又可以反过来使用这些数据在Mixer中进行策略决策,并发送这些数据到监控系统中,提供整个网络中的行为信息。 采用边车部署方式,可以把Istio的功能添加到一个已经存在的部署中,并且不需要重新构建或者重新编写代码。 控制平面 控制平面中主要包括Mixer、Pilot、Citadel部件。 1. Mixer Mixer是一个与平台无关的组件。Mixer负责在服务网络中实施访问控制和策略,并负责从Envoy代理和其他服务上收集遥测数据。代理提取请求级别的属性并发送到Mixer用于评估,评估请求是否能放行。 Mixer有一个灵活的插件模型。这个模型使得Istio可以与多种主机环境和后端基础设施对接。因此,Istio从这些细节中抽象了Envoy代理和Istio管理的服务。 Mixer架构如图1-4所示。 在每一个请求过程中,Envoy代理会在请求之前调用Mixer组件进行前置条件检查,在请求结束之后上报遥测数据给Mixer组件。为了提高性能,每个Envoy代理都会提前缓存大量前置条件检查规则,当需要进行前置条件检查时,直接在缓存中检查规则。如果本地缓存中没有需要的规则,再去调用Mixer组件获取规则。Mixer组件也有自己的缓存,以加速前置条件检查。需要上报的遥测数据也会被Envoy代理暂时缓存起来,等待时机上报Mixer组件,从而减少上报数据的调用次数。 2. Pilot Pilot为Envoy代理提供服务发现功能,并提供智能路由功能(例如:A/B测试、金丝雀发布等)和弹性功能(例如:超时、重试、熔断器等)。 Pilot将高级别的控制流量行为的路由策略转换为Envoy格式的配置形式,并在运行时分发给Envoy代理。Pilot抽象了平台相关的服务发现机制,并转换成Envoy数据平面支持的标准格式。这种松耦合设计使得Istio能运行在多平台环境,并保持一致的流量管理接口。 Pilot架构如图1-5所示。 Pilot抽象不同平台的服务发现机制,只需要为不同的平台实现统一的抽象模型接口,即可实现对接不同平台的服务发现机制。用户通过规则配置API来提交配置规则,Pilot把用户配置的规则和服务发现收集到的服务转换成Envoy代理需要的配置格式,推送给每个Envoy代理。 3. Citadel Citadel内置有身份和凭证管理,提供了强大的服务间和终端用户的认证。Citadel可以把不加密的通信升级为加密的通信。运维人员可以使用Citadel实施基于服务身份的策略而不用在网络层控制。现在,Istio还支持基于角色的访问控制,用于控制谁能够访问服务。 ——本文摘自《Istio入门与实战》,经出版方授权发布文章来源:微信公众号 K8S中文社区
导读:本文简要介绍将机器学习创意快速、简单和漂亮地转换为Web应用程序的工具。这并不是一个完整列表,如果你想了解更多,可以尝试使用的不同技术。 作者:曼纽尔·阿米纳特吉(Manuel Amunategui)、迈赫迪·洛佩伊(Mehdi Roopaei) 如需转载请联系大数据(ID:hzdashuju) 01 Jupyter Notebook Jupyter Notebook是基于Web的交互式Python解释器,非常适合构建、调整和发布任何使用Python脚本的东西。它被附加到一个功能完整的Python内核(将其设置为Python 3.x),并且可以像其他解释器一样加载和运行库及脚本。要安装Jupyter Notebook,请参考官方文档: http://jupyter.readthedocs.io/en/latest/install.html 安装方式多种多样,包括使用“pip3”命令,如果这种方法不适合你,则请查看官方文档,了解不同的方法(代码清单1)。 代码清单1 安装Jupyter sudo pip3 install jupyter Jupyter Notebook使用起来既简单又强大。你只需将它下载到本地计算机(它是带有* .ipynb扩展名的文件),打开命令/终端Shell窗口,导航到该文件夹,然后运行“notebook”命令(代码清单2)。 代码清单2 运行Notebook(查看官方文档,了解启动Notebook的其他方法) jupyter notebook 此命令将打开一个网页,显示它从中启动的文件夹的内容(图3)。你可以通过单击文件列表正上方的文件夹图标向下导航文件夹结构。 ▲图3 Jupyter Notebook登录页面 要打开Jupyter Notebook,只需单击任何带有“ * .ipynb”扩展名的文件。如果要创建全新Notebook,请单击紧接刷新按钮的仪表板右侧的“New”按钮。 【提示】有关其他信息、Jupyter Notebook问题以及附加内核的信息,请参阅: http://jupyter-notebook-beginner-guide.readthedocs. io/en/latest/execute.html 02 Flask Flask是一个轻量级但非常强大的服务器端Web框架。它是应用程序背后的“大脑”,也是Python数据生成函数和网页之间的黏合剂。我喜欢使用Flask的原因之一是,它允许我们在不离开Python语言的情况下将独立的Python脚本链接到服务器端Web框架,使得在对象之间传递数据更加容易! Flask附带了发布网页的最低要求。如果你需要其他支持,例如数据库、表单控件等,则必须安装其他库,这就是它被称为轻量级微框架的原因。这也是它易于使用的原因,因为你只需学习一些技巧,其他一切都可使用熟悉的经过验证的Python库。 遗憾的是,我们只能使用Python至此,最终你需要进入前端Web脚本。但是不要有困扰—互联网上有很多很棒的例子(Stackoverflow.com、w3schools.com)和令人难以置信的GetBootstrap.com模板,可以让你尽快到达目的地。 【提示】有关Flask的更多信息,请参阅官方Flask文档。 http://flask.pocoo.org/ 03 HTML HTML(超文本标记语言)是网络技术方面最基本的东西之一。它已存在多年,用于创建几乎所有的网页和Web应用程序。 对于那些想要了解这一主题的人来说,网上的免费资料浩如烟海。要了解HTML,推荐w3schools.com网站,这个网站的学习材料组织良好、全面,而且通常是交互式的。 04 CSS CSS(Cascading Style Sheet,层叠样式表)使大多数网站看起来很棒!我们在这里使用两种类型的CSS文件:大多数网页(最常见)的“”部分中加载的CSS链接和代码清单3中显示的自定义CSS。代码清单3 自定义CSS脚本块 <STYLE> .btn-circle.btn-xl { width: 70px; height: 70px; padding: 10px 2px; border-radius: 35px; font-size: 17px; line-height: 1.33; } </STYLE> 托管在外部服务器上的CSS文件无法自定义,但通常是同类最佳的。有时你只需要在页面上自定义功能,即在HTML页面中直接创建本地CSS文件或样式标签,然后使用“class”参数将其应用于特定标签或区域(代码清单4)。 代码清单4 将CSS标签应用于HTML标签 <button type="button" onclick="calculateBikeDemand(this)" id="season_spring" class="btn btn-info btn-circle btn-xl"> <i class="fa fa-check">Spring</i></button> CSS非常详细地定义了所有的尺寸、颜色、字体。它还允许你创建所见即所感的Web门户。只需创建一次,即可让所有页面调用它来继承该特定样式。 【提示】有关CSS的其他信息,请访问: w3schools.com 05 Jinja2 Jinja2用于生成标记和HTML代码,并与Flask变量紧密配合。它由Armin Ronacher创建,广泛用于处理Flask生成的数据以及直接在HTML模板中的if/then逻辑。 在此HTML模板示例中,使用Jinja2将名为“previous_slider_ value”的Flask生成的值注入滑块的“value”参数。注意使用双花括号(代码清单5)。 代码清单5 Jinja2将数据传递给HTML输入控件 <input type="range" min="1" max="100" value="{{previous_slider_value}}" id="my_slider"> 【提示】有关Jinja2的其他信息,请参阅: http://jinja.pocoo.org/docs/2.10/ 06 JavaScript JavaScript本身就是一种真正的编程语言,它可以为你的任何前端控件添加极其强大的行为。JavaScript为网页带来了很高的交互性。 这是一个有趣的示例,我们捕获HTML滑块控件的鼠标按钮松开(mouse-up)事件,以将表单提交到Flask服务器。这个想法是,每当用户更改滑块值时,Flask需要使用新的滑块值进行一些服务器端处理并重新生成网页(代码清单6)。 代码清单6 JavaScript捕获滑块onmouseup事件 slider1.onmouseup = function () { document.getElementById("submit_params").submit(); } 【提示】有关JavaScript的其他信息,请访问: w3schools.com 07 jQuery jQuery是一个定制的JavaScript库,可以帮助处理复杂的前端和行为事件,并确保不同浏览器版本之间的兼容性。 jQuery帮助优化按钮、下拉动态行为,甚至Ajax交互(许多项目中大量使用的关键技术)。 【提示】有关jQuery的更多信息,请查看jQuery.com上的官方文档。 08 Ajax Ajax是一种出色的前端脚本技术,可以为网页添加动态服务器端行为。它允许发送和接收数据,而无须像表单提交那样重建或重新加载整个页面。一个常用领域是地图网页,例如Google地图,它允许拖动和滑动地图,而无须在每次移动后重新加载整个页面。 【提示】有关Ajax的其他信息,请访问w3schools.com。 09 Bootstrap Bootstrap是一个非常强大、近乎神奇的前端Web工具。根据BuiltWith Trends的说法,它几乎占据了Web的13%。它包含大多数Web标签和控件的各种外观及行为。通过简单地将你的网页链接到最新的Bootstrap,CSS将为任何无聊的HTML页面提供即时和专业的改造! 如果你查看HTML文件,那么首先要注意的是页面顶部的LINK和SCRIPT标签中包含的链接。这是构建网页的最佳捷径(代码清单7)。 代码清单7 链接标签以继承Bootstrap CSS样式 HTML文件(更可能是你将来要创建的任何网页)都将使用这些链接来下载预制的Bootstrap和JavaScript脚本,并自动继承流行的字体、颜色、样式和行为。通过互联网,你可轻松且快速地获得最佳外观和行为。 【提示】有关Bootstrap的其他信息,请查看GetBootstrap.com上的官方文档。 10 Web插件 Web插件(plugin)具有巨大的优势:将大量硬件、数据和安全管理推送给专门从事该领域的人。没有理由重新发明轮子,浪费宝贵时间或引入安全风险。让其他人顾好这一点,而你专注于最擅长的事情。 遗憾的是,我们只能探索其中一部分,这里列出了我过去用过的好东西或者从别人那里听来的好东西(还有成千上万可能同样好的东西—寻找那些为小型企业提供良好支持的人,在成交之前他们往往会提供演示和测试账户)。 11 会员平台 有几个平台可用。 1. Memberful www.memberful.com 我个人非常喜欢Memberful.com,并认为对于任何想要轻松管理网站付费功能的人来说,它是一个很好的选择。它通过Stripe.com提供信用卡支付,以及用户管理功能,并紧密集成在你自己的Web应用程序中。 2. Patreon www.patreon.com Patreon是艺术家和内容创作者的会员平台与插件。 3. Wild Apricot www.wildapricot.com Wild Apricot是小型和非营利组织的会员平台。 4. Subhub www.subhub.com Subhub是一个为企业家、专家和组织设计的会员平台。 5. Membergate www.membergate.com Membergate是企业通信、新闻通讯、协会和受限访问站点的平台。 12 付款平台 有几个平台可用。 1. Paypal Donations www.paypal.com/us/webapps/mpp/donation 我过去使用过Paypal插件,它易于安装和使用。你所需要的只是一个信誉良好的Paypal账户,剩下的事情都很简单。 2. Paypal Express www.paypal.com/us/webapps/mpp/express-checkout Paypal Express仍然属于Paypal,能快速简便地结账。 3. Stripe http://stripe.com/ Stripe是一种付款选项,可让网站轻松接受在线信用卡付款。它是Memberful.com背后的支付引擎。 13 分析 构建自己的Web使用跟踪器需要在每个页面上添加大量的Flask自定义代码,以及用于保存这些交互的数据库和用于理解它的分析引擎。这个工作量很大!相反,使用Google Analytics,我们所要做的只是在每个页面顶部添加JavaScript代码段。基本分析可以免费使用,这对我们非常有利。 14 留言板 我过去曾使用https://disqus.com向静态网站添加留言板。它直接在你的网站上创建具有专业外观的留言板,同时在其他地方进行管理。 15 邮件列表 我已经使用formspree.io很多年了,我很喜欢它!可以很容易地将它添加到任何静态网页、文本框和提交按钮。用户可以在你的网页上添加他们的电子邮件地址,https://formspress.io将通过电子邮件向你发送已提交的信息。如果你正在托管静态站点或者不想自己管理数据库,那么这是一个很好的选择。 16 Git Git是一个很棒的版本控制工具,它能保存存储库中发生的任何代码创建、更改、更新以及删除。它与GitHub紧密集成,这对于代码保护和协作来说至关重要。它也集成在大多数云提供商那里。如果你需要处理大型应用程序或与他人协作,那么强烈建议你使用它。 大多数云提供商都支持GitHub、BitBucket等在线代码库。这些在线代码库利用Git,因此学习基础知识将对你很有帮助。在Microsoft Azure上部署Web应用程序的过程与Git紧密集成,因此有必要学一些入门知识或在线获取一些很棒的教程,例如try.github.io: git init:创建本地存储库。 git clone https://github.com/...:将GitHub存储库克隆到本地驱动器。 git status:列出已更改并等待提交(commit)和推送(push)至存储库的文件。 git add.:添加所有文件(注释期间)。 git add '*.txt':添加所有文本文件。 git commit:提交等待中的文件。 git log:查看提交历史记录。 git push(或git push azure master):将分支推送到远程主站。 git pull:将远程更改拉取到本地仓库。 git reset *:撤销git。 gitrm --cached :停止跟踪文件。 17 虚拟环境 使用虚拟环境能带来许多优势: 创建没有安装Python库的环境。 准确了解应用程序运行所需的Python库。 使计算机系统的其余部分与在此环境中安装的任何Python隔离开。 鼓励尝试。 要启动虚拟环境,请使用“venv”命令。如果你的计算机上没有安装它,建议安装一下(可以通过常见的安装程序,如pip、conda、brew等)。有关为操作系统安装虚拟环境的更多信息,请参阅“venv-Greation of virtual environments”用户指南: https://docs.python. org/3/library/venv.html 打开命令窗口并在命令行上调用Python 3“venv”函数以创建沙箱环境(代码清单8和代码清单9)。 代码清单8 创建Python虚拟环境 $ python3 -m venv some_name 代码清单9 激活环境 $ source some_name/bin/activate 完成后,可以使用代码清单10中的命令停用虚拟环境。 代码清单10 停用虚拟环境 $ deactivate 18 创建requirements.txt文件 大多数云提供商使用requirements.txt文件列出托管Web应用程序所需的所有Python库。在大多数情况下,它与Web文件一起打包并发送到其“无服务器计算”云上进行设置。 你可以创建自己的requirements.txt文件,并将其放在与Flask Python主脚本相同的文件夹中。让我们看看如何使用虚拟环境创建一个完整的requirements.txt文件。 使用虚拟环境时,你将创建一个不含任何Python库的安全沙箱。这允许你仅安装所需内容并运行“pip freeze”命令以获取库和当前版本号的快照。请注意,如果你已经知道需要哪些库、依赖项和版本号,则不需要执行此操作。 【第1步】在Python中创建虚拟环境,以从干净的平台开始,如代码清单11所示。 代码清单11 启动虚拟环境 $ python3 -m venv some_env_name $ source some_env_name/bin/activate 【第2步】使用“pip3”安装运行本地Web应用程序所需的库,如代码清单12所示。 代码清单12 安装一些库作为示例 $ pip3 install flask $ pip3 install pandas $ pip3 install sklearn 【第3步】冻结环境及所有已安装的Python库,包括requirements.txt文件中的版本号,如代码清单13所示。 代码清单13 已安装的必需库 $ pip3 freeze > requirements.txt 【第4步】停用虚拟环境,如代码清单14所示。 代码清单14 停用venv $ deactivate 通过上面这些步骤,创建了一个requirements.txt文件。使用“vi”查看其内容(按下ESC和Q键退出)。requirements.txt的内容可能看起来非常不同,但这没关系(代码清单15)。 代码清单15 检查requirements.txt文件的内容 输入: $ vi requirements.txt 输出: click==6.7 Flask==0.12.2 itsdangerous==0.24 Jinja2==2.10 MarkupSafe==1.0 numpy==1.14.2 scikit-learn scipy python-dateutil==2.7.2 pytz==2018.4 six==1.11.0 Werkzeug==0.14.1 Pillow>=1.0 matplotlib gunicorn>=19.7.1 wtforms>=2.1 在requirements.txt文件中,可以使用“==”符号来要求特定版本(代码清单16)。 代码清单16 准确分配 Flask==0.12.2 还可以要求大于等于或小于等于某版本(代码清单17)。 代码清单17 定向分配 Flask >= 0.12 或者可以简单地指定为安装程序可以找到的最新版本(代码清单18)。 代码清单18 使用最新版本 Flask 关于作者:曼纽尔·阿米纳特吉(Manuel Amunategui) 是SpringML(谷歌云和Salesforce的优选合作伙伴)的数据科学副总裁,拥有预测分析和国际管理硕士学位。在机器学习、医疗健康建模等方面有着丰富的咨询经验。 迈赫迪·洛佩伊(Mehdi Roopaei)迈赫迪·洛佩伊(Mehdi Roopaei) 是IEEE、AIAA和ISA的高级成员。他的研究兴趣包括人工智能驱动的控制系统、数据驱动决策、机器学习和物联网(IoT),以及沉浸式分析。 本文摘编自《机器学习即服务:将Python机器学习创意快速转变为云端Web应用程序》,经出版方授权发布。 文章来源:微信公众号 大数据
本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。 一些比较重要的配置信息,比如密码之类的敏感配置,我们希望将配置加密存储,保证安全性。Apollo框架本身没有提供数据加密的功能,如果想要实现数据加密的功能有两种方式,第一种是改Apollo的源码,增加加解密的逻辑,第二种比较简单,基于第三方的框架来对数据进行解密。 jasypt-spring-boot是一个基于Spring Boot开发的框架,可以将properties中加密的内容自动解密,在Apollo中也可以借助于jasypt-spring-boot这个框架来实现数据的加解密操作。 jasypt-spring-boot GitHub地址:https://github.com/ulisesbocchio/jasypt-spring-boot 将我们需要加密的配置通过jasypt-spring-boot提供的方法进行加密,然后将加密的内容配置在Apollo中,当项目启动的时候,jasypt-spring-boot会将Apollo加密的配置进行解密,从而让使用者获取到解密之后的内容。 创建一个新的Maven项目,加入Apollo和jasypt的依赖: 加入下面的依赖信息: jasypt.encryptor.password:配置加密的Key 创建一个加密的工具类,用于加密配置: 执行main方法,可以得到如下输出: input就是hello加密之后的内容,将input的值复制存储到Apollo中,存储的格式需要按照一定的规则才行: 需要将加密的内容用ENC包起来,这样jasypt才会去解密这个值。 使用的地方可以直接根据名称注入配置,比如: input的值就是解密之后的值,使用者不需要关心解密逻辑,jasypt框架在内部处理好了。 jasypt整合Apollo也是有一些不足的地方,目前我只发现了下面几个问题: 在配置中心修改值后,项目中的值不会刷新 注入Config对象获取的值无法解密 上面列举的2个问题,跟jasypt的实现方式是有关系的,意味着这种加密的方式可能只适合数据库密码之类的,启动时是可以解密的,而且只是用一次,如果是某些比较核心的业务配置需要加密的话,jasypt是支持不了的,无法做到实时更新。下章节我会讲解如何修改Apollo的源码来解决这2个问题。 扩展Apollo支持存储加解密 前面章节中给大家介绍了如何使用jasypt为Apollo中的配置进行加解密操作,基本的需求是能够实现的,但还是有一些不足的地方。 jasypt只是在启动的时候将Spring中带有ENC(xx)这种格式的配置进行解密,当配置发生修改时无法更新。由于Apollo框架本身没有这种对配置加解密的功能,如果我们想实现加解密,并且能够动态的更新,就需要对Apollo的源码做一些修改来满足需求。 对源码修改还需要重新打包,笔者在这边介绍一个比较简单的实现方式,就是创建一个跟Apollo框架中一模一样的类名进行覆盖,这样也不用替换已经在使用的客户端。 如果配置中心存储的内容是加密的,意味着Apollo客户端从配置中心拉取下来的配置也是加密之后的,我们需要在配置拉取下来之后就对配置进行解密,然后再走后面的流程,比如绑定到Spring中。在这个业务点进行切入之后,配置中心加密的内容就可以自动变成解密后的明文,对使用者透明。 通过分析Apollo的源码,笔者找到了一个最合适的切入点来做这件事情,这个类就是com.ctrip.framework.apollo.internals.DefaultConfig,DefaultConfig是Coonfig接口的实现类,配置的初始化和获取都会经过DefaultConfig的处理。 在DefaultConfig内部有一个更新配置的方法updateConfig,可以在这个方法中对加密的数据进行解密处理: 这边使用了AES来解密,也就是说配置中心的加密内容也需要用相同的加密算法进行加密,至于格式的话还是用的ENC(xx)这种格式来标识这就是一个加密的配置内容。解密之后将解密的明文内容重新赋值到Properties 中,其他的流程不变。 创建一个加密测试类,加密配置内容,复制存储到Apollo中 输出内容如下: Ke4LIPGOp3jCwbIHtmhmBA== 存储到Apollo中需要用ENC将加密内容包起来,如下: test.input = ENC(Ke4LIPGOp3jCwbIHtmhmBA==) 还是用之前的代码进行测试,Config获取和Spring注入的方式如可以成功的获取到解密的数据,并且在配置中心修改后也能实时推送到客户端成功解密。 本文摘自于《Spring Cloud微服务 入门 实战与进阶》一书。 文章来源:微信公众号 猿天地
导读:作为一种特殊的大数据,文本数据泛指各种以自然语言形式存在的数据。 目前,我们正处在一个以大数据与人工智能技术为核心的新的工业革命时代,其主要特征是大量各种可利用的数据可以视为一种特殊的生产资料,经过高效的智能数据分析与挖掘以及机器学习等人工智能技术处理后,这些数据可以产生巨大价值,创造智能。 01 一种特殊的大数据 大数据可以用两种方式创造智能。 其一,大量的数据可以作为训练数据,让监督式机器学习方法特别是深度学习,发挥巨大潜力,从大量数据中学得智能,从而使智能机器能够大量代替人力来完成各种任务(此类智能系统可称为自主型智能系统)。 例如,大量的可用于训练无人驾驶车的数据可以很自然地从人的驾驶过程中通过传感器获得,使机器可以自动驾驶车辆;又如,大量的客户服务记录数据,可以用来训练客户服务机器人,自动回答客户的问题。 其二,大量的数据可以作为对我们生活的世界的感知和观察的结果的描述,用数据挖掘或非监督式机器学习方法对数据加以处理,获得关于被观察系统的各种有用知识,从而拓展人类的感知能力,增强人的智能(此类系统自身往往智能程度不高,可以称为助理型智能系统)。 例如,大量电子病历数据可以用来构造一个医生或病人的辅助诊疗的智能助手系统;又如,大量金融数据、社交媒体数据以及新闻数据可以用来构造金融方面的决策支持系统。 比较两类基于大数据的智能系统,自主型智能系统能完成的任务不能太复杂(因机器需独立完成任务),且对数据的要求较高,需要有标注的数据,而获取极大量的高质量的标注数据在很多问题领域并不现实,所以这类应用目前只能在少量的特定应用领域起作用。 而且,由于机器的智能主要来自于人工标注的数据,机器的智能不容易超越人的智能。 相反,助理型智能系统由于不需要有标注的数据,任何数据都可以利用,所以在任何领域都可以起作用,有着非常广泛的应用。而且,有趣的是,尽管助理型智能系统本身的智能不高,甚至没有太多智能,但这样的系统一旦与人结合,人与系统相加以后的综合智能往往能大大超越人的智能。 这种情形下,助理型智能系统的功能有与显微镜及望远镜的功能相似之处,即它们都可以拓展人对世界的感知能力,从而增强人的智能,特别是有助于在复杂应用领域优化决策。 作为一种特殊的大数据,文本数据泛指各种以自然语言形式存在的数据,包括万维网页、新闻报道、社交媒体、产品评论、科学文献、政府文件等;语音和视频数据,经语音识别后也能产生文本数据。文本数据有着极其广泛的应用。 第一,文本数据可被视为人,作为一个富有智能的主观“传感器”所产生的数据,它可以与所有其它非文本数据相结合,共同支持助理型智能系统;又因为任何应用领域都会涉及相关的人群,人们会以各种形式产生可用的文本数据,所以文本数据在任何领域都会有应用价值。 第二,由于人的主观性,文本数据富含关于人的观点、偏好以及需求等信息,所以特别有助于挖掘关于人的各种属性,使智能系统可以更好地理解用户,从而可以对每一个特定的用户进行优化服务(即个性化服务)。 第三,由于文本数据是人们用自然语言交流和通信的产物,它的语义很丰富,相比非文本数据来说,文本数据更加直接地表达知识。从数据挖掘的角度看,更容易让计算机自动获取知识。然而,由于自然语言是为人类通信而设计的,需要有大量的常识及推理能力,才能准确理解,所以尽管自然语言理解研究已取得很大进展,计算机目前还不能全面理解不受限的自然语言的结构和语义,所以在所有文本数据的应用中,必须充分利用人的自然语言理解能力,让计算机成为一个智能助理。 02 从文本中挖掘数据 在过去的20年里,我们经历了在线信息的爆炸性增长。根据加利福尼亚大学伯克利分校2003年的一项研究[Lyman等2003]: ……世界每年产生1~2EB(exabyte,艾字节)的不同信息,这对于地球上的男人、女人和孩子来说,每人大约250MB(megabyte,兆字节)。各类印刷文档仅占总量的0.03%。 大量的在线信息是文本信息(即自然语言文本)。例如,根据上面引用的伯克利的研究: 报纸每年发表25TB(terabyte,太字节或称兆兆字节)内容,杂志发表10TB内容……办公文档包含195TB内容。据估计,每年发送的电子邮件总数达到6100亿封,包含11000TB信息。 当然,还有博客文章、论坛帖子、推文、科技文献以及政府文件等。Roe[2012]将电子邮件数量从2003年的6100亿封更新为2010年的107万亿封。根据IDC最近的一份报告[Gantz和Reinsel 2012],2005~2020年,数字宇宙将增长300倍,规模达130EB~40000EB。 一般来说,各种类型的在线信息都是有用的,但由于以下原因,文本信息起着特别重要的作用,可以说是最有用的一种信息。 文本(自然语言)是人类知识最自然的编码方式。因此,大多数人类的知识都是以文本数据的形式编码的。例如,科学知识几乎都整理在科学文献中,而技术手册包含如何操作设备的详细说明。 文本是人们遇到的最常见的信息类型。事实上,一个人每天产生和消费的大部分信息都是文本形式的。 文本是最具表达能力的信息形式。它可以用来描述其他媒体,如视频或图像,甚至图像搜索引擎(如Google和Bing支持的图像搜索引擎)也经常依靠匹配图像周围的文本来检索“匹配”用户关键字查询的图像。 网络文本信息的爆炸式增长强烈需要能够提供以下两种相关服务的智能化软件工具,帮助人们管理和利用文本大数据。 1. 文本检索 文本数据的增长使得人们无法及时消费数据。由于文本数据对我们积累的大部分知识进行了编码,因此通常不会被丢弃,从而导致大量文献数据的积累,这些文献数据现在超出了任何个人能够处理的能力范围,即便只是简单浏览。 在线文本信息的快速增长也意味着没有人能够消化每天产生的所有新信息。因此,迫切需要开发智能文本检索系统,以帮助人们快速、准确地获取所需的相关信息。这种需求促进了近期网络搜索行业的迅猛发展。 事实上,像Google和Bing这样的网络搜索引擎现在已经成为我们日常生活中不可或缺的一部分,每天都有数以百万计的查询。通常,在大量文本数据存在的地方,搜索引擎都是有用的(诸如桌面搜索、企业搜索或特定领域中的文献搜索,例如PubMed)。 2. 文本挖掘 文本数据是人类为了交流而产生的,所以它们通常含有丰富的语义内容,并且通常包含有价值的知识、信息、观点和个人的喜好。它们提供了很多机会来发掘对于许多应用有用的各种知识,特别是关于人类意见和偏好的知识。这些知识通常直接在文本数据中表达。 例如,现在人们习惯于通过产品评论、论坛讨论和社交媒体文本等包含主观见解的文本数据来获取有关他们感兴趣的话题的观点,并优化各种决策任务,如购买产品或选择一项服务。 同样,由于信息的巨大规模,人们需要智能的软件工具来帮助发现相关知识以优化决策或帮助他们更有效地完成任务。尽管支持文本挖掘的技术还没有像支持文本获取的搜索引擎那么成熟,但近年来在这方面已经取得了显著的进展,专业的文本挖掘工具已经在许多应用领域得到了广泛使用。 结构化数据采用定义良好的模式,使计算机处理起来相对容易,与结构化数据相比,文本没有明显的结构,所以计算机在上述智能软件工具开发过程中需要处理和理解文本编码的内容。 目前的自然语言处理技术还没有达到使计算机能够精确理解自然语言文本的水平(这也是人类往往应该参与到处理过程的主要原因),但是采用许多不同的统计和启发式方法来管理和分析文本数据已经在过去的几十年中得到了发展。它们通常非常健壮,可以用于对任何自然语言、任何主题的文本数据进行分析和管理。 上面讨论的两种服务(即文本检索和文本挖掘)在概念上对应于分析任何“大规模文本数据”过程中的两个自然步骤,如图1-1所示。 ▲图1-1 文本检索与数据挖掘是分析大规模文本数据的两项主要技术 尽管原始文本数据可能很大,但是具体的应用通常只需要少量最相关的文本数据,因此在概念上,任何应用的第一步应该是根据具体的应用或者决策去识别相关的文本数据,避免对大量不相关文本数据做不必要的处理。 将原始大规模文本数据转换成规模更小但高度相关的文本数据的第一步通常是在用户帮助下利用文本检索技术来完成(例如,用户可以使用多个查询来收集所有相关文本数据以用于决策问题)。在这第一步中,主要目标是将用户(或应用程序)与最相关的文本数据连接起来。 一旦获得一个小规模的最相关文本数据,我们需要对文本做进一步的分析来帮助用户消化文本数据中的知识和模式。这是文本挖掘的一个步骤,其目标是从文本数据中进一步发现知识和模式,以支持用户的任务。 此外,需要对任何发现的知识的可信度进行评估,所以用户通常需要返回到原始的文本数据中去获得用来解释所获得知识的上下文,并通过上下文信息验证知识的可信度。 因此,作为主要用于文本获取的搜索引擎系统,也必须在任何基于文本的决策支持系统中提供知识来源。因此,这两个步骤在概念上是交错的,一个完整的智能文本信息系统必须在一个统一的框架中进行整合。 值得指出的是,在“大数据”的背景下,文本数据与其他类型的数据是非常不同的,因为它通常是由人类直接生成的,通常也意味着要被人类消费。相反,其他数据往往是机器生成的(例如通过使用各种物理传感器收集的数据)。 由于人类可以比计算机更好地理解文本数据,所以人类参与挖掘和分析文本数据的过程绝对是至关重要的(比其他大数据应用程序更为必要)。如何最佳地将人与机器之间的工作分开从而优化人与机器之间的协作,以最少的人力来最大化其“智能组合”,是所有文本数据管理和分析应用中的一个普遍挑战。 以上讨论的两个步骤可以被认为是文本信息系统协助人类的两种不同的方式: 信息检索系统帮助用户从大量的文本数据中找到解决具体应用问题所需的最相关文本数据,从而有效地将大规模原始文本数据转换成可以被人类更容易处理的规模较小的相关文本数据; 而文本挖掘应用系统可以帮助用户分析文本数据中的模式,以提取和发现对于完成任务或进行决策直接有用的、可操作的知识,从而为用户提供更直接的任务支持。 03 文本信息系统的功能 从用户的角度来看,文本信息系统(TIS)可以提供三种不同但相关的功能,如图1-2所示。 ▲图1-2 信息获取、知识获取和文本组织是文本信息系统的三个主要功能,文本组织对信息获取和知识获取起到支撑作用,而知识获取也常被称为数据挖掘 1. 信息获取(information access) 这种能力使用户可以在需要时获取有用的信息。有了这个能力,文本信息系统可以在正确的时间连接正确的信息和正确的用户。例如,搜索引擎使得用户能够通过查询来获取文本信息,而推荐系统可以在发现可用的新信息项目时将相关信息推送给用户。 由于信息获取的主要目的是将用户与相关信息联系起来,提供这种能力的系统通常只对文本数据进行最小限度的分析,只需满足将相关信息与用户的信息需求匹配,而原始信息项目(例如,网页)通常以其原始形式交付给用户,但是也经常提供项目的摘要。 从文本分析的角度来看,用户通常需要阅读信息项目来进一步消化和利用所传递的信息。 2. 知识获取(knowledge acquisition)或文本分析(text analysis) 这种能力使得用户能够获得文本数据中蕴含的有用知识。如果没有对大规模的数据进行合成和分析,用户通常不容易获得这些知识。文本信息系统可以分析大量的文本数据以发现文本中隐藏的有趣模式。具有知识获取能力的文本信息系统可以被称为分析引擎。 例如,搜索引擎可以将产品的相关评论返回给用户,分析引擎可以使用户直接获得关于产品的主要的正面或负面意见,并比较人们对多个类似产品的意见。提供知识获取能力的系统通常需要更详细地分析文本数据,综合来自多个文本文档的信息,发现有趣的模式,创造新的信息或知识。 3. 文本组织(text organization) 此能力使系统能够用有意义的(主题)结构来注释一组文本文档,从而可以连接分散的信息,使用户可以根据该结构在信息空间中巡览。 虽然这样的结构可以被认为是从文本数据中获得的“知识”,并且直接对用户有用,但是通常它们仅用于促进信息获取或知识获取,或者兼而有之。 从这个意义上说,文本组织的能力在文本信息系统中起到了支持作用,使得信息获取和知识获取更加有效。例如,添加的结构可以允许用户使用结构上的约束进行搜索,或者根据结构进行浏览。考虑到结构的约束,结构也可以用来进行详细的分析。 信息获取可以进一步分为两种模式:拉取和推送。在拉取模式下,用户主动从系统中“拉”出有用的信息,在这种情况下,系统是被动的,等待用户提出请求,然后系统用相关信息回应。 当用户具有临时信息需求(即临时需要关于产品的意见)时,这种信息获取模式通常非常有用。例如,像Google这样的搜索引擎通常为用户提供拉取模式信息获取。在推送模式下,系统主动向用户“推”(推荐)它认为对用户有用的信息。 当用户具有相对稳定的信息需求(例如,一个人的爱好)时,推送模式常常工作良好;在这种情况下,系统可以“预先”知道用户的偏好和兴趣,从而能够向用户推荐信息而不需要用户采取主动。 拉取模式还包括两种互补的方式让用户获得相关信息:查询和浏览。在查询的情况下,用户通过(关键字)查询指定信息需求,系统将该查询作为输入并返回估计与查询相关的文档。在浏览的情况下,用户简单地沿着将信息项目链接在一起的结构进行巡览并逐渐地获得相关信息。 由于查询也可以被看作是一步即导航到一组相关文档,很显然,浏览和查询可以自然地交织。事实上,网络搜索引擎的用户通常交错进行查询和浏览。 从文本数据中获取知识通常是通过文本挖掘过程来实现的。文本挖掘可以被定义为挖掘文本数据以发现有用的知识。数据挖掘社区和自然语言处理(Natural Language Processing,NLP)社区都开发了文本挖掘的方法,但两个社区对这个问题的看法往往略有不同。 从数据挖掘的角度来看,我们可能将文本挖掘视为挖掘一种特殊的数据,即文本。遵循数据挖掘的总体目标,文本挖掘的目标自然会被视为发现和提取文本数据中的有趣模式,其中可能包括潜在主题、主题趋势或异常值。 从NLP的角度来看,文本挖掘可以被看作是理解自然语言文本的一部分,将文本转化为某种形式的知识表示,并基于提取的知识进行有限的推理。因此,一个主要的任务是执行信息抽取(information extraction),它的目标是识别和提取所涉及的各种实体(例如人员、组织和位置)及其关系(例如谁与谁见面)。 实际上,任何文本挖掘应用都可能涉及模式发现(即数据挖掘角度)和信息抽取(即NLP角度)。信息抽取丰富了文本的语义表示,使得模式发现算法能够生成语义上更有意义的模式,而不是直接处理文本的字或字符串表示。 文本挖掘的应用可以被分类为直接应用和间接应用。直接应用中被发现的知识将被用户直接消费,而间接应用中发现的知识不一定直接对用户有用,但可以通过提供更好的支持间接地帮助用户获取信息。知识获取也可以基于发现了什么知识来进一步分类。 然而,由于“知识”涉及的范围广泛,所以不可能使用少量的类别来覆盖所有的形式。尽管如此,我们仍然可以找出几个常见的类别。例如,可以发现的一种知识类型是一组隐藏在文本数据中的主题或子主题,它们可以作为文本数据中主要内容的简明摘要。另一种可以从用户生成的主观性文本中获得的知识是关于某个主题的观点的总体情感极性。 04 文本信息系统的概念框架 从概念上讲,文本信息系统可能由几个模块组成,如图1-3所示。 ▲图1-3 文本信息系统的概念框架 首先,需要基于自然语言处理技术的内容分析模块。该模块允许系统将原始文本数据转换为更有意义的表示,以便在搜索引擎中可以更有效地与用户的查询匹配,在文本分析中可以更有效地进行处理。 目前的NLP技术主要依赖于统计机器学习,以有限的语言知识作为辅助,进行不同深度的文本数据理解;浅层技术是健壮的,但更深层的语义分析只适用于非常有限的领域。一些文本信息系统能力(如摘要)会比其他能力(如搜索)需要更深的NLP。 大多数文本信息系统使用非常浅的NLP,其中文本将被简单地表示为“词袋”,词是表示的基本单位,并且文本中词的顺序被忽略(尽管保留了词频)。然而,也可以使用更复杂的表示,可以基于识别出的实体、关系或其他更深层的文本理解技术。 以内容分析为基础,文本信息系统中有多个组件以不同的方式帮助用户。以下是管理和分析文本信息的一些常见功能。 1. 搜索(search) 接收用户查询并返回相关文档。文本信息系统中的搜索组件通常称为搜索引擎。网络搜索引擎是最有用的搜索引擎之一,它使用户能够有效和高效地处理大量的文本数据。 2. 过滤/推荐(filtering/recommendation) 监督传入的数据流,确定哪些项目与用户的兴趣相关(或不相关),然后向用户推荐相关项目(或者过滤掉不相关的项目)。 根据系统是否侧重于识别相关项目或不相关项目,这个组件可以被称为推荐系统(其目标是向用户推荐相关项目)或者过滤系统(其目标是过滤掉非相关项目,允许用户只保留相关项目)。文献推荐器和垃圾邮件过滤器分别是推荐系统和过滤系统的典型例子。 3. 分类(categorization) 将文本对象划分到一个或多个预定义类别,其中类别可根据应用程序而变化。文本信息系统中的分类组件可以用各种有意义的类别对文本对象进行注释,从而丰富了文本数据的表示,进一步提升了文本分析的效率和深度。类别也可用于组织文本数据,便于文本访问。 将文章分类为一个或多个主题类别的主题分类器和将句子分类为正面、负面或中性的情感极性的情感标注器都是文本分类系统的具体例子。 4. 摘要(summarization) 对一个或多个文本文件生成一个简要的内容摘要。摘要减少了人们消化文本信息的负担,也可以提高文本挖掘的效率。生成摘要的组件称为摘要器。新闻摘要和意见摘要都是摘要器的实例。 5. 主题分析(topic analysis) 提取并分析给定文档集合的主题。主题直接促进了用户对文本数据的理解,并支持浏览文本数据。当与相关的非文本数据如时间、地点、作者等元数据相结合,主题分析可以产生许多有趣的模式,如主题的时间趋势、主题的时空分布和作者的主题概况。 6. 信息抽取(information extraction) 从文本中提取实体、实体之间的关系或其他“知识单元”。信息抽取组件可以构建实体关系图。这种知识图有多种用途,包括支持导航(沿着图的边和路径)以及进一步应用图挖掘算法去发现有趣的实体关系模式。 7. 聚类(clustering) 发现相似文本对象(例如术语、句子及文档等)的群组。聚类组件在帮助用户探索信息空间方面起着重要的作用。它使用经验数据来创建有意义的结构,这对于浏览文本对象和快速理解大型文本数据集都非常有用。它对识别无法与其他对象聚集的异常对象也是非常有用的。 8. 可视化(visualization) 以可见的方式显示文本数据中的模式。可视化组件对于吸引人们参与发现有趣模式的过程非常重要。由于人类非常善于识别视觉模式,所以将各种文本挖掘算法产生的结果可视化有很大需求。 关于作者:翟成祥(Chengxiang Zhai),信息检索与数据挖掘领域世界知名学者,ACM会士、ACM杰出科学家,伊利诺伊大学香槟分校计算机科学系以及图书馆与信息科学研究生院、基因生物学研究所和统计系教授、Willet学者。研究兴趣包括信息检索、文本挖掘、自然语言处理、机器学习、生物医学与健康信息学以及智能教育信息系统。 本文摘编自《文本数据管理与分析:信息检索与文本挖掘的实用导论》,经出版方授权发布。 文章来源:微信公众号 大数据
书 灵魂我们从来都不缺书,缺的是有质量有灵魂的好书。 1. 功利与我们 最近 Kotlin 官方在搞一个 “Kotlin Hero” 的活动,这个活动可能在国外会比较流行,但在国内恐怕就要做冷板凳了。因为我们都很功利,能看到利益的我们就冲上去了,而这个 “Kotlin Hero” 能给我们带来什么?我拿了世界冠军,面试的时候有人看吗? 我们的功利,同样还反映在书上面。每当一个热点出现,就会有无数追热点的人。我记得 17年谷歌刚刚在 IO 大会上立太子的时候,一夜之间那个小而美的QQ群从400人涨到了 1700 多人,里面不乏打广告的,挖人的,干什么的都有。 2. 灵魂与书 当时市面上还缺一本 Kotlin 的书,我在想,大概很快就会有一本 《Kotlin 权威指南》问世了吧。不过幸运的是,这事儿没有发生。为什么呢?我在读书的时候每周二下午都会在学校图书馆等新书,Android、Java 相关的旧书也基本上被我都翻了一遍,总结的经验是什么 “疯狂XXX”、什么“XXX权威指南“、什么”XXXX实战经典“ 这样的书大多就是抄袭官方或翻译文档,要不就是写一个 Demo 项目通篇贴代码,很少见到作者的思考——这也不能怪他们,因为可能他们也没怎么真正用过这些东西呢。 当然,让我感到幸运的是,”深入理解“ 这块儿牌子暂时还没有被毁,周志明老师的《深入理解 Java 虚拟机》这本书的内容我三番五次的翻阅,算得上是我买的最值的一本书了; 3. 一本有灵魂的书 同样没有被毁掉的,还有 ”XXX核心“,最出名的自然就是 《Java 核心技术》了。然而,最近发现一本书叫 《Kotlin 核心编程》,我看到这个名字的时候第一感觉是”不好,又要毁书名啦“。但本着实事求是的态度,我当然是要鉴定下再下结论的嘛,通读了一下,发现书的内容还是非常超预期的。 作为一门讲解 Kotlin 语法的书,我们对它的基本预期就是把语法用法讲解清楚,如果只是做到这一点,那么它就会像我前面提到的”没有灵魂“;但如果能把语法设计思路以及与其他语言的横向对比都综合起来讲解的话,那么作者对于语言设计本身的理解和认识都就跃然纸上,读起来就不再会是翻字典一样的枯燥无味了。 这本书在讲解 Kotlin 的时候对比了 Scala、Java 8 甚至 Haskell,Kotlin 在语言设计之初就极大的参考了这些语言,这一点我是非常认同的,如果大家留意之前我的文章,你就会发现我也特别喜欢这样做。横向对比的好处是让你清楚这项语言特性从设计思路上到应用实践上有何种取舍和考量,你不必对于其他语言做到非常熟悉,你只需要知道其中一点就好:例如 Kotlin 的数据类在 Scala 中类似的角色是什么——这对于提升我们的编程思维有极大的好处,拓宽视野就自不必说了。 在讲解 Kotlin 的语法时,本书侧重于更深入的剖析,因此开篇用了一章(第2章)就快速的贯穿了整个语言最有特色的部分,例如 val/var,Lambda,表达式等等,对于有基础的开发者来说,这个讲法显得非常的不浪费时间,因为很快你就会发现你想要的东西。例如,2.4 节标题叫”面向表达式编程“,这个提法我过去是没有听到过的,自己也没有往这个方面去想,尽管我们经常在可以使用表达式的时候优选表达式(例如 when表达式,函数表达式),但这一理念并没有被明确的提出来——如果大家用其他的语言,你就会明显的发现这一点,很少有语言把 if ... else 也作为表达式的。 我比较喜欢的还有对于 Kotlin 的类型系统的讲解。本书花了一章的篇幅讲解类型系统,类型是语言的根基,教学的过程中我也确实发现有些同学对于类型的认识不足,导致对很多语法现象无法深入理解。例如书中提到 Any? 和 Any 的关系,实际上我们没有从官方得到明确的定义,但直觉告诉我们这二者一定是有关系的,什么关系呢?通过里氏替换原则,所有适用于 Any? 的地方都可以用 Any 替代,那么后者一定是前者的子类,类似的还有 Nothing? 与 Nothing 的关系等等。类型系统的讲解,自然要提到泛型,那么泛型相关的方方面面也都呈现了出来,除了必须要讲的型变,连 Java 1.5 的泛型为什么采用擦除的机制(而我们知道 C# 2.0 引入泛型时却背道而驰)都有提到,甚至还把泛型信息存储到字节码签名信息中的点也都点到,很全面,而且都是我面试的时候特别爱问题的语法基础知识点。 函数式编程部分对我帮助比较大,毕竟我也没有特别多的函数式理论基础,JavaScript 的函数式的书籍看的比较多,Scala 的小红书(《Scala 函数式编程》)啃了一半儿就扔一边儿了。这一章以 Kotlin 为实现目标,以 Scala 为辅助,以函数式的几个重要概念为主线展开讲解,整个内容读下来还是颇为耐人寻味的。特别是读到 Functor 的部分时,作者居然用到了定义在类内部的扩展方法来实现: 使用时则通过 run 来获得外部类对象的作用域: 这个写法颇有技巧性,恰好我在前几天完善我的 Kotlin DeepCopy 这个框架的时候也用到了这个技巧,会心一笑。 整体读下来书的内容还是非常翔实的,如果大家初学 Kotlin,那么建议阅读官方开发者编写的《Kotlin 实战》,如果大家有一定的 Kotlin 开发经验,需要寻求进阶,特别是转 Kotlin 的 Scala 开发者,《Kotlin 核心编程》这本书会非常的适合你。 文章来源:微信公众号 Kotlin
本文应注重掌握如下知识点: 线程组的使用 如何切换线程状态 SimpleDataFormat 类与多线程的解决办法 如何处理线程的异常 1.线程的状态 线程对象在不同运行时期有不同的状态,状态信息就处于State枚举类中,如图所示: 线程状态 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 阻塞(BLOCKED):表示线程阻塞于锁。 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。 终止(TERMINATED):表示该线程已经执行完毕。 调用与线程有关的方法是造成线程状态改变的主要原因,其关系如图所示:(图片来源于网络) 2.线程组 可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程。 下面看下线程组的使用示例: public class Group implements Runnable { public static void main(String[] args) { Group runnable = new Group(); ThreadGroup threadGroup = new ThreadGroup("我的线程组"); Thread threadA = new Thread(threadGroup,runnable); Thread threadB = new Thread(threadGroup,runnable); threadA.start(); threadB.start(); System.out.println("活动的线程"+threadGroup.activeCount()); System.out.println("线程组的名称"+threadGroup.getName()); } @Override public void run() { while (true){ System.out.println("Thread-Name: "+Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } } 运行结果: 活动的线程2 线程组的名称我的线程组 Thread-Name: Thread-0 Thread-Name: Thread-1 控制台中的信息表明有2个线程在名为“我的线程组”下活动。 上面的线程树结构图中显示,所有的线程与线程组都在系统线程组下,下面演示如何创建具有多级关联关系的线程结构,并获取相关线程信息。 示例代码: public class Group implements Runnable { public static void main(String[] args) { ThreadGroup threadGroupMain = Thread.currentThread().getThreadGroup(); Group runnable = new Group(); ThreadGroup threadGroup = new ThreadGroup(threadGroupMain,"我的线程组"); Thread threadA = new Thread(threadGroup,runnable); Thread threadB = new Thread(threadGroup,runnable); threadA.start(); threadB.start(); System.out.println("系统线程组的名字:"+Thread.currentThread().getThreadGroup().getName()); System.out.println("系统线程组中有多少子线程:"+Thread.currentThread().getThreadGroup().activeCount()); Thread[] threads1 = new Thread[threadGroupMain.activeCount()]; threadGroupMain.enumerate(threads1); System.out.println("这些子线程具体是:"); for (int i = 0; i < threads1.length; i++) { System.out.println(threads1[i].getName()); } System.out.println("系统线程组中有多少子线程组:"+Thread.currentThread().getThreadGroup().activeGroupCount()); ThreadGroup[] listGroup = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()]; //enumerate方法将子线程组以复制的形式拷贝到数组中,并返回拷贝的数量 Thread.currentThread().getThreadGroup().enumerate(listGroup); System.out.println("子线程组的名字是:"+listGroup[0].getName()); Thread[] threads = new Thread[listGroup[0].activeCount()]; listGroup[0].enumerate(threads); System.out.println("子线程组中的线程有:"); for (int i = 0; i < threads.length; i++) { System.out.println(threads[i].getName()); } } @Override public void run() { while (true){ try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } } } 运行结果: 系统线程组的名字:main 系统线程组中有多少子线程:4 这些子线程具体是: main Monitor Ctrl-Break Thread-0 Thread-1 系统线程组中有多少子线程组:1 子线程组的名字是:我的线程组 子线程组中的线程有: Thread-0 Thread-1 需要说明的是,在实例化一个ThreadGroup线程组x时如果不指定所属的线程组,则x线程组自动归属到当前线程对象所属的线程组中,也就是隐式的当前线程组中添加了一个子线程组。 线程组批量操作 通过将线程归属到线程组中,当调用线程组ThreadGroup的interrupt()方法时,可以将该组中的所有正在运行的线程批量停止。 3.SimpleDataFormat非线程安全 SimpleDataFormat主要负责日期的转换与格式化,但在多线程的环境中,使用此类容易造成数据及处理的不准确,因为SimpleDataFormat并不是安全的。 解决方法有两种,一是每个线程都new一个新的SimpleDataFormat实例对象;二是利用ThreadLocal类将SimpleDataFormat对象绑定到线程上。 4.线程中出现异常的处理 在 Java 的多线程技术中,可以对多线程中的异常进行“捕捉”,使用的是 UncaughtExceptionHandler类,从而可以对发生的异常进行有效的处理。 thread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程"+t.getName()+"出现了异常"); e.printStackTrace(); } }); 方法setUncaughtExceptionHandler()是给指定的线程对象设置的异常处理器。在Thread类中还可以使用setDefaultUncaughtExceptionHandler()方法对所有线程对象设置异常处理器。示例代码如下: MyThread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程"+t.getName()+"出现了异常"); e.printStackTrace(); } }); 方法setDefaultUncaughtExceptionHandler()的作用是为指定的线程类的所有线程对象设置默认的异常处理器。 5.线程组中出现异常的处理 使用重写uncaughtException方法处理组内线程的异常时,每个线程内部不要有异常catch语句,如果有catch语句,则public void uncaughtException(Thread t, Throwable e) 方法不执行。 ThreadGroup group = new ThreadGroup(""){ @Override public void uncaughtException(Thread t, Throwable e) { super.uncaughtException(t, e); //一个线程出现异常,中断组内所有线程 this.interrupt(); } }; 前面介绍了线程对象的异常处理,线程类的异常处理,线程组的异常处理。将它们放一起会出现什么效果呢? 示例代码: public class MyThread{ public static void main(String[] args) { ThreadGroup threadGroup = new ThreadGroup("ThreadGroup"){ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程组的异常处理"); super.uncaughtException(t, e); } }; Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程类的异常处理"); } }); Thread thread = new Thread(threadGroup,"Thread"){ @Override public void run() { System.out.println(Thread.currentThread().getName()+"执行"); int i= 2/0; } }; thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程对象的异常处理"); } }); thread.start(); } } 运行结果: Thread执行 线程对象的异常处理 注释掉线程对象的异常处理之后,再次运行: public class MyThread{ public static void main(String[] args) { ThreadGroup threadGroup = new ThreadGroup("ThreadGroup"){ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程组的异常处理"); super.uncaughtException(t, e); } }; Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("线程类的异常处理"); } }); Thread thread = new Thread(threadGroup,"Thread"){ @Override public void run() { System.out.println(Thread.currentThread().getName()+"执行"); int i= 2/0; } }; // thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { // @Override // public void uncaughtException(Thread t, Throwable e) { // System.out.println("线程对象的异常处理"); // } // }); thread.start(); } } 运行结果: Thread执行 线程组的异常处理 线程类的异常处理 6.文末总结 本文弥补了前面几个文章的技术空白点。到此,Java多线程编程核心技术的学习告一段落。 文章来源:微信公众号 薛勤的博客
本文只需要考虑一件事:如何使单例模式遇到多线程是安全的、正确的。 1.立即加载 / "饿汉模式" 什么是立即加载?立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接 new 实例化。 public class MyObject { private static MyObject myObject = new MyObject(); public MyObject(){ } public static MyObject getInstance(){ return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 打印结果: 985396398 985396398 985396398 控制台打印的 hashCode 是同一个值,说明对象是同一个,也就实现了立即加载型单例设计模式。 此版本的缺点是不能有其他其他实例变量,因为getInstance()方法没有同步,所以有可能出现非线程安全问题。 2.延迟加载 / "懒汉模式" 什么是延迟加载?延迟加载就是在调用 get() 方法时实例才被创建,常见的实现方法就是在 get() 方法中进行 new() 实例化。 测试代码: public class MyObject { private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { //模拟对象在创建之前做的一些准备工作 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 打印结果: 985396398 610025186 21895028 从运行结果来看,创建了三个对象,并不是真正的单例模式。原因显而易见,3个线程同时进入了if (myObject == null) 判断语句中,最后各自都创建了对象。 3.延迟加载解决方案 3.1 声明synchronized关键字 既然多个线程可以同时进入getInstance() 方法,那么只需要对其进行同步synchronized处理即可。 public class MyObject { private static MyObject myObject; public MyObject() { } synchronized public static MyObject getInstance() { try { if (myObject == null) { //模拟对象在创建之前做的一些准备工作 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 打印结果: 961745937 961745937 961745937 虽然运行结果表明,成功实现了单例,但这种给整个方法上锁的解决方法效率太低。 3.2 尝试同步 synchronized 代码块 同步方法是对方法的整体加锁,这对运行效率来讲很不利的。改成同步代码块后: public class MyObject { private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { synchronized (MyObject.class) { if (myObject == null) { //模拟对象在创建之前做的一些准备工作 Thread.sleep(3000); myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 打印结果: 355159803 355159803 355159803 运行结果虽然表明是正确的,但同步synchronized语句块依旧把整个 getInstance()方法代码包括在内,和synchronize 同步方法效率是一样低下。 3.3 针对某些重要的代码进行单独同步 所以,我们可以针对某些重要的代码进行单独的同步,而其他的代码则不需要同步。这样在运行时,效率完全可以得到大幅度提升。 public class MyObject { private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { //模拟对象在创建之前做的一些准备工作 Thread.sleep(3000); synchronized (MyObject.class) { myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 运行结果: 985396398 21895028 610025186 此方法只对实例化对象的关键代码进行同步,从语句的结构上来说,运行的效率的确得到的提升。但是在多线程的情况下依旧无法解决得到一个单例对象的结果。 3.4 使用DCL双检查锁机制 在最后的步骤中,使用DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式。 public class MyObject { private volatile static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { //模拟对象在创建之前做的一些准备工作 Thread.sleep(3000); synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 运行结果: 860826410 860826410 860826410 使用DCL双重检查锁功能,成功地解决了“懒汉模式”遇到多线程的问题。DCL也是大多数多线程结合单例模式使用的解决方案。 4.使用静态内置类实现单例模式 DCL可以解决多线程单例模式的非线程安全问题。当然,还有许多其它的方法也能达到同样的效果。 public class MyObject { public static class MyObjectHandle{ private static MyObject myObject = new MyObject(); public static MyObject getInstance() { return myObject; } } public static MyObject getInstance(){ return MyObjectHandle.getInstance(); } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 打印结果: 1035057739 1035057739 1035057739 静态内置类可以达到线程安全问题,但如果遇到序列化对象时,使用默认的方式运行得到的结果还是多例的。 解决方法就是在反序列化中使用readResolve()方法: public class MyObject implements Serializable { //静态内部类 public static class MyObjectHandle{ private static final MyObject myObject = new MyObject(); } public static MyObject getInstance(){ return MyObjectHandle.myObject; } protected Object readResolve(){ System.out.println("调用了readResolve方法"); return MyObjectHandle.myObject; } public static void main(String[] args) throws IOException, ClassNotFoundException { MyObject myObject = MyObject.getInstance(); FileOutputStream outputStream = new FileOutputStream(new File("myObject.txt")); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(myObject); objectOutputStream.close(); System.out.println(myObject.hashCode()); FileInputStream inputStream = new FileInputStream(new File("myObject.txt")); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); MyObject object = (MyObject) objectInputStream.readObject(); objectInputStream.close(); System.out.println(object.hashCode()); } } 运行结果: 621009875 调用了readResolve方法 621009875 5.使用static代码块实现单例模式 静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特点来实现单例设计模式。 public class MyObject { private static MyObject myObject = null; static { myObject = new MyObject(); } public static MyObject getInstance(){ return myObject; } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 运行结果: 355159803 355159803 355159803 6.使用enum枚举数据类型实现单例模式 枚举enum 和静态代码块的特性相似,在使用枚举类时,构造方法会被自动调用,也可以应用其这个特性实现单例设计模式。 public enum Singleton { INSTANCE; private MyObject myObject = null; Singleton() { myObject = new MyObject(); } public MyObject getInstance(){ return myObject; } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { System.out.println(Singleton.INSTANCE.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(Singleton.INSTANCE.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(Singleton.INSTANCE.getInstance().hashCode()); } }).start(); } } 运行结果: 1516133987 1516133987 1516133987 这样实现的一个弊端就是违反了“职责单一原则”,完善后的代码如下: public class MyObject { public enum Singleton { INSTANCE; private MyObject myObject = null; Singleton() { myObject = new MyObject(); } public MyObject getInstance() { return myObject; } } public static MyObject getInstance(){ return Singleton.INSTANCE.getInstance(); } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } } 运行结果: 610025186 610025186 610025186 7.文末总结 本文使用若干案例来阐述单例模式与多线程结合遇到的情况与解决方案。 嗨,你还在看吗? 文章来源:微信公众号 薛勤的博客
本文主要介绍使用Java5中Lock对象也能实现同步的效果,而且在使用上更加方便。 本文着重掌握如下2个知识点: ReentrantLock 类的使用。 ReentrantReadWriteLock 类的使用。 1. 使用ReentrantLock 类 在Java多线程中,可以使用 synchronized 关键字来实现线程之间同步互斥,但在JDK1.5中新增加了 ReentrantLock 类也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比 synchronized 更加的灵活。 1.1 使用ReentrantLock实现同步 调用ReentrantLock对象的lock()方法获取锁,调用unlock()方法释放锁。 下面是初步的程序示例: public class Demo { private Lock lock = new ReentrantLock(); public void test(){ lock.lock(); for (int i= 0;i<5;i++){ System.out.println(Thread.currentThread().getName()+" - "+i); } lock.unlock(); } public static void main(String[] args) { Demo demo = new Demo(); for (int i = 0;i<5;i++){ new Thread(new Runnable() { @Override public void run() { demo.test(); } }).start(); } } } 运行结果: Thread-0 - 0 Thread-0 - 1 Thread-0 - 2 Thread-0 - 3 Thread-0 - 4 Thread-1 - 0 Thread-1 - 1 Thread-1 - 2 Thread-1 - 3 Thread-1 - 4 Thread-2 - 0 Thread-2 - 1 Thread-2 - 2 Thread-2 - 3 Thread-2 - 4 Thread-3 - 0 Thread-3 - 1 Thread-3 - 2 Thread-3 - 3 Thread-3 - 4 Thread-4 - 0 Thread-4 - 1 Thread-4 - 2 Thread-4 - 3 Thread-4 - 4 从运行的结果来看,当前线程打印完毕后将锁进行释放,其他线程才可以继续打印。 1.1.2 锁住类的所有实例对象 上面的示例是所有线程调用一个ReentrantLock实例对象实现同步,如果每个线程都调用各自ReentrantLock实例对象的同一段代码呢? 示例代码: public class MyService implements Runnable{ private ReentrantLock lock = new ReentrantLock(); public void method(){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+"锁定..."); Thread.sleep(2000); System.out.println(Thread.currentThread().getName()+"解锁。"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) { new Thread(new MyService()).start(); new Thread(new MyService()).start(); new Thread(new MyService()).start(); } @Override public void run() { method(); } } 运行结果: Thread-0锁定... Thread-2锁定... Thread-1锁定... Thread-2解锁。 Thread-0解锁。 Thread-1解锁。 从运行结果来看,并没有实现想要的方法同步的效果。如果我们想要实现类似synchronized(class),也就是给Class类上锁,可以把 ReentrantLock 声明为 static 静态变量。 示例代码: public class MyService implements Runnable{ private static ReentrantLock lock = new ReentrantLock(); public void method(){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+"锁定..."); Thread.sleep(2000); System.out.println(Thread.currentThread().getName()+"解锁。"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) { new Thread(new MyService()).start(); new Thread(new MyService()).start(); new Thread(new MyService()).start(); } @Override public void run() { method(); } } 运行结果: Thread-0锁定... Thread-0解锁。 Thread-1锁定... Thread-1解锁。 Thread-2锁定... Thread-2解锁。 从运行结果来看,成功实现了预期的结果。 1.2 使用Condition 实现等待 / 通知 关键字 synchronized 与 wait() 和 notify() / notifyAll() 方法相结合可以实现等待 / 通知模式,类 ReentrantLock 也可以实现同样的功能,但需要借助于 Condition(即对象监视器)实例,线程对象可以注册在指定的 Condition 中,从而可以有选择性地进行线程通知,在调度线程上更加灵活。 在使用 notify() / notifyAll() 方法进行通知时,被通知的线程却是由JVM随机选择的。但使用 ReentrantLock 结合 Condition 类是可以实现前面介绍过的“选择性通知”,这个功能是非常重要的,而且在 Condition 类中是默认提供的。 示例代码: public class Demo { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void await() { try { lock.lock(); System.out.println("开始等待:" + System.currentTimeMillis()); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void signal() { try { lock.lock(); System.out.println("结束等待:" + System.currentTimeMillis()); condition.signal(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Demo demo = new Demo(); new Thread(new Runnable() { @Override public void run() { demo.await(); } }).start(); Thread.sleep(3000); demo.signal(); } } 运行结果: 开始等待:1537352883839 结束等待:1537352886839 成功实现等待 / 通知模式。 在Object中,有wait() 、wait(long)、notify()、notifyAll()方法。 在Condition类中,有 await()、await(long)、signal()、signalAll()方法。 1.3使用多个Condition实现通知部分线程 示例代码: public class Demo { private Lock lock = new ReentrantLock(); private Condition conditionA = lock.newCondition(); private Condition conditionB = lock.newCondition(); public void awaitA() { try { lock.lock(); System.out.println("A开始等待:" + System.currentTimeMillis()); conditionA.await(); System.out.println("A结束等待:" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void awaitB() { try { lock.lock(); System.out.println("B开始等待:" + System.currentTimeMillis()); conditionB.await(); System.out.println("B结束等待:" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void signalAll_B() { try { lock.lock(); conditionB.signalAll(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Demo demo = new Demo(); new Thread(new Runnable() { @Override public void run() { demo.awaitA(); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.awaitB(); } }).start(); Thread.sleep(3000); demo.signalAll_B(); } } 运行结果: A开始等待:1537354021740 B开始等待:1537354021741 B结束等待:1537354024738 可以看到,只有B线程被唤醒了。 通过此实验可知,使用 ReentrantLock 对象可以唤醒指定种类的线程,这是控制部分线程行为的方便行为。 1.4 公平锁和非公平锁 锁Lock分为”公平锁“和“非公平锁”,公平锁表示线程获取锁的顺序是按照线程加载的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。 设置公平锁: Lock lock = new ReentrantLock(true); 使用ReentrantLock类设置公平锁只需要在构造时传入boolean参数即可。默认false。需要明白的是,即使设置为true也不能保证百分百公平。 总结: 公平锁:先去判断等待队列是否为空,也就是是否有线程在等待,没有就去获取锁,否则把自己加入等待队列。 非公平锁:先去尝试获取锁,如果失败再加入到等待队列。 1.5 方法getHoldCount()、getQueryLength()和getWaitQueryLength() 1.方法getHoldCount() 的作用是查询当前线程保持此锁定的个数,也就是调用 lock() 方法的次数。 示例代码: public class Service { private ReentrantLock lock = new ReentrantLock(); public void method() { try { lock.lock(); System.out.println("getHoldCount() " + lock.getHoldCount()); method2(); } finally { lock.unlock(); } } public void method2() { try { lock.lock(); System.out.println("getHoldCount() " + lock.getHoldCount()); } finally { lock.unlock(); } } public static void main(String[] args) { Service service = new Service(); service.method(); } } 运行结果: getHoldCount() 1 getHoldCount() 2 2.方法getQueryLength() 的作用是返回正等待获取此锁定的线程估计数。比如有5个方法,1个线程首先执行 await()方法,那么在调用getQueueLength()方法后返回值是4,说明有4个线程同时在等待 lock 的释放。 示例代码: public class Service { private ReentrantLock lock = new ReentrantLock(); public void method() { try { lock.lock(); System.out.println("Name: " + Thread.currentThread().getName()); Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Service service = new Service(); Runnable runnable = new Runnable() { @Override public void run() { service.method(); } }; for (int i = 0; i < 5; i++) { new Thread(runnable).start(); } Thread.sleep(1000); ReentrantLock lock = service.getLock(); System.out.println("有多少线程在等待:"+lock.getQueueLength()); } private ReentrantLock getLock() { return lock; } } 运行结果: Name: Thread-1 有多少线程在等待:4 3.方法getWaitQueryLength(condition) 的作用是返回等待与此锁定相关的给定条件Condition的线程估计数,比如有5个线程,每个线程都执行了同一个condition 对象的await() 方法,则调用 getWaitQueryLength(condition) 方法时返回的int值是5。 示例代码: public class Service { private ReentrantLock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void method() { try { lock.lock(); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void notifyMethod() { try { lock.lock(); System.out.println("等待condition的线程数" + lock.getWaitQueueLength(condition)); condition.signalAll(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Service service = new Service(); Runnable runnable = new Runnable() { @Override public void run() { service.method(); } }; for (int i = 0; i < 5; i++) { new Thread(runnable).start(); } Thread.sleep(1000); service.notifyMethod(); } } 运行结果: 等待condition的线程数5 1.6 方法hasQueuedThread()、hasQueuedThreads()和hasWaiters() 1.方法 boolean hasQueuedThread(Thread thread) 的作用是查询指定的线程是否正在等待获取此锁定。 2.方法 boolean hasQueuedThreads() 的作用是查询是否有线程正在等待获取此锁定。 1、2示例代码: public class Service { private ReentrantLock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void waitMethod(){ try { lock.lock(); Thread.sleep(Integer.MAX_VALUE); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } } public ReentrantLock getLock(){ return lock; } public static void main(String[] args) throws InterruptedException { final Service service = new Service(); Runnable runnable = new Runnable() { @Override public void run() { service.waitMethod(); } }; Thread thread1 = new Thread(runnable); Thread thread2 = new Thread(runnable); thread1.start(); thread2.start(); Thread.sleep(1000); ReentrantLock lock = service.getLock(); System.out.println(lock.hasQueuedThreads()); System.out.println(lock.hasQueuedThread(thread1)); System.out.println(lock.hasQueuedThread(thread2)); } } 运行结果: true false true 3.方法 boolean hasWaiters(Condition condition) 的作用是查询是否有线程正在等待与此锁定有关的 condition 条件。 示例代码: public class Service { private ReentrantLock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void waitMethod(){ try { lock.lock(); condition.await(); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } } public void notifyMethod(){ try { lock.lock(); System.out.println("有没有线程正在等待 condition ?" + lock.hasWaiters(condition) + " 线程数是多少?" + lock.getWaitQueueLength(condition)); condition.signalAll(); }finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Service service = new Service(); Runnable runnable = new Runnable() { @Override public void run() { service.waitMethod(); } }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } Thread.sleep(2000); service.notifyMethod(); } } 运行结果: 有没有线程正在等待 condition ?true 线程数是多少?10 1.7 方法isFair()、isHeldByCurrentThread()和isLocked() 方法boolean isFair() 的作用是判断是不是公平锁。 方法boolean isHeldByCurrentThread() 的作用是查询当前线程是否保持此锁定。 方法boolean isLocked() 的作用是查询此锁定是否由任意线程保持。 更改上面的部分代码: System.out.println(lock.isHeldByCurrentThread()); System.out.println(lock.isLocked()); lock.lock(); System.out.println(lock.isLocked()); System.out.println(lock.isHeldByCurrentThread()); 运行结果: false false true true 1.8 方法lockInterruptibly()、tryLock()和tryLock(long timeout, TimeUnit unit) 下面的三个方法都是对lock.lock()方法的另一种变形: 方法void lockInterruptibly()的作用是:如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。 而使用 lock() 方法,即使线程被中断(调用thread.interrupt()方法),也不会出现异常。 方法boolean tryLock() 的作用是,仅在未被另一个线程保持的情况下,才获取该锁定。 假设有两个线程同时调用同一个lock对象的tryLock()方法,那么除了第一个获得锁(返回true),其它都获取不到锁(返回false)。 方法 boolean tryLock(long timeout, TimeUnit unit) 的作用是,如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。 1.9 方法 condition.awaitUninterruptibly()的使用 前面讲到,执行condition.await()方法后,线程进入等待状态,如果这时线程被中断(调用thread.interrupt()方法)则会抛出异常。而使用 condition.awaitUninterruptibly() 方法代替 condition.await() 方法则不会抛出异常。 1.10 方法 condition.awaitUntil(Date deadline)的使用 使用方法 condition.awaitUntil(Date deadline) 可以代替 await(long time, TimeUnit unit) 方法进行线程等待,该方法在等待时间到达前是可以被提前唤醒的。 1.11 使用Condition实现顺序执行 使用Condition对象可以对线程执行的业务进行排序规划。 示例代码: public class DThread{ volatile private static int nextPrintWho = 1; private static ReentrantLock lock = new ReentrantLock(); final private static Condition conditionA = lock.newCondition(); final private static Condition conditionB = lock.newCondition(); final private static Condition conditionC = lock.newCondition(); public static void main(String[] args) { Thread threadA = new Thread(){ @Override public void run() { try { lock.lock(); while (nextPrintWho != 1){ conditionA.await(); } for (int i = 0;i<3;i++){ System.out.println("ThreadA "+(i+1)); } nextPrintWho = 2; conditionB.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }; Thread threadB = new Thread(){ @Override public void run() { try { lock.lock(); while (nextPrintWho != 2){ conditionA.await(); } for (int i = 0;i<3;i++){ System.out.println("ThreadB "+(i+1)); } nextPrintWho = 3; conditionB.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }; Thread threadC = new Thread(){ @Override public void run() { try { lock.lock(); while (nextPrintWho != 3){ conditionA.await(); } for (int i = 0;i<3;i++){ System.out.println("ThreadC "+(i+1)); } nextPrintWho = 1; conditionB.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } }; for (int i= 0;i<5;i++){ new Thread(threadA).start(); new Thread(threadB).start(); new Thread(threadC).start(); } } } 打印结果: ThreadA 1 ThreadA 2 ThreadA 3 ThreadB 1 ThreadB 2 ThreadB 3 ThreadC 1 ThreadC 2 ThreadC 3 .... 2.使用ReentrantReadWriteLock类 类 ReentrantLock 具有完全互斥排他的效果,即同一时间只有一个线程在执行ReentrantLock.lock() 方法后面的任务。这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的。所以在JDK中提供了一种读写锁 ReentrantReadWriteLock 类,使用它可以加快运行效率,在某些不需要操作实例变量的方法中,完全可以使用读写 ReentrantReadWriteLock 来提升该方法的代码运行速度。读写锁表示也有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。在没有线程 Thread进行写入操作时,进行读取操作的多个 Thread 都可以获取读锁,而进行写入操作的 Thread 只有在获取写锁后才能进行写入操作。即多个 Thread可以同时进行读取操作但是同一时刻只允许一个 Thread 进行写入操作。 总结起来就是:读读共享,写写互斥,读写互斥,写读互斥。 声明读写锁: ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 获取读锁: lock.readLock().lock(); 获取写锁: lock.writeLock().lock(); 3.文末总结 学习完本文完全可以使用Lock对象将 synchronized关键字替换掉,而且其具有的独特功能也是 synchronized 所不具有的。在学习并发时,Lock是synchronized关键字的进阶,掌握Lock有助于学习并发包中源代码的实现原理,在并发包中大量的类使用了Lock 接口作为同步的处理方式。 嗨,你还在看吗? 文章来源:微信公众号 薛勤的博客
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。 在本章中需要着重掌握的技术点如下: 方法join的使用 ThreadLocal类的使 4.方法join的使用 在很多情况下,主线程创建并启动了子线程,如果子线程中要进行大量的耗时运算,主线程往往将早于子线程之前结束。这时,如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到 join() 方法了。方法 join() 的作用是等待线程对象销毁。 示例代码: public class MyThread extends Thread{ @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"执行完毕"); } public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.start(); thread.join(); System.out.println("我想在thread执行完之后执行,我做到了"); } } 打印结果: Thread-0执行完毕 我想在thread执行完之后执行,我做到了 方法join() 的作用是使所属的线程对象 x 正常执行 run() 方法中的任务,而使当前线程 z 进行无限期的阻塞,等待线程x 销毁后再继续执行线程z 后面的代码。 join与synchronized的区别是:join 在内部使用 wait() 方法进行等待,而synchronize 关键字使用的是“对象监视器”原理做为同步。 在前面已经讲到:当线程呈 wait() 方法时,调用线程对象的 interrupt() 方法会出现 InterruptedException 异常。说明方法 join() 和 interrupt() 方法如果彼此遇到,则会出现异常。 4.1 方法 join(long) 的使用 方法 join(long) 中的参数是设定等待的时间。 4.2 join(long) 和 sleep(long) 的区别 方法 join(long) 的功能在内部是使用 wait(long) 方法来实现的,所以 join(long) 方法具有释放锁的特点。 方法 join(long) 的源代码如下: 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; } } } 从源代码可以了解到,当执行 wait(long) 方法后,当前线程的锁被释放,那么其他线程就可以调用此线程中的同步方法了。而 Thread.sleep() 方法却不释放锁。 5.类ThreadLocal的使用 变量值的共享可以使用 public static 变量的形式,所有的线程都使用同一个 public static 变量。如果想实现每一个线程都有自己的共享变量该如何解决呢?JDK中提供的类ThreadLocal正是为了解决这样的问题。 类ThreadLocal 主要解决的就是每个线程绑定自己的值,可以将 ThreadLocal 类比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。 示例代码: public class LocalThread extends Thread { private static ThreadLocal local = new ThreadLocal(); @Override public void run() { local.set("线程的值"); System.out.println("thread线程:"+ local.get()); } public static void main(String[] args) throws InterruptedException { System.out.println(local.get()); local.set("main的值"); LocalThread t = new LocalThread(); t.start(); Thread.sleep(1000); System.out.println("main线程:"+ local.get()); } } 打印结果: null thread线程:线程的值 main线程:main的值 在第一次调用get()方法返回的是null,怎么样能实现第一次调用get()不返回 null 呢?也就是具有默认值的效果。 答案是继承 LocalThread 类重写 initialValue() 方法: public class Local extends ThreadLocal { @Override protected Object initialValue() { return new Date(); } } ThreadLocal原理 ThreadLocal内部使用了ThreadLocalMap,ThreadLocal的set方法内部通过当前线程对象获取ThreadLocalMap对象,然后将当前ThreadLocal对象作为Key与Value一起保存到ThreadLocalMap中。 public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocal的get方法内部也是通过当前线程对象获取ThreadLocalMap对象,把当前ThreadLocal对象作为Key,获取Value。 public T get() { Thread t = Thread.currentThread(); 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(); } 6.类 InheritableThreadLocal 的使用 使用类 InheritableThreadLocal 可以在子线程中取得父线程继承下来的值。 示例代码: public class LocalThread extends Thread { private static InheritableThreadLocal local = new InheritableThreadLocal(); @Override public void run() { System.out.println("thread线程:"+ local.get()); } public static void main(String[] args) throws InterruptedException { local.set("main的值"); LocalThread t = new LocalThread(); t.start(); System.out.println("main线程:"+ local.get()); } } 如果想要自定义 get() 方法默认值,具体操作也和 ThreadLocal 是一样的。 public class Local extends InheritableThreadLocal { @Override protected Object initialValue() { return new Date(); } } InheritableThreadLocal 提供继承的同时还可以进行进一步的处理。代码如下: public class Local extends InheritableThreadLocal { @Override protected Object initialValue() { return new Date(); } @Override protected Object childValue(Object parentValue) { return parentValue+"[子线程增强版]"; } } 但在使用 InheritableThreadLocal 类需要注意一点的是,如果子线程在取得值的同时,主线程将 InheritableThreadLocal 中的值进行更改,那么子线程取到的值还是旧值。 7.文末总结 经过本文的学习,可以将以前分散的线程对象进行彼此的通信与协作,线程任务不再是单打独斗,更具有团结性,因为它们之间可以相互通信。 嗨,你还在看吗? 文章来源:微信公众号 薛勤的博客
线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体。线程间的通信就是成为整体的必用方案之一,可以说,使线程间进行通信后,系统之间的交互性会更强大,在大大提高CPU利用率的同时还会使程序员对各线程任务在处理的过程中进行有效的把控与监督。 在本章中需要着重掌握的技术点如下: 使用wait/notify实现线程间的通信 生产者/消费者模式的实现 1.等待 / 通知机制 通过本节可以学习到,线程与线程之间不是独立的个体,它们彼此之间可以互相通信和协作。 1.1 不使用等待 / 通知机制实现线程间通信 下面的示例,是sleep()结合while(true)死循环来实现多个线程间通信。 public class MyService { volatile private List<Integer> list = new ArrayList<>(); public void add(){ list.add(1); } public int size(){ return list.size(); } public static void main(String[] args) { MyService myService = new MyService(); new Thread(new Runnable() { @Override public void run() { for (int i= 0;i<10;i++) { myService.add(); System.out.println("添加了"+myService.size()+"个元素"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { try { while (true){ if (myService.size() == 5){ System.out.println(" == 5 ,我要退出了"); throw new InterruptedException(); } } } catch (InterruptedException e) { System.out.println(myService.size()); e.printStackTrace(); } } }).start(); } } 打印结果: 添加了1个元素 添加了2个元素 添加了3个元素 添加了4个元素 添加了5个元素 == 5 ,我要退出了 5 java.lang.InterruptedException at cn.zyzpp.thread3_1.MyService$2.run(MyService.java:42) at java.lang.Thread.run(Thread.java:745) 添加了6个元素 添加了7个元素 添加了8个元素 添加了9个元素 添加了10个元素 虽然两个线程间实现了通信,但有一个弊端就是,线程ThreadB.java不停地通过while语句轮询机制来检测某一个条件,这样会浪费CPU资源。如果轮询的时间间隔很小,更浪费CPU资源;如果轮询的时间间隔很大,有可能会取不到想要得到的数据。所以就需要一种机制来实现减少CPU的资源浪费,而且还可以实现在多个线程间通信,它就是“wait / notify”机制。 1.2 什么是等待 / 通知机制 等待 / 通知机制在生活中比比皆是,比如你去餐厅点餐,服务员去取菜,菜暂时还没有做出来,这时候服务员就进入”等待“的状态,等到厨师把菜放在菜品传递台上,其实就相当于一种”通知“,这时服务员才可以拿到菜并交给就餐者。 需要说明的是,上节多个线程间也可以实现通信,原因是多个线程共同访问同一个变量,但那种通信不是“等待/通知”,两个线程完全是主动式地读取一个共享变量,在花费读取时间的基础上,读到的值是不是想要的,并不能完全确定。所以现在迫切需要一种“等待 / 通知”机制来满足上面的要求。 1.3 等待 / 通知机制的实现 方法 wait() 的作用是使当前执行代码的线程进行等待,wait()方法是object类的方法,该方法用来将当前线程置于“预执行队列”中,并且在wait()所在的代码行处停止执行,直到接到通知或被中断为止。在调用wait()方法之前,线程必须拿到该对象的对象级别锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时没有持有适当的锁,则抛出 java.lang.IllegalMonitorStateException 异常,它是RuntimeException 的一个子类,因此,不需要try-catch语句进行捕捉异常。 方法notify()也要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁。如果调用notify时没有适当的锁,也会抛出 java.lang.IllegalMonitorStateException 异常。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选出其中一个呈 wait 状态的线程,对其发出通知 notify,并使它等待获取该对象的对象锁。需要说明的是,在执行 notify 方法后,当前线程不会马上释放该对象锁,呈 wait 状态的线程也并不能马上获取该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁,而呈wait状态所在的线程才可以获取该对象锁。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则该对象以及空闲,其它 wait 状态等待的线程由于没有得到该对象的通知,还会继续阻塞在 wait 状态,知道直到这个对象发出一个 notify 或 notifyAll。 用一句话来总结一下 wait 和 notify :wait 使线程停止运行,而 notify 使停止的线程继续运行。 示例代码: public class MyServiceTwo extends Thread { private Object lock; public MyServiceTwo(Object object) { this.lock = object; } @Override public void run() { try { synchronized (lock){ System.out.println("开始等待"+System.currentTimeMillis()); lock.wait(); System.out.println("结束等待"+System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class MyServiceThree extends Thread { private Object lock; public MyServiceThree(Object object) { this.lock = object; } @Override public void run() { synchronized (lock) { System.out.println("开始通知" + System.currentTimeMillis()); lock.notify(); System.out.println("结束通知" + System.currentTimeMillis()); } } public static void main(String[] args) throws InterruptedException { Object lock = new Object(); MyServiceTwo serviceTwo = new MyServiceTwo(lock); serviceTwo.start(); Thread.sleep(100); MyServiceThree serviceThree = new MyServiceThree(lock); serviceThree.start(); } } 打印结果: 开始等待1537185132949 开始通知1537185133048 结束通知1537185133048 结束等待1537185133048 从控制台的打印来看,100ms后线程被 notify 通知唤醒。 下面我们使用 wait / notify 来实现刚开始的实验: public class MyService { volatile private List<Integer> list = new ArrayList<>(); public void add() { list.add(1); } public int size() { return list.size(); } public static void main(String[] args) { MyService myService = new MyService(); Object lock = new Object(); new Thread(new Runnable() { @Override public void run() { try { synchronized (lock) { if (myService.size() != 5) { System.out.println("等待 "+System.currentTimeMillis()); lock.wait(); System.out.println("等待结束 "+System.currentTimeMillis()); } } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { synchronized (lock) { for (int i = 0; i < 10; i++) { if (myService.size() == 5){ lock.notify(); System.out.println("已发出通知!"); } myService.add(); System.out.println("添加了" + myService.size() + "个元素"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }).start(); } } 打印结果: 等待 1537186277023 添加了1个元素 添加了2个元素 添加了3个元素 添加了4个元素 添加了5个元素 已发出通知! 添加了6个元素 添加了7个元素 添加了8个元素 添加了9个元素 添加了10个元素 等待结束 1537186287034 日志信息 wait end 在最后输出,这也说明 notify 方法执行后并不立即释放锁。 关键字 synchronized 可以将任何一个 Object 对象作为同步对象来看待,而 Java 为每个 Object 都实现了 wait 和 notify 方法,它们必须用在被 synchronized 同步的 object 的临界区内。通过调用 wait() 方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象对象的锁。而 notify 操作可以唤醒一个因调用了 wait 操作而处于阻塞状态中的线程,使其进入就绪状态。被重新换醒的线程会试图重新获得临界区的控制权,也就是锁,并继续执行临界区内 wait 之后的代码。如果发出 notify 操作时没有处于阻塞状态中的线程,那么该命令会被忽略。 wait 方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。 notify 方法可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,也就是 notify() 方法仅通知“一个”线程。 notifyAll() 方法可以使所有正在等待队列中等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。并使该线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于JVM虚拟机的实现。 在《Java多线程编程核心技术(一)Java多线程技能》中,已经介绍了与Thread有关的大部分 API ,这些 API 可以改变线程对象的状态。 新创建一个新的线程对象后,再调用它的 start() 方法,系统会为此线程分配CPU资源,使其处于 Runnable(可运行)状态,这是一个准备运行的阶段。如果线程抢占到CPU资源,此线程就处于 Running(运行)状态。 Runnable 状态和 Running 状态可相互切换,因为有可能线程运行一段时间后,有其他高优先级的线程抢占了CPU资源,这时此线程就从 Running 状态变成 Runnable 状态。 线程进入Runable 状态大体分为如下3中情况: 调用 sleep方法后经过的时间超过了指定的休眠时间。 线程调用的阻塞IO已经返回,阻塞方法执行完毕。 线程成功地获得了试图同步的监视器。 线程正在等待某个通知,其他线程发出了通知。 处于挂起状态的线程调用了 resurne恢复方法。 Blocked是阻寒的意思, 例如遇到了一个IO操作, 此时CPU处于空闲状态, 可能会转而把CPU时间片分配给其他线程, 这时也可以称为“暂停”状态。Blocked 状态结束后,进入 Runnable状态, 等待系统重新分配资源。 出现阻塞的情况大体分为如下5种: 线程调用 sleep方法, 主动放弃占用的处理器资源。 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。 线程等待某个通知。 程序调用了 suspend方法将该线程挂起。此方法容易导致死锁,尽量避免使用该方法。 main() 方法运行结束后进人销毁阶段,整个线程执行完毕。 每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后,才会进入就绪队列,等待CPU的调度;反之,一个线程被 wait 后,就会进入阻塞队列,等待下一次被唤醒。 1.4 方法wait()锁释放与notify()锁不释放当方法 wait() 被执行后,锁自动释放,但执行完 notify() 方法,锁却不自动释放。 1.5 当interrupt方法遇到wait方法当线程呈 wait() 方法时,调用线程对象的 interrupt() 方法会出现 InterruptedException 异常。 下面我们做一个实验: public class MyServiceTwo extends Thread { private Object lock; public MyServiceTwo(Object object) { this.lock = object; } @Override public void run() { try { synchronized (lock){ System.out.println("开始等待"+System.currentTimeMillis()); lock.wait(); System.out.println("结束等待"+System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); System.out.println("出现异常了"); } } public static void main(String[] args) throws InterruptedException { Object lock = new Object(); MyServiceTwo service = new MyServiceTwo(lock); service.start(); Thread.sleep(5000); service.interrupt(); } } 运行结果: 开始等待1537194007598 java.lang.InterruptedException 出现异常了 at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at cn.zyzpp.thread3_1.MyServiceTwo.run(MyServiceTwo.java:19) 通过上面的实验可以总结如下三点: 执行完同步代码块就会释放对象的锁。 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。 在执行同步代码块的过程中,执行了锁所属对象的 wait() 方法,这个线程会释放对象锁,而此线程对象会进入线程等待池中,等待被唤醒。 1.6 notify()和notifyAll() 调用方法 notify() 一次只随机通知一个线程进行唤醒。 当多次调用 notify() 方法会随机将等待 wait 状态的线程进行唤醒。 notifyAll() 方法会唤醒全部线程。 1.7 方法 wait(long) 的使用 带一个参数的 wait(long) 方法的功能是等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。 1.8 等待/通知之交叉备份 假设我们创建了20个线程,我们需要这20个线程的运行效果变成有序的,我们可以在 等待 / 通知的基础上,利用如下代码作为标记: volatile private boolean prevIsA = false; 再使用while()循环: while(prevIsA){ wait(); } 实现交替打印。 2.生产者 / 消费者模式 等待 / 通知模式最经典的案列就是”生产者 / 消费者“模式。但此模式在使用上有几种”变形“,还有一些小的注意事项,但原理都是基于 wait/notify 的。 1.一生产与一消费:操作值 生产者: public class P { private String lock; public P(String lock) { super(); this.lock = lock; } public void setValue(){ try { synchronized (lock){ if (!ValueObject.value.equals("")){ lock.wait(); } String value = System.currentTimeMillis() + "_" + System.nanoTime(); System.out.println("set的值是 "+value); ValueObject.value = value; lock.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } 消费者: public class C { private String lock; public C(String lock) { super(); this.lock = lock; } public void getVlue() { try { synchronized (lock) { if (ValueObject.value.equals("")) { lock.wait(); } System.out.println("get的值是 " + ValueObject.value); ValueObject.value = ""; lock.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } 操作值: public class ValueObject { public static String value = ""; } main方法: public class Run { public static void main(String[] args) { String lock = new String(); P p = new P(lock); C c = new C(lock); new Thread(new Runnable() { @Override public void run() { while (true) { p.setValue(); } } }).start(); new Thread(new Runnable() { @Override public void run() { while (true) { c.getVlue(); } } }).start(); } } 打印结果: set的值是 1537253968947_1379616037064493 get的值是 1537253968947_1379616037064493 set的值是 1537253968947_1379616037099625 get的值是 1537253968947_1379616037099625 set的值是 1537253968947_1379616037136730 get的值是 1537253968947_1379616037136730 set的值是 1537253968947_1379616037173047 ..... 本实例是1个生产者与消费者进行数据的交互,在控制台中打印的日志get和set是交替运行的。 但如果在此实验的基础上,设计出多个生产者与消费者,那么在运行的过程中极有可能出现“假死”的情况,也就是所有的线程都呈 WAITING 等待状态。 2.多生产与多消费:操作值 生产者: public class P { private String lock; public P(String lock) { super(); this.lock = lock; } public void setValue(){ try { synchronized (lock){ while (!ValueObject.value.equals("")){ System.out.println("生产者"+Thread.currentThread().getName()+"WAITING"); lock.wait(); } String value = System.currentTimeMillis() + "_" + System.nanoTime(); System.out.println("生产者"+Thread.currentThread().getName()+"set的值是 "+value); ValueObject.value = value; lock.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } 消费者: public class C { private String lock; public C(String lock) { super(); this.lock = lock; } public void getVlue() { try { synchronized (lock) { while (ValueObject.value.equals("")) { System.out.println("消费者"+Thread.currentThread().getName()+"WAITING"); lock.wait(); } System.out.println("消费者"+Thread.currentThread().getName()+"get的值是 " + ValueObject.value); ValueObject.value = ""; lock.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } 操作值: public class ValueObject { public static String value = ""; } main方法: public class Run { public static void main(String[] args) { String lock = new String(); P p = new P(lock); C c = new C(lock); for (int i = 0; i < 2; i++) { new Thread(new Runnable() { @Override public void run() { while (true) { p.setValue(); } } }).start(); new Thread(new Runnable() { @Override public void run() { while (true) { c.getVlue(); } } }).start(); } } } 运行结果: ... 消费者Thread-1WAITING 消费者Thread-3WAITING 生产者Thread-0set的值是 1537255325047_1380972136738280 生产者Thread-0WAITING 消费者Thread-1get的值是 1537255325047_1380972136738280 消费者Thread-1WAITING 消费者Thread-3WAITING 生产者Thread-2set的值是 1537255325048_1380972137330390 生产者Thread-2WAITING 生产者Thread-0WAITING 运行结果显示,最后所有的线程都呈WAITING状态。为什么会出现这样的情况呢?在代码中已经 wait/notify 啊? 在代码中确实已经通过 wait / notify 进行呈通信了,但不保证 notify 唤醒的是异类,也许是同类,比如“生产者”唤醒“生产者”,或“消费者”唤醒“消费者”这样的情况。如果按这样情况运行的比率积少成多,就会导致所有的线程都不能继续运行下去,大家都在等待,都呈 WAITING 状态,程序最后也就呈“假死”的状态,不能继续运行下去了。 解决“假死”的情况很简单,将P.java和C.Java文件中的 notify() 改成 notifyAll() 方法即可,它的原理就是不光通知同类线程,也包括异类。这样就不至于出现假死的状态了,程序会一直运行下去。 3.通过管道进行线程间通信 字节流 在 Java 语言中提供了各种各样的输入 / 输出流Stream,使我们能够很方便地对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。通过使用管道,实现不同线程间的通信,而无须借助于类似临时文件之类的东西。 在 Java 的JDK中的IO包提供了4个类来使线程间可以进行通信: PipedInputStream 和 PipedOutputStream PipedReader 和 PipedWriter 下面来演示字节流的使用。 读线程: public class ReadThread extends Thread{ PipedInputStream inputStream; public ReadThread(PipedInputStream inputStream) { this.inputStream = inputStream; } @Override public void run() { readMethod(); } private void readMethod(){ try { System.out.println("Read :"); byte[] bytes = new byte[20]; int readLength = inputStream.read(bytes); while (readLength != -1){ String data = new String(bytes,0,readLength); System.out.print(data); readLength = inputStream.read(bytes); } System.out.println(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } 写线程: public class WriteThread extends Thread{ PipedOutputStream outputStream; public WriteThread(PipedOutputStream outputStream) { this.outputStream = outputStream; } @Override public void run() { readMethod(); } private void readMethod(){ try { System.out.println("write :"); for (int i=0;i<300;i++){ String data = ""+(i+1); outputStream.write(data.getBytes()); System.out.print(data); } System.out.println(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } 运行类: public class Run { public static void main(String[] args) throws InterruptedException, IOException { PipedOutputStream outputStream = new PipedOutputStream(); PipedInputStream inputStream = new PipedInputStream(); // inputStream.connect(outputStream); outputStream.connect(inputStream); ReadThread readThread = new ReadThread(inputStream); WriteThread writeThread = new WriteThread(outputStream); readThread.start(); Thread.sleep(2000); writeThread.start(); } } 打印结果: Read : write : 123456789101112131415161718192021222324... 123456789101112131415161718192021222324... 使用代码inputStream.connect(outputStream) 或 outputStream.connect(inputStream) 的作用使两个 Stream 之间产生通信链接,这样才可以将数据进行输入与输出。 但在此实验中,首先是读取线程启动,由于当时没有数据被写入。所以线程阻塞在 int readLength = inputStream.read(bytes) 代码中,直到有数据被写入,才继续向下运行。 字符流 写线程: public class WriteThread extends Thread{ PipedWriter outputStream; public WriteThread(PipedWriter outputStream) { this.outputStream = outputStream; } @Override public void run() { readMethod(); } private void readMethod(){ try { System.out.println("write :"); for (int i=0;i<300;i++){ String data = ""+(i+1); outputStream.write(data); System.out.print(data); } System.out.println(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } 读线程: public class ReadThread extends Thread{ PipedReader inputStream; public ReadThread(PipedReader inputStream) { this.inputStream = inputStream; } @Override public void run() { readMethod(); } private void readMethod(){ try { System.out.println("Read :"); char[] chars = new char[20]; int readLength = inputStream.read(chars); while (readLength != -1){ String data = new String(chars); System.out.print(data); readLength = inputStream.read(chars); } System.out.println(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } 运行类: public class Run { public static void main(String[] args) throws InterruptedException, IOException { PipedWriter outputStream = new PipedWriter(); PipedReader inputStream = new PipedReader(); // inputStream.connect(outputStream); outputStream.connect(inputStream); ReadThread readThread = new ReadThread(inputStream); WriteThread writeThread = new WriteThread(outputStream); readThread.start(); Thread.sleep(2000); writeThread.start(); } } 运行结果: Read : write : 123456789101112131415161718... 123456789101112131415161718... 打印的结果基本和前一个基本一样,此实验是在两个线程中通过管道流进行字符数据的传输。 嗨,你还在看吗? 文章来源:微信公众号 薛勤的博客
3.volatile关键字 关键字volatile的主要作用是使变量在多个线程间可见。 3.1 关键字volatile与死循环 如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在取得程序运行的结果上并没有多大的区别。如果一旦出现”多继承“的情况,则用实现Runable接口的方式来处理多线程的问题就是很有必要的。 public class PrintString implements Runnable{ private boolean isContinuePrint = true; @Override public void run() { while (isContinuePrint){ System.out.println("Thread: "+Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public boolean isContinuePrint() { return isContinuePrint; } public void setContinuePrint(boolean continuePrint) { isContinuePrint = continuePrint; } public static void main(String[] args) throws InterruptedException { PrintString printString = new PrintString(); Thread thread = new Thread(printString,"Thread-A"); thread.start(); Thread.sleep(100); System.out.println("我要停止它!" + Thread.currentThread().getName()); printString.setContinuePrint(false); } } 运行结果: Thread: Thread-A 我要停止它!main 上面的代码运行起来没毛病,但是一旦运行在 -server服务器模式中64bit的JVM上时,会出现死循环。解决的办法是使用volatile关键字。 关键字volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。 3.2 解决异步死循环 在研究volatile关键字之前先来做一个测试用例,代码如下: public class PrintString implements Runnable{ private boolean isRunnning = true; @Override public void run() { System.out.println("Thread begin: "+Thread.currentThread().getName()); while (isRunnning == true){ } System.out.println("Thread end: "+Thread.currentThread().getName()); } public boolean isRunnning() { return isRunnning; } public void setRunnning(boolean runnning) { isRunnning = runnning; } public static void main(String[] args) throws InterruptedException { PrintString printString = new PrintString(); Thread thread = new Thread(printString,"Thread-A"); thread.start(); Thread.sleep(1000); printString.setRunnning(false); System.out.println("我要停止它!" + Thread.currentThread().getName()); } } JVM有Client和Server两种模式,我们可以通过运行:java -version来查看jvm默认工作在什么模式。我们在IDE中把JVM设置为在Server服务器的环境中,具体操作只需配置运行参数为 -server。然后启动程序,打印结果: Thread begin: Thread-A 我要停止它!main 代码 System.out.println("Thread end: "+Thread.currentThread().getName());从未被执行。 是什么样的原因造成将JVM设置为-server就出现死循环呢? 在启动thread线程时,变量boolean isContinuePrint = true;存在于公共堆栈及线程的私有堆栈中。在JVM设置为-server模式时为了线程运行的效率,线程一直在私有堆栈中取得isRunning的值是true。而代码thread.setRunning(false);虽然被执行,更新的却是公共堆栈中的isRunning变量值false,所以一直就是死循环的状态。内存结构图: 这个问题其实就是私有堆栈中的值和公共堆栈中的值不同步造成的。解决这样的问题就要使用volatile关键字了,它主要的作用就是当线程访问isRunning这个变量时,强制性从公共堆栈中进行取值。 将代码更改如下: volatile private boolean isRunnning = true; 再次运行: Thread begin: Thread-A 我要停止它!main Thread end: Thread-A 通过使用volatile关键字,强制的从公共内存中读取变量的值,内存结构如图所示: 使用volatile关键字增加了实例变量在多个线程之间的可见性。但volatile关键字最致命的缺点是不支持原子性。 下面将关键字synchronized和volatile进行一下比较: 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。 volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。 再次重申一下,关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。 线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。 3.3 volatile非原子性的特征 关键字虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。 示例代码: public class MyThread extends Thread { volatile private static int count; @Override public void run() { addCount(); } private void addCount() { for (int i = 0;i<100;i++){ count++; } System.out.println(count); } public static void main(String[] args) { MyThread[] myThreads = new MyThread[100]; for (int i=0;i<100;i++){ myThreads[i] = new MyThread(); } for (int i=0;i<100;i++){ myThreads[i].start(); } } } 运行结果: ... 8253 8353 8153 8053 7875 7675 在addCount方法上加入synchronized同步关键字与static关键字,达到同步的效果。 再次运行结果: .... 9600 9700 9800 9900 10000 关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性。但在这里需要注意的是:如果修改实例变量中的数据,比如i++,也就是比 i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全。表达式i++的操作步骤分解为下面三步: 从内存中取i的值; 计算i的值; 将i值写入到内存中。 假如在第二步计算i值的时候,另外一个线程也修改i的值,那么这个时候就会脏数据。解决的方法其实就是使用synchronized关键字。所以说volatile关键字本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存中。 3.4 使用原子类进行i++操作 除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。 原子操作是不可分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。它可以在没有锁的情况下做到线程安全。 示例代码: public class MyThread extends Thread { private static AtomicInteger count = new AtomicInteger(0); @Override public void run() { addCount(); } private static void addCount() { for (int i = 0;i<100;i++){ System.out.println(count.incrementAndGet()); } } public static void main(String[] args) { MyThread[] myThreads = new MyThread[100]; for (int i=0;i<100;i++){ myThreads[i] = new MyThread(); } for (int i=0;i<100;i++){ myThreads[i].start(); } } } 打印结果: .... 9996 9997 9998 9999 10000 成功达到累加的效果。 3.5 原子类也不安全 原子类在具有有逻辑性的情况下输出结果也具有随机性。 示例代码: public class MyThread extends Thread { private static AtomicInteger count = new AtomicInteger(0); public MyThread(String name) { super(name); } @Override public void run() { this.addCount(); } private void addCount() { System.out.println(Thread.currentThread().getName()+"加100之后:"+count.addAndGet(100)); count.addAndGet(1); } public static void main(String[] args) throws InterruptedException { MyThread[] myThreads = new MyThread[10]; for (int i = 0; i < 10; i++) { myThreads[i] = new MyThread("Thread-"+i); } for (int i = 0; i < 10; i++) { myThreads[i].start(); } Thread.sleep(2000); System.out.println(MyThread.count); } } 打印结果: Thread-0加100之后:100 Thread-2加100之后:201 Thread-1加100之后:302 Thread-5加100之后:602 Thread-4加100之后:502 Thread-3加100之后:402 Thread-6加100之后:706 Thread-7加100之后:807 Thread-9加100之后:908 Thread-8加100之后:1009 1010 可以看到,结果值正确但是打印顺序出错了,出现这样的原因是因为AtomicInteger的addAndGet()方法是原子的,但方法与方法之间的调用却不是原子的。也就是方法addCount的调用不是原子的。解决这样的问题必须要用同步。 3.6 synchronized代码块有volatile同步的功能 关键字synchronized可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。 我们把前面讲到的异步死循环代码改造一下: public class PrintString implements Runnable{ private boolean isRunnning = true; @Override public void run() { String lock = new String(); System.out.println("Thread begin: "+Thread.currentThread().getName()); while (isRunnning == true){ synchronized (lock){ //加与不加的效果就是是否死循环 } } System.out.println("Thread end: "+Thread.currentThread().getName()); } public boolean isRunnning() { return isRunnning; } public void setRunnning(boolean runnning) { isRunnning = runnning; } public static void main(String[] args) throws InterruptedException { PrintString printString = new PrintString(); Thread thread = new Thread(printString,"Thread-A"); thread.start(); Thread.sleep(1000); printString.setRunnning(false); System.out.println("我要停止它!" + Thread.currentThread().getName()); } } 打印结果: Thread begin: Thread-A 我要停止它!main Thread end: Thread-A 关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥相和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。 学习多线程并发。要着重“外修互斥,内修可见”,这是掌握多线程、学习多线程并发的重要技术点。 文章来源:微信公众号 薛勤的博客
本文为《Java并发编程系列》第一章,主要介绍并发基础概念与API 1、进程和线程 一个程序就是一个进程,而一个程序中的多个任务则被称为线程。 进程是表示资源分配的基本单位,线程是进程中执行运算的最小单位,亦是调度运行的基本单位。 举个例子: 打开你的计算机上的任务管理器,会显示出当前机器的所有进程,QQ,360等,当QQ运行时,就有很多子任务在同时运行。比如,当你边打字发送表情,边好友视频时这些不同的功能都可以同时运行,其中每一项任务都可以理解成“线程”在工作。 2、使用多线程 在Java的JDK开发包中,已经自带了对多线程技术的支持,可以很方便地进行多线程编程。实现多线程编程的方式有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。使用继承 Thread 类创建线程,最大的局限就是不能多继承,所以为了支持多继承,完全可以实现 Runnable 接口的方式。需要说明的是,这两种方式在工作时的性质都是一样的,没有本质的区别。如下所示: 1.继承 Thread 类 public class MyThread extends Thread { @Override public void run() { //... } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } 2.实现 Runnable 接口 public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { //... } }).start(); } Thread.java 类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,也就是使线程得到运行,多线程是异步的,线程在代码中启动的顺序不是线程被调用的顺序。 Thread构造方法 Thread() 分配新的 Thread 对象。 Thread(Runnable target) 分配新的 Thread 对象。 Thread(Runnable target, String name) 分配新的 Thread 对象。 Thread(String name) 分配新的 Thread 对象。 Thread(ThreadGroup group, Runnable target) 分配新的 Thread 对象。 Thread(ThreadGroup group, Runnable target, String name) 分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,并作为 group 所引用的线程组的一员。 Thread(ThreadGroup group, Runnable target, String name, long stackSize) 分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,作为 group 所引用的线程组的一员,并具有指定的堆栈大小。 Thread(ThreadGroup group, String name) 分配新的 Thread 对象。 3、实例变量与线程安全 自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。当每个线程都有各自的实例变量时,就是变量不共享。共享数据的情况就是多个线程可以访问同一个变量。来看下面的示例:` public class MyThread implements Runnable { private int count = 5; @Override public void run() { count--; System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count); } } 以上代码定义了一个线程类,实现count变量减一的效果。运行类Runjava代码如下: public class Ruu { public static void main(String[] args) throws InterruptedException { MyThread myThread = new MyThread(); Thread a = new Thread(myThread,"A"); Thread b = new Thread(myThread,"B"); Thread c = new Thread(myThread,"C"); Thread d = new Thread(myThread,"D"); Thread e = new Thread(myThread,"E"); a.start(); b.start(); c.start(); d.start(); e.start(); } } 打印结果如下: 线程C 计算 count = 3 线程B 计算 count = 3 线程A 计算 count = 2 线程D 计算 count = 1 线程E 计算 count = 0 线程C,B的打印结果都是3,说明C和B同时对count进行了处理,产生了“非线程安全问题”。而我们想要的得到的打印结果却不是重复的,而是依次递减的。 在某些JVM中,i--的操作要分成如下3步: 取得原有变量的值。 计算i-1。 对i进行赋值。 在这三个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。 解决方法就是使用 synchronized 同步关键字 使各个线程排队执行run()方法。修改后的run()方法: public class MyThread implements Runnable { private int count = 5; @Override synchronized public void run() { count--; System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count); } } 打印结果: 线程B 计算 count = 4 线程C 计算 count = 3 线程A 计算 count = 2 线程E 计算 count = 1 线程D 计算 count = 0 关于System.out.println()方法 先来看System.out.println()方法源码: public void println(String x) { synchronized (this) { print(x); newLine(); } } 虽然println()方法内部使用 synchronized 关键字,但如下所示的代码在执行时还是有可能出现非线程安全问题的。 System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count--);原因在于println()方法内部同步,但 i-- 操作却是在进入 println()之前发生的,所以有发生非线程安全问题的概率。 4、多线程方法 1. currentThread()方法 currentThread()方法可返回代码段正在被哪个线程调用的信息。 Thread.currentThread().getName() 2. isAlive()方法 方法isAlive()的功能是判断当前的线程是否处于活动状态。 thread.isAlive(); 3. sleep()方法 方法sleep()的作用是在指定的毫秒数内让当前"正在执行的线程"休眠(暂停执行)。这个"正在执行的线程"是指this.currentThread()返回的线程。 Thread.sleep() 4. getId()方法 getId()方法的作用是取得线程的唯一标识。 thread.getId() 5、停止线程 停止线程是在多线程开发时很重要的技术点。停止线程并不像break语句那样干脆,需要一些技巧性的处理。 在Java中有以下3种方法可以终止正在运行的线程: 1)使用退出标志,使线程正常退出,也就是当run()方法完成后线程停止。 2)使用stop()方法强行终止线程,但是不推荐使用这个方法,因为该方法已经作废过期,使用后可能产生不可预料的结果。 3)使用interrupt()方法中断线程。 1.暴力法停止线程 调用stop()方法时会抛出 java.lang.ThreadDeath 异常,但在通常的情况下,此异常不需要显示地捕捉。 try { myThread.stop(); } catch (ThreadDeath e) { e.printStackTrace(); } 方法stop()已经被作废,因为如果强制让线程停止线程则有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的情况。示例如下: public class UserPass { private String username = "aa"; private String password = "AA"; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } synchronized public void println(String username, String password){ this.username = username; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.password = password; } public static void main(String[] args) throws InterruptedException { UserPass userPass = new UserPass(); Thread thread = new Thread(new Runnable() { @Override public void run() { userPass.println("bb","BB"); } }); thread.start(); Thread.sleep(500); thread.stop(); System.out.println(userPass.getUsername()+" "+userPass.getPassword()); } } 运行结果: bb AA 2.异常法停止线程 使用interrupt()方法并不会真正的停止线程,调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。 那我们如何判断该线程是否被打上了停止标记,Thread类提供了两种方法。 interrupted() 测试当前线程是否已经中断。 isInterrupted() 测试线程是否已经中断。 interrupted() 方法 不止可以判断当前线程是否已经中断,而且可以会清除该线程的中断状态。而对于isInterrupted() 方法,只会判断当前线程是否已经中断,不会清除线程的中断状态。 仅靠上面的两个方法可以通过while(!this.isInterrupted()){}对代码进行控制,但如果循环外还有其它语句,程序还是会继续运行的。这时可以抛出异常从而使线程彻底停止。示例如下: public class MyThread extends Thread { @Override public void run() { try { for (int i=0; i<50000; i++){ if (this.isInterrupted()) { System.out.println("已经是停止状态了!"); throw new InterruptedException(); } System.out.println(i); } System.out.println("不抛出异常,我会被执行的哦!"); } catch (Exception e) { // e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { MyThread myThread =new MyThread(); myThread.start(); Thread.sleep(100); myThread.interrupt(); } } 打印结果: ... 2490 2491 2492 2493 已经是停止状态了! 注意 如果线程在sleep()状态下被停止,也就是线程对象的run()方法含有sleep()方法,在此期间又执行了thread.interrupt() 方法,则会抛出java.lang.InterruptedException: sleep interrupted异常,提示休眠被中断。 3.return法停止线程 return法很简单,只需要把异常法中的抛出异常更改为return即可。代码如下: public class MyThread extends Thread { @Override public void run() { for (int i=0; i<50000; i++){ if (this.isInterrupted()) { System.out.println("已经是停止状态了!"); return;//替换此处 } System.out.println(i); } System.out.println("不进行return,我会被执行的哦!"); } } 不过还是建议使用“抛异常”来实现线程的停止,因为在catch块中可以对异常的信息进行相关的处理,而且使用异常能更好、更方便的控制程序的运行流程,不至于代码中出现多个return,造成污染。 6、暂停线程 暂停线程意味着此线程还可以恢复运行。在Java多线程中,可以使用 suspend() 方法暂停线程,使用 resume()方法恢复线程的执行。 这俩方法已经和stop()一样都被弃用了,因为如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。示例如下: public class MyThread extends Thread { private Integer i = 0; @Override public void run() { while (true) { i++; System.out.println(i); } } public Integer getI() { return i; } public static void main(String[] args) throws InterruptedException { MyThread myThread =new MyThread(); myThread.start(); Thread.sleep(100); myThread.suspend(); System.out.println("main end"); } } 打印结果: ... 3398 3399 3400 3401 执行上段程序永远不会打印main end。出现这样的原因是,当程序运行到 println() 方法内部停止时,PrintStream对象同步锁未被释放。方法 println() 源代码如下: public void println(String x) { synchronized (this) { print(x); newLine(); } } 这导致当前PrintStream对象的println() 方法一直呈“暂停”状态,并且锁未被myThread线程释放,而主线程中的代码System.out.println("main end") 还在傻傻的排队等待,导致迟迟不能运行打印。 使用 suspend() 和 resume() 方法也容易因为线程的暂停而导致数据不同步的情况,示例如下: public class UserPass2 { private String username = "aa"; private String password = "AA"; public String getUsername() { return username; } public String getPassword() { return password; } public void setValue(String username, String password){ this.username = username; if (Thread.currentThread().getName().equals("a")) { Thread.currentThread().suspend(); } this.password = password; } public static void main(String[] args) throws InterruptedException { UserPass2 userPass = new UserPass2(); new Thread(new Runnable() { @Override public void run() { userPass.setValue("bb","BB"); } },"a").start(); new Thread(new Runnable() { @Override public void run() { System.out.println(userPass.getUsername()+" "+userPass.getPassword()); } },"b").start(); } } 打印结果: bb AA 7、yield()方法 yield() 方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。 public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 8、线程的优先级 在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。 设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。 设置线程优先级使用setPriority()方法,此方法的JDK源码如下: public final void setPriority(int newPriority) { ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException(); } if((g = getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } setPriority0(priority = newPriority); } } 在Java中,线程优先级划分为1 ~ 10 这10个等级,如果小于1或大于10,则JDK抛出异常。 从JDK定义的3个优先级常量可知,线程优先级默认为5。 public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; 线程优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。 线程优先级具有规则性,线程的优先级与在代码中执行start()方法的顺序无关,与优先级大小有关。 线程优先级具有随机性,CPU尽量使线程优先级较高的先执行完,但无法百分百肯定。也就是说,线程优先级较高的不一定比线程优先级较低的先执行。 9、守护线程 在Java中有两种线程,一种是用户线程,一种守护线程。 什么是守护线程?守护线程是一种特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有了存在的必要了,自动销毁。可以简单地说:任何一个守护线程都是非守护线程的保姆。 如何设置守护线程?通过Thread.setDaemon(false)设置为用户线程,通过Thread.setDaemon(true)设置为守护线程。如果不设置属性,默认为用户线程。 thread.setDaemon(true); 示例如下: public class MyThread extends Thread { private int i = 0; @Override public void run() { try { while (true){ i++; System.out.println("i="+i); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.setDaemon(true); thread.start(); Thread.sleep(5000); System.out.println("我离开后thread对象也就不再打印了"); } } 打印结果: i=1 i=2 i=3 i=4 i=5 我离开后thread对象也就不再打印了 参考与总结 《Java多线程编程核心技术》高洪岩 著 本文主要介绍了Thread类的API,算是为学习多线程更深层次知识打下一些基础,文章若有错误请在评论区指正。 文章来源:微信公众号 薛勤的博客
单体应用确实有问题! 最近在研究微服务架构,有一点点心得 这个话题有点大,我会分几篇文章和大家慢慢说,今天就先来说说传统的单体应用有哪些弊端,正是因为单体应用存在的弊端,使得我们不得不考虑发展微服务。 人类发展的历史就是一个社会分工不断细化的历史,从这个角度来讲,微服务这种将一个复杂的大项目拆分为众多小项目,然后程序员分工合作,共同完成项目,这种协作方式是符合历史潮流的。 这是我们站在今天的角度来说的,曾经的单体应用也是先进生产力的代表。 但是,随着互联网的发展,我们对一个系统的要求越来越高,单体应用已经很难适应当前的开发,因此在回答我们为什么要使用微服务这个问题之前,我们有必要来聊一聊单体应用目前都面临哪些问题。 面临的问题 1.项目过度复杂 你要创建一个简单的用户管理系统,二话不说,直接创建 Maven 项目然后开干就完事了,这没问题,因为这很简单。 但是你要说想搞一个淘宝网站,或者你想搞一个用友 U8 系统,那你恐怕就得先慢慢设计系统架构了。单体应用,由于就是一个项目,所有的功能都是写在一个项目中,不可避免的出现项目过度复杂的情况。而且这种复杂情况会不断恶化。 有的小伙伴可能有这样的经验,刚入职了一家公司,新接手了一个项目,上面催的很急,让你赶快修复几个 bug ,项目复杂,光是实体类的包就有好几个 bean、model、pojo 等,一个项目被很多人经手之后,到你手里,早已经一团乱麻,你小心翼翼尽量不碰触到已有的功能,终于修完了几个 bug,搞了俩礼拜,你觉得这个项目太坑爹了,不想干了,于是接盘侠从你手里接到了一个复杂度又上升了一步的项目。 就这样,一个原本简简单单的单体项目,在变复杂的路上一去不复返。 2.开发速度缓慢 单体应用开发速度缓慢,因为单体应用复杂了之后,项目变得异常臃肿而且庞大,每一次编译构建、运行以及测试,都需要花费大量时间,而且如果测试有问题,又得从头来一遍,注意,这里的每一次从头编译构建等都是整个项目的从头编译构建。 即使你可能只要修改某一个参数,你也得把上面整个流程走一遍,相当于每一次的修改都是牵一发而动全身的操作。 速度没法快。 3.不易扩展 项目中不同模块对计算机的性能要求不一样,例如使用 Redis 来保存了大量的热点数据,那么我们希望服务器的内存非常大,另外有一个模块涉及到了图片处理,我们又希望服务器的 CPU 非常强,如果是单体应用部署的话,那么这些条件服务器都要满足。 4.技术栈不易扩展 单体应用还有一个劣势就是技术栈不易扩展,一旦你选定了某一个技术栈来开发项目,以后很难在技术栈上做切换。有的公司还会自己搞一套系统,这种在当时看起来好像都没有啥问题,可是经过几年之后,回头再看,已经很过时了,很 low 了,当初设计系统的人可能已经离职了,刚入职的新手也不敢动这个老古董,只能在这个老古董上面忍痛开发。 有的时候,有一个服务需要处理高并发,你很想用 Go 语言来做,可是做不到,没法引入其他语言。 这些都是单体应用的劣势,如果有微服务,上面这些问题都将得到解决。 曾经的优势 当然,事物都是有两面性的,单体应用也有它自己的优势,例如: 开发简单,一个 IDE 就可以快速构建出一个单体应用 测试简单 部署简单,Tomcat 安装好之后,应用扔上去就行了 集群化部署也很容易,多个 Tomcat + 一个 Nginx 分分钟就搭建好集群环境了 这么多优势,还是难掩劣势。 不过大家在做项目的时候,还是要结合实际情况来选择,不能因为微服务厉害,所有项目都是微服务,如果你仅仅只想做一个用户的增删改查,那么很明显,创建一个简单的单体应用是最合适的。 好了,本文主要和大家分享了传统单体应用存在的一些问题,正是因为这些问题,我们需要引入微服务,下篇文章,我们就来看看微服务有哪些优势。 参考资料: [1] Chris Richardson.微服务架构设计模式[M].北京:机械工业出版社,2019. 文章来源:微信公众号 江南一点雨
导读:在业界,近些年来机器学习在人机对弈、语音识别、图像识别等场景下取得了蓬勃发展,引发了人们对人工智能改造未来社会的无限热情和期待。但在学界,却有不少科学家指出了机器学习的发展局限。加拿大滑铁卢大学教授Shai Ben-David探索的就是这样一个机器学习的本质问题:我们能不能判定人工智能的可学习性? Shai Ben-David通过研究给出的答案是:不一定!他指出,如果一个问题只需要“是”或“否”的回答,我们还是可以确切地知道这个问题可否被机器学习算法解决。但是,一旦涉及到更一般的设置时,我们就无法区分可学习和不可学习的任务了。 那么机器学习都能解决哪些问题?让我们回归本质,探讨一下机器学习最基础知识及应用。 作者:沙伊·沙莱夫施瓦茨(Shai Shalev-Shwartz)、沙伊·本戴维(Shai Ben-David)如需转载请联系大数据(ID:hzdashuju) 01 什么是学习 我们首先来看几个存在于大自然的动物学习的例子。从这些熟悉的例子中可以看出,机器学习的一些基本问题也存在于自然界。 1. 怯饵效应——老鼠学习躲避毒饵 当老鼠遇到有新颖外观或气味的食物时,它们首先会少量进食,随后的进食量将取决于事物本身的风味及其生理作用。如果产生不良反应,那么新的食物往往会与这种不良后果相关联,随之,老鼠不再进食这种食物。很显然,这里有一个学习机制在起作用——动物通过经验来获取判断食物安全性的技能。如果对一种食物过去的经验是负标记的,那么动物会预测在未来遇到它时也会产生负面影响。 前文的示例解释了什么是学习成功,下面我们再举例说明什么是典型的机器学习任务。假设我们想对一台机器进行编程,使其学会如何过滤垃圾邮件。一个最简单的解决方案是仿照老鼠学习躲避毒饵的过程。机器只须记住所有以前被用户标记为垃圾的邮件。当一封新邮件到达时,机器将在先前垃圾邮件库中进行搜索。如果匹配其中之一,它会被丢弃。否则,它将被移动到用户的收件箱文件夹。 虽然上述“通过记忆进行学习”的方法时常是有用的,但是它缺乏一个学习系统的重要特性——标记未见邮件的能力。一个成功的学习器应该能够从个别例子进行泛化,这也称为归纳推理。在“怯饵效应”例子中,老鼠遇到一种特定类型的食物后,它们会对新的、没见过的、有相似气味和口味的食物采取同样的态度。 为了实现垃圾邮件过滤任务的泛化,学习器可以扫描以前见过的电子邮件,并提取那些垃圾邮件的指示性的词集;然后,当新电子邮件到达时,这台机器可以检查它是否含有可疑的单词,并相应地预测它的标签。这种系统应该有能力正确预测未见电子邮件的标签。 但是,归纳推理有可能推导出错误的结论。为了说明这一点,我们再来思考一个动物学习的例子。 2. 鸽子迷信 心理学家B. F. Skinner进行过一项实验,他在笼子里放了一群饥饿的鸽子。笼子上附加了一个自动装置,不管鸽子当时处于什么行为状态,都会以固定的时间间隔为它们提供食物。饥饿的鸽子在笼子里走来走去,当食物第一次送达时,每只鸽子都在进行某项活动(啄食、转动头部等)。食物的到来强化了它们各自特定的行为,此后,每只鸟都倾向于花费更多的时间重复这种行为。 接下来,随机的食物送达又增加了每只鸟做出这种行为的机会。结果是,不管第一次食物送达时,每只鸟处于什么行为状态,这一连串的事件都增强了食物送达和这种行为之间的关联。进而,鸽子们也更勤奋地做出这种行为。 了解更多: http://psychclassics.yorku.ca/Skinner/Pigeon 有用的学习机制与形成迷信的学习机制有何差别?这个问题对自动学习器的发展至关重要。尽管人类可以依靠常识来滤除随机无意义的学习结论,但是一旦我们将学习任务付之于一台机器,就必须提供定义明确、清晰的规则,来防止程序得出无意义或无用的结论。发展这些规则是机器学习理论的一个核心目标。 是什么使老鼠的学习比鸽子更成功?作为回答这个问题的第一步,我们仔细看一下老鼠在“怯饵效应”实验中的心理现象。 3. 重新审视“怯饵效应”——老鼠未能获得食物与电击或声音与反胃之间的关联 老鼠的怯饵效应机制可能比你想象中的更复杂。Garcia进行的实验(Garcia & Koelling 1996)表明,当进食后伴随的是不愉快的刺激时,比如说电击(不是反胃反应),那么关联没有出现。即使将进食后电击的机制重复多次,老鼠仍然倾向于进食。 同样,食物引起的反胃(口味或气味)与声音之间的关联实验也失败了。老鼠似乎有一些“内置的”先验知识,告诉它们,虽然食物和反胃存在因果相关,但是食物与电击或声音与反胃之间不太可能存在因果关系。 由此我们得出结论,怯饵效应和鸽子迷信的一个关键区别点是先验知识的引入使学习机制产生偏差,也称为“归纳偏置”。在实验中,鸽子愿意采取任何食物送达时发生的行为。然而,老鼠“知道”食物不能导致电击,也知道与食物同现的噪音不可能影响这种食物的营养价值。老鼠的学习过程偏向于发现某种模式,而忽略其他的关联。 事实证明,引入先验知识导致学习过程产生偏差,这对于学习算法的成功必不可少(正式陈述与证明参见“没有免费的午餐”定理)。这种方法的发展,即能够表示领域知识,将其转化为一个学习偏置,并量化偏置对学习成功的影响,是机器学习理论的一个核心主题。粗略地讲,具有的先验知识(先验假设)越强,越容易从样本实例中进行学习。但是,先验假设越强,学习越不灵活——受先验假设限制。 02 什么时候需要机器学习 什么时候需要机器学习,而不是直接动手编程完成任务?在指定问题中,程序能否在“经验”的基础上自我学习和提高,有两方面的考量:问题本身的复杂性和对自适应性的需要。 1. 过于复杂的编程任务 动物/人可执行的任务:虽然人类可以习惯性地执行很多任务,但是反思我们如何完成任务的内省机制还不够精细,无法从中提取一个定义良好的程序。汽车驾驶、语音识别和图像识别都属于此类任务。面对此类任务,只要接触到足够多的训练样本,目前最先进的机器学习程序,即能“从经验中学习”的程序,就可以达到比较满意的效果。 超出人类能力的任务:受益于机器学习技术,另一大系列任务都涉及对庞大且复杂的数据集进行分析:天文数据,医疗档案转化为医学知识,气象预报,基因组数据分析,网络搜索引擎和电子商务。随着越来越多的数字数据的出现,显而易见的是,隐含在数据里的有意义、有价值的信息过于庞大复杂,超出了人类的理解能力。学习在大量复杂数据中发现有意义的模式是一个有前途的领域,无限内存容量加上不断提高的处理速度,更为这一领域开辟了新的视野。 2. 自适应性 编程的局限之一是其刻板性——一旦程序的编写与安装完成,它将保持不变。但是,任务会随着时间的推移而改变,用户也会出现变更。机器学习方法——其行为自适应输入数据的程序——为这个难题提供了一个解决方案。机器学习方法天生具备自适应于互动环境变化的性质。 机器学习典型的成功应用有:能够适应不同用户的手写体识别,自动适应变化的垃圾邮件检测,以及语音识别。 03 学习的种类 学习是一个非常广泛的领域。因此,机器学习根据学习任务的不同分为不同的子类。这里给出一个粗略的分类,下面给出四种分类方式。 1. 监督与无监督 学习涉及学习器与环境之间的互动,那么可以根据这种互动的性质划分学习任务。首先需要关注的是监督学习与无监督学习之间的区别。 下面以垃圾邮件检测和异常检测为例说明。对于垃圾邮件检测任务,学习器的训练数据是带标签的邮件(是/否垃圾邮件)。在这种训练的基础上,学习器应该找出标记新电子邮件的规则。相反,对于异常检测任务,学习器的训练数据是大量没有标签的电子邮件,学习器的任务是检测出“不寻常”的消息。 抽象一点来讲,如果我们把学习看做一个“利用经验获取技能”的过程,那么监督学习正是这样的一种场景:经验是包含显著信息(是/否垃圾邮件)的训练数据,“测试数据”缺少这些显著信息,但可从学到的“技能”中获取。 此种情况下,获得的“技能”旨在预测测试数据的丢失信息,我们可以将环境看做通过提供额外信息(标签)来“监督”学习器的老师。然而,无监督学习的训练数据和测试数据之间没有区别。学习器处理输入数据的目标是提取概括信息(浓缩数据)。聚类(相似数据归为一类)是执行这样任务的一个典型例子。 还有一种中间情况,训练数据比测试数据包含更多的信息,也要求学习器预测更多信息。举个例子,当学习数值函数判断国际象棋游戏中白棋和黑棋谁更有利时,训练过程中提供给学习器的唯一信息是,谁在整个实际的棋牌类游戏中最终赢得那场比赛的标签。这种学习被称作“强化学习”。 2. 主动学习器与被动学习器 学习可依据学习器扮演的角色不同分类为“主动”和“被动”学习器。主动学习器在训练时通过提问或实验的方式与环境交互,而被动学习器只观察环境(老师)所提供的信息而不影响或引导它。请注意,垃圾邮件过滤任务通常是被动学习——等待用户标记电子邮件。我们可以设想,在主动学习中,要求用户来标记学习器挑选的电子邮件,以提高学习器对“垃圾邮件是什么”的理解。 3. 老师的帮助 人类的学习过程中(在家的幼儿或在校的学生)往往会有一个良师,他向学习者传输最有用的信息以实现学习目标。相比之下,科学家研究自然时,环境起到了老师的作用。环境的作用是消极的——苹果坠落、星星闪烁、雨点下落从不考虑学习者的需求。 在对这种学习情境建模时,我们假定训练数据(学习者的经验)是由随机过程产生的,这是统计机器学习的一个基本构成单元。此外,学习也发生在学习者的输入是由对立“老师”提供的。 垃圾邮件过滤任务(如果垃圾邮件制作者尽力误导垃圾邮件过滤器设计者)和检测欺诈学习任务就是这种情况。当不存在更好的假设时,我们也会使用对立老师这一最坏方案。如果学习器能够从对立老师中学习,那么遇到任何老师都可以成功。 4. 在线与批量 在线响应还是处理大量数据后才获得技能,是对学习器的另一种分类方式。举个例子,股票经纪人必须基于当时的经验信息做出日常决策。随着时间推移,他或许会成为专家,但是也会犯错并付出高昂的代价。相比之下,在大量的数据挖掘任务中,学习器,也就是数据挖掘器,往往是在处理大量训练数据之后才输出结论。 04 与其他领域的关系 作为一门交叉学科,机器学习与统计学、信息论、博弈论、最优化等众多数学分支有着共同点。我们的最终目标是在计算机上编写程序,所以机器学习自然也是计算机科学的一个分支。在某种意义上,机器学习可以视为人工智能的一个分支,毕竟,要将经验转变成专业知识或从复杂感知数据中发现有意义的模式的能力是人类和动物智能的基石。 但是,应该注意的是,与传统人工智能不同,机器学习并不是试图自动模仿智能行为,而是利用计算机的优势和特长与人类的智慧相得益彰。机器学习常用于执行远远超出人类能力的任务。例如,机器学习程序通过浏览和处理大型数据,能够检测到超出人类感知范围的模式。 机器学习(的经验)训练涉及的数据往往是随机生成的。机器学习的任务就是处理这些背景下的随机生成样本,得出与背景相符的结论。这样的描述强调了机器学习与统计学的密切关系。两个学科之间确实有很多共同点,尤其表现在目标和技术方面。 但是,两者之间仍然存在显著的差别:如果一个医生提出吸烟与心脏病之间存在关联这一假设,这时应该由统计学家去查看病人样本并检验假设的正确性(这是常见的统计任务——假设检验)。相比之下,机器学习的任务是利用患者样本数据找出心脏病的原因。我们希望自动化技术能够发现被人类忽略的、有意义的模式(或假设)。 与传统统计学不同,算法在机器学习中扮演了重要的角色。机器学习算法要靠计算机来执行,因此算法问题是关键。我们开发算法完成学习任务,同时关心算法的计算效率。两者的另外一个区别是,统计关心算法的渐近性(如随着样本量增长至无穷大,统计估计的收敛问题),机器学习理论侧重于有限样本。也就是说,给定有限可用样本,机器学习理论旨在分析学习器可达到的准确度。 机器学习与统计学之间还有很多差异,我们在此仅提到了少数。比如,在统计学中,常首先提出数据模型假设(生成数据呈正态分布或依赖函数为线性);在机器学习中常考虑“非参数”背景,对数据分布的性质假设尽可能地少,学习算法自己找出最接近数据生成过程的模型。 关于作者:沙伊·沙莱夫-施瓦茨(Shai Shalev-Shwartz),以色列希伯来大学计算机及工程学院副教授,还在Mobileye公司研究自动驾驶。2009年之前他在芝加哥的丰田技术研究所工作。他的研究方向是机器学习算法。 沙伊·本-戴维(Shai Ben-David),加拿大滑铁卢大学计算机科学学院教授。先后在以色列理工学院、澳大利亚国立大学和康奈尔大学任教。 本文摘编自《深入理解机器学习:从原理到算法》,经出版方授权发布。 文章来源:微信公众号 大数据
【导语】学习逻辑回归模型,今天的内容轻松带你从0到100!阿里巴巴达摩院算法专家、阿里巴巴技术发展专家、阿里巴巴数据架构师联合撰写,从技术原理、算法和工程实践3个维度系统展开,既适合零基础读者快速入门,又适合有基础读者理解其核心技术;写作方式上避开了艰涩的数学公式及其推导,深入浅出。 0、前言 简单理解逻辑回归,就是在线性回归基础上加一个 Sigmoid 函数对线性回归的结果进行压缩,令其最终预测值 y 在一个范围内。这里 Sigmoid 函数的作用就是将一个连续的数值压缩到一定范围内,它将最终预测值 y 的范围压缩到在 0 到 1 之间。虽然逻辑回归也有回归这个词,但由于这里的自变量和因变量呈现的是非线性关系,因此严格意义上讲逻辑回归模型属于非线性模型。逻辑回归模型通常用来处理二分类问题,如图 4-4 所示。在逻辑回归中,计算出的预测值是一个 0 到 1 的概率值,通常的,我们以 0.5 为分界线,如果预测的概率值大于 0.5 则会将最终结果归为 1 这个类别,如果预测的概率值小于等于 0.5 则会将最终结果归为 0 这个类别。而 1 和 0 在实际项目中可能代表了很多含义,比如 1 代表恶性肿瘤,0 代表良性肿瘤,1 代表银行可以给小王贷款,0 代表银行不能给小王贷款等等。 图4-4 逻辑回归分类示意图 虽然逻辑回归很简单,但它被广泛应用在实际生产之中,而且通过改造逻辑回归也可以处理多分类问题。逻辑回归不仅本身非常受欢迎,它同样也是我们将在第 5 章介绍的神经网络的基础。普通神经网络中,常常使用 Sigmoid 对神经元进行激活。关于神经网络的神经元,第 5 章会有详细的介绍(第 5 章会再次提到 Sigmoid 函数),这里只是先提一下逻辑回归和神经网络的关系,读者有个印象。 1、Sigmoid 函数 Sigmoid 的函数表达式如下: 该公式中,e 约等于 2.718,z 则是线性回归的方程式,p 为计算出来的概率,范围在 0 到 1 之间。接下来我们将这个函数绘制出来,看看它的形状。使用 Python 的 Numpy 以及 Matplotlib 库进行编写,代码如下: import numpy as np import matplotlib.pyplot as plt def sigmoid(x): y = 1.0 / (1.0 + np.exp(-x)) return y plot_x = np.linspace(-10, 10, 100) plot_y = sigmoid(plot_x) plt.plot(plot_x, plot_y) plt.show() 效果如图 4-5 所示: 图4-5 Sigmoid函数 我们对上图做一个解释,当 x 为 0 的时候,Sigmoid 函数值为 0.5,随着 x 的不断增大,对应的 Sigmoid 值将无线逼近于 1;而随着 x 的不断的减小,Sigmoid 值将不断逼近于 0 。所以它的值域是在 (0,1) 之间。由于 Sigmoid 函数将实数范围内的数值压缩到(0,1)之间,因此也被称为压缩函数。但这里多提一下,压缩函数其实可以有很多,比如 tanh 可以将实数范围内的数值压缩到(-1,1)之间,因此 tanh 有时也会被成为压缩函数。 2、 梯度下降法 在学习 4.1.1 小节的时候,我们在介绍一元线性回归模型的数学表达之后又介绍了一元线性回归模型的训练过程。类似的,在 4.2.1 小节学习完逻辑回归模型的数学表达之后我们来学习逻辑回归模型的训练方法。首先与 4.1.1 小节类似,我们首先需要确定逻辑回归模型的评价方式,也就是模型的优化目标。有了这个目标,我们才能更好地“教”模型学习出我们想要的东西。这里的目标也和 4.1.1 一样,定义为 接下来是选择优化这个目标的方法,也就是本小节中重点要介绍的梯度下降法。 首先带大家简单认识一下梯度下降法。梯度下降算法(Gradient Descent Optimization)是常用的最优化方法之一。“最优化方法”属于运筹学方法,它指在某些约束条件下,为某些变量选取哪些的值,使得设定的目标函数达到最优的问题。最优化方法有很多,常见的有梯度下降法、牛顿法、共轭梯度法等等。由于本书重点在于带大家快速掌握“图像识别”技能,因此暂时不对最优化方法进行展开,感兴趣的读者可以自行查阅相关资料进行学习。由于梯度下降是一种比较常见的最优化方法,而且在后续第 5 章、第 7 章的神经网络中我们也将用到梯度下降来进行优化,因此我们将在本章详细介绍该方法。 接下来我们以图形化的方式带领读者学习梯度下降法。 我们在 Pycharm 新建一个 python 文件,然后键入以下代码: import numpy as np import matplotlib.pyplot as plt if __name__ == '__main__': plot_x = np.linspace(-1, 6, 141) #从-1到6选取141个点 plot_y = (plot_x - 2.5) ** 2 – 1 #二次方程的损失函数 plt.scatter(plot_x[5], plot_y[5], color='r') #设置起始点,颜色为红色 plt.plot(plot_x, plot_y) # 设置坐标轴名称 plt.xlabel('theta', fontproperties='simHei', fontsize=15) plt.ylabel('损失函数', fontproperties='simHei', fontsize=15) plt.show() 通过上述代码,我们就能画出如图 4-6 所示的损失函数示意图,其中 x 轴代表的是我们待学习的参数 (theta),y 轴代表的是损失函数的值(即 loss 值),曲线 y 代表的是损失函数。我们的目标是希望通过大量的数据去训练和调整参数,使损失函数的值最小。想要达到二次方程的最小值点,可以通过求导数的方式,使得导数为 0 即可。也就是说,横轴上 2.5 的位置对应损失最小,在该点上一元二次方程 切线的斜率则为 0。暂且将导数描述为 ,其中 J 为损失函数,为待求解的参数。 梯度下降中有个比较重要的参数:学习率 (读作eta,有时也称其为步长),它控制着模型寻找最优解的速度。加入学习率后的数学表达为 。 图4-6 损失函数示意图 接下来我们画图模拟梯度下降的过程。 首先定义损失函数及其导数 def J(theta): #损失函数 return (theta-2.5)**2 -1 def dJ(theta): #损失函数的导数 return 2 * (theta - 2.5) 通过 Matplotlib 绘制梯度下降迭代过程,具体代码如下: theta = 0.0 #初始点 theta_history = [theta] eta = 0.1 #步长 epsilon = 1e-8 #精度问题或者eta的设置无法使得导数为0 while True: gradient = dJ(theta) #求导数 last_theta = theta #先记录下上一个theta的值 theta = theta - eta * gradient #得到一个新的theta theta_history.append(theta) if(abs(J(theta) - J(last_theta)) < epsilon): break #当两个theta值非常接近的时候,终止循环 plt.plot(plot_x,J(plot_x),color='r') plt.plot(np.array(theta_history),J(np.array(theta_history)),color='b',marker='x') plt.show() #一开始的时候导数比较大,因为斜率比较陡,后面慢慢平缓了 print(len(theta_history)) #一共走了46步 我们来看下所绘制的图像是什么样子的,可以观察到 从初始值 0.0 开始不断的向下前进,一开始的幅度比较大,之后慢慢趋于缓和,逐渐接近导数为 0,一共走了 46 步。如图 4-7 所示: 图4-7 一元二次损失函数梯度下降过程示意图 3、学习率的分析 上一小节我们主要介绍了什么是梯度下降法,本小节主要介绍学习率对于梯度下降法的影响。 第一个例子,我们将 设置为 0.01(之前是 0.1 ),我们会观察到,步长减少之后,蓝色的标记更密集,说明步长减少之后,从起始点到导数为 0 的步数增加了。步数变为了 424 步,这样整个学习的速度就变慢了。效果如图 4-8 所示: 图4-8 学习率时,一元二次损失函数梯度下降过程示意图 第二个例子,我们将 设置为 0.8,我们会观察到,代表蓝色的步长在损失函数之间跳跃了,但在跳跃过程中,损失函数的值依然在不断的变小。步数是 22 步,因此当学习率为 0.8 时,优化过程时间缩短,但是最终也找到了最优解。效果如图 4-9 所示: 图4-9 学习率 时,一元二次损失函数梯度下降过程示意图 第三个例子,我们将设置为1.1,看一下效果。这里注意,学习率本身是一个 0 到 1 的概率,因此 1.1 是一个错误的值,但为了展示梯度过大会出现的情况,我们暂且用这个值来画图示意。我们会发现程序会报这个错误 OverflowError: ( 34, 'Result too large' )。我们可以想象得到,这个步长跳跃的方向导致了损失函数的值越来越大,所以才报了“Result too large”效果,我们需要修改下求损失函数的程序: def J(theta): try: return (theta-2.5)**2 -1 except: return float('inf') i_iter= 0 n_iters = 10 while i_iter < n_iters: gradient = dJ(theta) last_theta = theta theta = theta - eta * gradient i_iter += 1 theta_history.append(theta) if (abs(J(theta) - J(last_theta)) < epsilon): break # 当两个theta值非常接近的时候,终止循环 另外我们需要增加一下循环的次数。 我们可以很明显的看到,我们损失函数在最下面,学习到的损失函数的值在不断的增大,也就是说模型不会找到最优解。如图 4-10 所示: 图4-10 学习率时,一元二次损失函数不收敛 通过本小节的几个例子,简单讲解了梯度下降法,以及步长 的作用。从三个实验我们可以看出,学习率是一个需要认真调整的参数,过小会导致收敛过慢,而过大可能导致模型不收敛。 4、逻辑回归的损失函数 逻辑回归中的 Sigmoid 函数用来使值域在(0,1)之间,结合之前所讲的线性回归,我们所得到的完整的公式其实是:,其中的 就是之前所介绍的多元线性回归。 现在的问题就比较简单明了了,对于给定的样本数据集 X,y,我们如何找到参数 theta ,来获得样本数据集 X 所对应分类输出 y(通过p的概率值) 需要求解上述这个问题,我们就需要先了解下逻辑回归中的损失函数,假设我们的预测值为: 损失函数假设为下面两种情况,y 表示真值;表示为预测值: 结合上述两个假设,我们来分析下,当 y 真值为 1 的时候,p 的概率值越小(越接近0),说明y的预测值偏向于0,损失函数 cost 就应该越大;当 y 真值为 0 的时候,如果这个时候 p 的概率值越大则同理得到损失函数 cost 也应该越大。在数学上我们想使用一个函数来表示这种现象,可以使用如下这个: 我们对上面这个函数做一定的解释,为了更直观的观察上述两个函数,我们通过 Python 中的 Numpy 以及 Matplotlib 库进行绘制。 我们先绘制下 ,代码如下: import numpy as np import matplotlib.pyplot as plt def logp(x): y = -np.log(x) return y plot_x = np.linspace(0.001, 1, 50) #取0.001避免除数为0 plot_y = logp(plot_x) plt.plot(plot_x, plot_y) plt.show() 如下图4-9所示: 图4-9 损失函数if y=1 当p=0的时候,损失函数的值趋近于正无穷,根据说明y的预测值偏向于0,但实际上我们的 y 真值为 1 。当 p 达到 1 的时候,y 的真值和预测值相同,我们能够从图中观察到损失函数的值趋近于 0 代表没有任何损失。 我们再来绘制一下,代码如下: import numpy as np imort matplotlib.pyplot as plt def logp2(x): y = -np.log(1-x) return y plot_x = np.linspace(0, 0.99, 50) #取0.99避免除数为0 plot_y = logp2(plot_x) plt.plot(plot_x, plot_y) plt.show() 效果如图4-10所示: 图4-10 损失函数 if y=0 当p=1的时候,损失函数的值趋近于正无穷,根据说明y的预测值 偏向于1,但实际上我们的 y 真值为 0 。当 p 达到 0 的时候,y 的真值和预测值相同,我们能够从图中观察到损失函数的值趋近于 0 代表没有任何损失。 我们再对这两个函数稍微整理下,使之合成一个损失函数: 公式如下: 当公式变为上述的时候,对于我们来说,只需要求解一组使得损失函数最小就可以了,那么对于如此复杂的损失函数,我们一般使用的是梯度下降法进行求解。 5、Python实现逻辑回归 结合之前讲的理论,本小节开始动手实现一个逻辑回归算法。首先我们定义一个类,名字为 LogisticRegressionSelf ,其中初始化一些变量:维度、截距、theta 值,代码如下: class LogisticRegressionSelf: def __init__(self): """初始化Logistic regression模型""" self.coef_ = None #维度 self.intercept_ = None #截距 self._theta = None 接着我们实现下在损失函数中的 这个函数,我们之前在 Sigmoid 函数那个小节已经实现过了,对于这个函数我们输入的值为多元线性回归中的(其中恒等于1),为了增加执行效率,我们建议使用向量化来处理,而尽量避免使用 for 循环,所以对于我们使用来代替,具体代码如下: def _sigmoid(x): y = 1.0 / (1.0 + np.exp(-x)) return y 接着我们来实现损失函数, 代码如下: #计算损失函数 def J(theta,X_b,y): p_predcit = self._sigmoid(X_b.dot(theta)) try: return -np.sum(y*np.log(p_predcit) + (1-y)*np.log(1-p_predcit)) / len(y) except: return float('inf') 然后我们需要实现下损失函数的导数。具体求导过程读者可以自行百度,我们这边直接给出结论,对于损失函数cost,得到的导数值为: ,其中,之前提过考虑计算性能尽量避免使用 for 循环实现累加,所以我们使用向量化计算。 完整代码如下: import numpy as np class LogisticRegressionSelf: def __init__(self): """初始化Logistic regression模型""" self.coef_ = None #维度 self.intercept_ = None #截距 self._theta = None #sigmoid函数,私有化函数 def _sigmoid(self,x): y = 1.0 / (1.0 + np.exp(-x)) return y def fit(self,X_train,y_train,eta=0.01,n_iters=1e4): assert X_train.shape[0] == y_train.shape[0], '训练数据集的长度需要和标签长度保持一致' #计算损失函数 def J(theta,X_b,y): p_predcit = self._sigmoid(X_b.dot(theta)) try: return -np.sum(y*np.log(p_predcit) + (1-y)*np.log(1-p_predcit)) / len(y) except: return float('inf') #求sigmoid梯度的导数 def dJ(theta,X_b,y): x = self._sigmoid(X_b.dot(theta)) return X_b.T.dot(x-y)/len(X_b) #模拟梯度下降 def gradient_descent(X_b,y,initial_theta,eta,n_iters=1e4,epsilon=1e-8): theta = initial_theta i_iter = 0 while i_iter < n_iters: gradient = dJ(theta,X_b,y) last_theta = theta theta = theta - eta * gradient i_iter += 1 if (abs(J(theta,X_b,y) - J(last_theta,X_b,y)) < epsilon): break return theta X_b = np.hstack([np.ones((len(X_train),1)),X_train]) initial_theta = np.zeros(X_b.shape[1]) #列向量 self._theta = gradient_descent(X_b,y_train,initial_theta,eta,n_iters) self.intercept_ = self._theta[0] #截距 self.coef_ = self._theta[1:] #维度 return self def predict_proba(self,X_predict): X_b = np.hstack([np.ones((len(X_predict), 1)), X_predict]) return self._sigmoid(X_b.dot(self._theta)) def predict(self,X_predict): proba = self.predict_proba(X_predict) return np.array(proba > 0.5,dtype='int') 小结 以上内容主要讲述了线性回归模型和逻辑回归模型,并做了相应的实现。其中线性回归是逻辑回归的基础,而逻辑回归经常被当做神经网络的神经元,因此逻辑回归又是神经网络的基础。我们借逻辑回归模型介绍了机器学习中离不开的最优化方法,以及最常见的最优化方法——梯度下降。了解本节内容会对接下来第 5 章神经网络的学习有着很大的帮助。本文摘自《深度学习与图像识别:原理与实践》,经出版方授权发布。 文章来源:微信公众号 AI科技大本营
通过大量的实践经验,我们总结出10个最关键且有效的安全原则,分别是纵深防御、运用PDCA模型、最小权限法则、白名单机制、安全的失败、避免通过隐藏来实现安全、入侵检测、不要信任基础设施、不要信任服务、交付时保持默认是安全的。 1. 纵深防御 在安全领域,有一种最基本的假设—任何单一的安全措施都是不充分的,任何单一的安全措施都是可以绕过的。 试想一下,在谍战影片中,最核心的机密文件一般放在哪里? 最核心的机密文件不会放在别人轻易接触到的地方,而是放在有重兵把守的深宅大院里面,房间的门会配置重重的铁锁,进入房间后还有保险柜,打开保险柜之后,发现原来机密文件还是加密过的。在这样的场景中,守门的精兵强将、铁锁、保险柜都是防止机密文件被接触到的防御手段,加密是最后一道防御,防止机密文件万一被窃取后导致的信息泄露。这是典型的纵深防御的例子。 早在1998年的时候,由美国国家安全局和国防部联合组织编写的《信息保障技术框架(Information Assurance Technical Framework,IATF)》开始出版。该书针对美国的“信基础设施”防护,提出了“纵深防御策略”(该策略包括了网络与基础设施防御、区域边界防御、计算环境防御和支撑性基础设施等深度防御目标)。从此,信息安全领域的纵深防御的思想被广泛流传了下来。 纵深防御(Defense in depth),也被称为“城堡方法(Castle Approach)”,是指在信息系统上实施多层的安全控制(防御)。实施纵深防御的目标是提供冗余的安全控制,也就是在一种控制措施失效或者被突破之后,可以用另外的安全控制来阻挡进一步的危害。换句话说,纵深防御的目标也就是增加攻击者被发现的机率和降低攻击者攻击成功的机率。 纵深防御的概念,可以参考图1: 图1 纵深防御体系 如图1所示,为了保护核心数据,我们需要在多个层面进行控制和防御,一般来说包括物理安全防御(例如,服务器加锁、安保措施等)、网络安全防御(例如,使用防火墙过滤网络包等)、主机安全防御(例如,保障用户安全、软件包管理和文件系统防护等)、应用安全防御(例如,对Web应用防护等),以及对数据本身的保护(例如,对数据加密等)。如果没有纵深防御体系,就难以构建真正的系统安全体系。 2. 运用PDCA模型 在实施了纵深防御策略以后,我们还需要不断的检查策略的有效性,细致分析其中潜在的问题,调查研究新的威胁,从而不断的改进和完善。 我们需要牢记的一句话是“安全不是一劳永逸的,它不是一次性的静态过程,而是不断演进、循环发展的动态过程,它需要坚持不懈的持续经营。”因此,笔者认为,动态运营安全是一条需要持续贯彻的原则,而PDCA模型恰好能有效的辅助这种运营活动。《ISO/IEC 27001:2005 信息安全管理体系规范与使用指南》中也明确指出,“本国际标准采用了‘计划-执行-检查-改进’(PDCA)模型去构架全部信息安全管理体系(Information Security Management System,ISMS)流程。” PDCA(Plan–Do–Check–Act,计划—执行—检查—改进),也被称为戴明环(Deming Cycle),是在管理科学中常用的迭代控制和持续改进的方法论。PDCA迭代循环所强调的持续改进也正是精益生产(Lean Production)的灵魂。 标准的PDCA循环改进流程如图2所示: 图2 标准的PDCA循环改进流程 在安全领域实施PDCA的方法和步骤如下: 在计划阶段的任务是 (1)梳理资产:遗忘的资产往往成为入侵的目标,也往往导致难以在短时间内发现入侵行为。对资产“看得全,理的清,查得到”,已经成为企业在日常安全建设中首先需要解决的问题;同时在发生安全事件时,全面及时的资产数据支持,也将大大缩短排查问题的时间周期,减少企业损失。资产梳理的方法包括使用配置管理数据库(Configuration Management Database,CMDB)、网段扫描、网络流量分析、对相关人员(如业务方、运营方、开发方、运维方)进行访谈等。需要梳理的对象包括:服务器IP地址信息(公网、内网)、域名信息、管理平台和系统地址、网络设备IP地址信息以及与这些资产相关的被授权人信息。 (2)制定安全策略:安全策略既包括安全技术策略,也包括安全管理策略。实现两手抓,两手都要硬。安全技术策略,包括安全工具和系统、平台。如果没有它们的辅助,那么就没有办法阻止恶意入侵。安全管理策略,包括制度和流程。如果没有它们发挥强有力的作用,那么就会使得安全技术策略的效力也大打折扣。 (3)制定安全策略的实施方案:在这个阶段,需要制定具体的安全策略实施方法、实施负责人、实施步骤、实施周期。 (4)制定安全策略的验证方案:制定验证方案的的目的,是在检查阶段能够以此为基准检查确认安全策略的有效性。 在执行阶段的任务是实施计划阶段制定的方案。这个阶段的工作包括物理防护、网络防护、主机防护、应用防护和数据防护以及安全管理制度的实施。 在检查阶段的任务是按照计划阶段制定的验证方案验证安全策略的有效性,从而确认安全策略的效果。这个阶段的工作包括自我检查、漏洞扫描、网络扫描、应用扫描、渗透测试等,也包括安全管理制度实施效果的检查。这一阶段的成果是下一阶段的输入。 在改进阶段的任务是以检查阶段的输出为指导,完善安全策略,进入下一个升级迭代。 3. 最小权限法则 最小权限法则(Principle of Least Privilege,PoLP)是指仅仅给予人员、程序、系统最小化的、恰恰能完成其功能的权限。 在系统运维工作中,最小化权限法则应用的一些例子包括: 服务器网络访问权限控制。例如,某些后端服务器不需要被外部访问,那么在部署时,就不需要给予公网IP地址。这些服务器包括MySQL、Redis、Memcached以及内网API服务器等。 使用普通用户运行应用程序。例如,在Linux环境中,监听端口在1024以上的应用程序,除有特殊权限需求以外,都应该使用普通用户(非root用户)来运行。在这种情况下,可以有效的降低应用程序漏洞带来的风险。 为程序设置Chroot环境。在经过Chroot之后,程序所能读取和写入到的目录和文件将不在是旧系统根下的而是新根下(即被指定的新的位置)的目录结构和文件。这样,即使在最糟糕的情况下发生了入侵事件,也可以组织黑客访问到系统的其他目录和文件。 数据库访问控制。例如,针对报表系统对MySQL数据库的访问控制,一般情况下,授予SELECT权限即可,而不应该给予ALL的权限。 在运维和运营过程中,未遵循最小权限法则将会对系统安全造成极其严重的威胁。例如,根据The Hack News网站(https://thehackernews.com/2018/06/redis-server-hacking.html,访问日期:2019年1月5日)报道,运行在公网上、未使用认证的Redis服务器中高达75%已经被黑客入侵。造成该严重安全问题的重要原因之一就是未遵循最小权限法则来限制Redis服务器其对外服务和使用较低权限的用户启动Redis服务。 4. 白名单机制 白名单机制(Whitelisting)明确定义什么是被允许的,而拒绝所有其他情况。 白名单机制和黑名单机制(Blacklisting)相对,后者明确定义了什么是不被允许的,而允许所有其他情况。单纯使用黑名单机制的显而易见的缺陷是,在很多情况下,我们是无法穷尽所有可能的威胁的;另外,单纯使用黑名单机制,也可能会给黑客通过各种变形而绕过的机会。使用白名单机制的好处是,那些未被预期到的新的威胁也是被阻止的。例如,在设置防火墙规则时,最佳实践是在规则最后设置成拒绝所有其他连接而不是允许所有其他连接。本书“第2章 Linux网络防火墙”中正是使用了这一原则来进行网络防护。 5. 安全的失败 安全的失败(Fail Safely)是指安全的处理错误。安全的处理错误是安全编程的一个重要方面。 在程序设计时,要确保安全控制模块在发生异常时是遵循了禁止操作的处理逻辑。如代码清单1-1所示: 代码清单1-1 不安全的处理错误 isAdmin = true; try { codeWhichMayFail(); isAdmin = isUserInRole(“Administrator”); } catch (Exception ex) { log.write(ex.toString()); } 如果codeWhichMayFail()出现了异常,那么用户默认就是管理员角色了,这显然导致了一个非常严重的安全风险。 修复这个问题的处理方式很简单,如代码清单1-2所示。 代码清单1-2 安全的处理错误 isAdmin = false; try { codeWhichMayFail(); isAdmin = isUserInrole( "Administrator" ); } catch (Exception ex) { log.write(ex.toString()); } 在代码清单1-2中,默认用户不是管理员角色,那么即使codeWhichMayFail()出现了异常也不会导致用户变成管理员角色。这样就更加安全了。 6. 避免通过隐藏来实现安全 通过隐藏来实现安全(Security by obscurity)是指通过试图对外部隐藏一些信息来实现安全。举个生活中的例子。把贵重物品放在车里,然后给它盖上一个报纸,我们就认为它安全无比了。这就大错特错了。 在信息安全领域,通过隐藏来实现安全也是不可取的。例如,我们把Redis监听端口从TCP 6379改成了TCP 6380但依然是放在公网上提供服务并不明显提高Redis的安全性。又例如我们把WordPress的版本号隐藏掉就认为WordPress安全了也是极其错误的。当前互联网的高速连接速度和强大的扫描工具已经让试图通过隐藏来实现安全越来越变得不可能了。 7. 入侵检测 在入侵发生后,如果没有有效的入侵检测系统(Intrusion Detection System,IDS)的支持,我们的系统可能会长时间被黑客利用而无法察觉,导致业务长期受到威胁。例如,在2018年9月份,某知名国际酒店集团爆出发现约5亿预定客户信息发生泄露,但经过严密审查发现,其实自2014年以来,该集团数据库就已经持续的遭到了未授权的访问(https://answers.kroll.com/,访问日期:2018年12月31日)。该事件充分证明了建设有效入侵检测系统的必要性和急迫性。 入侵检测系统,按照部署的位置,一般可以分为网络入侵检测系统和主机入侵检测系统。 网络入侵检测系统部署在网络边界,分析网络流量,识别出入侵行为。 主机系统检测系统部署在服务器上,通过分析文件完整性、网络连接活动、进程行为、日志字符串匹配、文件特征等识别出是否正在发生入侵行为或者判断出是否已经发生入侵行为。 《Linux系统安全:纵深防御、安全扫描与入侵检测》“第11章 入侵检测系统”、“第12章 Linux Rootkit与病毒木马检查”、“第13章 日志和审计”将详细介绍入侵检测相关技术和实践。 8. 不要信任基础设施 在信息安全领域有一种误解,那就是“我使用了主流的基础设施,例如网站服务器、数据库服务器、缓存服务器,因此我不需要额外防护我的应用了。我完全依赖这些基础设施提供的安全措施。” 虽然主流的信息基础设施在设计和实现时会把安全放在重要的位置,但是如果没有健壮的验证机制和安全控制措施,这些应用反而成为基础设施中显而易见的攻击点,使得黑客通过应用漏洞完全控制基础设施。 Weblogic这样一个广泛使用的Web容器平台就曾经爆发过严重的安全漏洞。例如,在2017年12月末,国外安全研究者K.Orange在Twitter上爆出有黑产团体利用Weblogic反序列化漏洞(CVE-2017-3248)对全球服务器发起大规模攻击,大量企业服务器已失陷且被安装上了watch-smartd挖矿程序。这个例子告诉我们,要时时刻刻关注信息基础设施的安全,及时修正其存在的安全缺陷。 9. 不要信任服务 这里的服务是指任何外部或者内部提供的系统、平台、接口、功能,也包括自研客户端和作为客户端功能的软件,例如浏览器、FTP上传下载工具等。 在实践中,我们常常见到,对于由外部第三方提供的服务,特别是银行支付接口、短信通道接口,应用一般都是直接信任的,对其返回值或者回调请求缺少校验。同样,对于内部服务,应用一般也是直接信任的。事实上,这种盲目的信任关系会直接导致严重的安全风险。如外部和内部服务被成功控制后,我们的业务也可能会受到直接影响。来自自研客户端或者作为客户端功能的软件的数据,更是应该进行严格校验的,因为这些数据被恶意篡改的概率是非常大的。例如,黑客通过逆向工程(Reverse Engineering)对自研客户端进行反编译(Decompilation)往往可以直接分析出客户端和服务器端交互的数据格式从而可以进一步模拟请求或者伪造请求而尝试入侵。 10. 交付时保持默认是安全的 在交付应用时,我们要保证默认情况下的设置是安全的。比如,对于有初始密码的应用,我们要设置较强的初始密码,并且启用密码失效机制来强制用户在第一次使用的时候就必须修改掉默认密码。另一个例子是虚拟机镜像的交付。我们在烧制虚拟机镜像的时候,应该对镜像进行基础的安全设置,这包括删除无用的系统默认账号、默认密码设置、防火墙设置、默认启动的应用剪裁等。在虚拟机镜像交付给用户以后,用户可以按照实际需要再进行优化和完善,以满足业务需求。 本文主要内容摘要自我的新书《Linux系统安全:纵深防御、安全扫描与入侵检测》。通过学习《Linux系统安全:纵深防御、安全扫描与入侵检测》,可以系统性、整体化的构建Linux安全体系。 文章来源:微信公众号 运维技术实践
Rootkit是一组计算机软件的合集,通常是恶意的,它的目的是在非授权的情况下维持系统最高权限(在Unix、Linux下为root,在Windows下为Administrator)来访问计算机。与病毒或者木马不同的是,Rootkit试图通过隐藏自己来防止被发现,以到达长期利用受害主机的目的。Rootkit和病毒或者木马一样,都会对Linux系统安全产生极大的威胁。 本章将首先介绍Linux Rootkit的分类和原理,然后介绍用于检测Rootkit的工具和方法。接下来,本章将介绍病毒木马扫描技术。Webshell作为恶意代码的一种例子,也可以看做是一种特殊形式的木马,它以Web服务器运行环境为依托,实现黑客对受害主机长期隐蔽性的控制。在本章的最后部分,也对这种恶意代码的检测方法做了讲解。 1.1 Rootkit分类和原理 Rootkit的主要功能包括: 隐藏进程 隐藏文件 隐藏网络端口 后门功能 键盘记录器 Rootkit主要分为以下2种: 用户态Rootkit(User-mode Rootkit):一般通过覆盖系统二进制和库文件来实现。它具有如下的特点: 它通常替换的二进制文件为ps、netstat、du、ping、lsof、ssh、sshd等,例如已知的Linux t0rn rootkit(https://www.sans.org/security-resources/malwarefaq/t0rn-rootkit,访问日期:2019年2月11日)替换的文件就包括ps(用于隐藏进程)、du(用于隐藏特定文件和目录)。 它也可能使用环境变量LD_PRELOAD和/etc/ld.so.preload、/etc/ld.so.conf等加载黑客自定义的恶意库文件来实现隐藏。例如,beurk(https://github.com/unix-thrust/beurk)这个Rootkit正是使用了这种技术。 它还可能直接在现有进程中加载恶意模块来实现隐藏。例如,在GitHub上托管的这个项目https://github.com/ChristianPapathanasiou/apache-rootkit就是在Apache进程中注入恶意动态加载库来实现远程控制和隐藏的Rootkit。 它不依赖于内核(Kernel-independent)。 需要为特定的平台而编译。 内核态Rootkit(Kernel-mode Rootkit):通常通过可加载内核模块(LoadableKernel Module,LKM)将恶意代码被直接加载进内核中。它具有如下的特点: 它直接访问/dev/{k,}mem。 更加隐蔽,更难以检测,通常包含后门。 在这里需要指出的是,用于获得root权限的漏洞利用工具不是Rootkit;用于获得root权限的漏洞利用工具被称为提权工具。通常情况下,黑客攻击的动作序列为: (1)定位目标主机上的漏洞,这一般是通过网络扫描和Web应用扫描工具来实现的,使用的工具包括但不限于《Linux系统安全:纵深防御、安全扫描与入侵检测》中《第10章 Linux安全扫描工具》中提到的相关工具。 (2)利用漏洞提权。在步骤(1)中获得的权限可能不是root超级用户权限,此时黑客通过系统中的本地提权漏洞非法的获得root权限。 (3)提权成功后安装Rootkit。 (4)擦除痕迹,通过删除本地日志、操作历史等擦除痕迹。 (5)长期利用被植入了Rootkit的主机。黑客可能会把这些植入了Rootkit的主机作为挖矿机、发动DDoS分布式拒绝服务攻击的僵尸网络等等。 1.2 可加载内核模块 Linux是单内核(monolithic kernel),即操作系统的大部分功能都被称为内核,并在特权模式下运行。通过可加载内核模块可以在运行时动态地更改Linux。可动态更改是指可以将新的功能加载到内核或者从内核去除某个功能。 加载一个模块使用如下命令(只有root有此权限): #insomod module.o 使用可加载内核模块的优点有: 可以让内核保持比较小的尺寸,不至于内核过大过臃肿。 动态加载,避免重启系统。 常常用于加载驱动程序。 模块加载之后,与原有的内核代码地位等同。 但是,在带来便利性的同时,可加载内核模块也带来了如下的风险: 可能会被恶意利用在内核中注入恶意代码,例如12.1节中提到的内核态Rootkit。 可能会导致一定的性能损失和内存开销。 代码不规范的模块可能会导致内核崩溃、系统宕机。 1.3 利用Chkrootkit检查Rootkit Chkrootkit是本地化的检测rootkit迹象的安全工具,其官方网站是http://www.chkrootkit.org。Chkrootkit包含: chkrootkit:这是一个shell脚本,用于检查系统二进制文件是否被rootkit修改。 ifpromisc.c:检查网络端口是否处于混杂模式(promiscuousmode)。 chklastlog.c:检查lastlog是否删除。 chkwtmp.c:检查wtmp是否删除。 check_wtmpx.c:检查wtmpx是否删除(仅适用于Solaris) chkproc.c:检查可加载内核模块木马的痕迹。 chkdirs.c:检查可加载内核模块木马的痕迹。 strings.c:快捷的字符串替换。 chkutmp.c:检查utmp是否删除。 Chkrootkit可以识别的Rootkit如图1所示: 图1 Chkrootkit可识别的Rootkit 1.3.1 Chkrootkit安装 使用如下命令安装Chkrootkit: cd /opt #进入/opt目录 wgetftp://ftp.pangeia.com.br/pub/seg/pac/chkrootkit.tar.gz #下载源码包 2019-02-11 11:25:35 (17.9 KB/s) -‘chkrootkit.tar.gz’ saved [40031] #完成下载源码包 wgetftp://ftp.pangeia.com.br/pub/seg/pac/chkrootkit.md5 #下载md5校验文件 2019-02-11 11:25:53 (3.18 MB/s) -‘chkrootkit.md5’ saved [52] #完成下载md5校验文件 md5sum chkrootkit.tar.gz #计算源码包md5 0c864b41cae9ef9381292b51104b0a04 chkrootkit.tar.gz #md5计算结果 cat chkrootkit.md5 #查看md5校验文件内容 0c864b41cae9ef9381292b51104b0a04 chkrootkit.tar.gz #和已下载源码包md5对比,文件完整性校验通过 tar zxvf chkrootkit.tar.gz #解压源码包 cd chkrootkit-0.52 #进入源码包解压目录make sense #编译安装 1.3.2 执行Chkrootkit 经过1.3.1中的安装步骤后,在/opt/chkrootkit-0.52/目录下存储了编译后的二进制文件和相关脚本。执行Rootkit检查的命令如下: cd /opt/chkrootkit-0.52 ./chkrootkit 输出结果中可能包含的状态字段如下: “INFECTED”:检测出了一个可能被已知rootkit修改过的命令。 “not infected”:未检测出任何已知的rootkit指纹。 “not tested”:未执行测试--在以下情形中发生这种情况: 这种测试是特定于某种操作系统的。 这种测试依赖于外部的程序,但这个程序不存在。 给定了一些特定的命令行选项(例如,-r)。“not found”:要检测命令对象不存在。 “Vulnerable but disabled”:命令虽然被感染,但没有在使用中(例如,非运行状态或者在inetd.conf被注释掉了) 1.4 利用Rkhunter检查Rootkit Rkhunter是Rootkit Hunter(Rootkit狩猎者)的缩写,是另一款常用的开源Rootkit检测工具。 Rkhunter的官方网站是http://rkhunter.sourceforge.net。 1.4.1 Rkhunter安装 使用如下命令安装Rkhunter: cd /opt #进入/opt目录 wgethttps://sourceforge.net/projects/rkhunter/files/rkhunter/1.4.6/rkhunter-1.4.6.tar.gz/download-O rkhunter-1.4.6.tar.gz #下载rkhunter源码包 tar zxf rkhunter-1.4.6.tar.gz #解压rkhunter源码包 cd rkhunter-1.4.6 #进入解压后目录 ./installer.sh --install #安装rkhunter 1.4.2 执行Rkhunter 在完成1.4.1的安装步骤后,Rkhunter的二进制可执行文件被存储在/usr/local/bin/rkhunter路径。 执行以下命令进行系统扫描: /usr/local/bin/rkhunter -c 执行完成后,扫描日志会写入/var/log/rkhunter.log文件中。重点关注该文件最后部分的内容即可,如下所示: [22:12:42] System checks summary #系统检测结果汇总开始 [22:12:42] ===================== [22:12:42] [22:12:42] File properties checks... #文件属性检测 [22:12:42] Required commands check failed [22:12:42] Files checked: 130 [22:12:43] Suspect files: 3 #可疑的文件数量,如该数量不为0,则表示发现可疑文件,再从该日志中查找Warning的相关行进行详细分析 [22:12:43] [22:12:43] Rootkit checks... [22:12:43] Rootkits checked : 434 [22:12:43] Possible rootkits: 0 #可能的rootkit数量,如该数量不为0,则表示发现可疑文件,再从该日志中查找Warning的相关行进行详细分析 [22:12:43] [22:12:43] Applications checks... [22:12:43] All checks skipped [22:12:43] [22:12:43] The system checks took: 1 minuteand 47 seconds #系统检测花费的时间 [22:12:43] [22:12:43]Info: End date is Mon Feb 11 22:12:43 CST 2019 #系统检测结束的时间 1.5总结 Linux Rootkit作为黑客隐藏其恶意行为的关键技术,具有很强的隐蔽性和迷惑性。本文讲的两种工具,可以在一定程度上识别出常见Rootkit。对于新型的Rootkit,可以使用独立编译的Busybox等二进制进行对比分析。 文章来源:微信公众号 新钛云服
摘自《微服务架构设计模式》作者::[美] (Chris Richardson)译者:喻勇 导语:微服务架构如何与更广泛的软件架构概念相结合?什么是服务?服务的规模有多重要?为了回答这些问题,我们需要退后一步,看看软件架构的含义。 软件的架构是一种抽象的结构,它由软件的各个组成部分和这些部分之间的依赖关系构成。正如你将在本文中看到的,软件的架构是多维的,因此有多种方法可以对其进行描述。架构很重要的原因是它决定了应用程序的质量属性或能力。传统上,架构的目标是可扩展性、可靠性和安全性。但是今天,该架构能够快速安全地交付软件,这一点非常重要。你将了解微服务架构是一种架构风格,可为应用程序提供更高的可维护性、可测试性和可部署性。 我将通过描述软件架构的概念及其重要性来开始本文。接下来,我将讨论架构风格的概念。然后我将微服务架构定义为特定的架构风格。让我们从理解软件架构的概念开始。 1 软件架构是什么,为什么它如此重要 架构显然很重要。至少有两个专门讨论该主题的会议:O’Reilly的软件架构会议和SATURN会议。许多开发人员的目标是成为一名架构师。但什么是架构,为什么它如此重要? 为了回答这个问题,我首先定义术语软件架构的含义。之后,我将讨论应用程序的架构是多维的,并使用一组视图或蓝图进行描述。然后我将强调软件架构的重要性,因为它对应用程序的质量属性有显著的影响。 软件架构的定义 软件架构有很多定义。例如,维基百科上列举了大量的定义。我最喜欢的定义来自卡耐基梅隆大学软件工程研究所的Len Bass及其同事,他们在使软件架构成为一门学科方面发挥了关键作用。他们定义的软件架构如下: 计算机系统的软件架构是构建这个系统所需要的一组结构,包括软件元素、它们之间的关系以及两者的属性。 这显然是一个非常抽象的定义。但其实质是应用程序的架构是将软件分解为元素(element)和这些元素之间的关系(relation)。由于以下两个原因,分解很重要: 它促进了劳动和知识的分工。它使具有特定专业知识的人们(或多个团队)能够就应用程序高效地协同工作。 它定义了软件元素的交互方式。 将软件分解成元素以及定义这些元素之间的关系,决定了软件的能力。 软件架构的4+1视图模型 从更具体的角度而言,应用程序的架构可以从多个视角来看,就像建筑架构,一般有结构、管线、电气等多个架构视角。Phillip Krutchen在他经典的论文《Architectural Blueprints —The 4+1 View Model of Software Architecture》中提出了软件架构的4+1视图。图1展示的这套视图定义了四个不同的软件架构视图,每一个视图都只描述架构的一个特定方面。每个视图包括一些特定的软件元素和它们相互之间的关系。 图1 4+1视图模型使用四个视图描述应用程序的架构,并显示每个视图中的元素如何协作处理请求的场景 每个视图的目的如下: 逻辑视图:开发人员创建的软件元素。在面向对象的语言中,这些元素是类和包。它们之间的关系是类和包之间的关系,包括继承、关联和依赖。 实现视图:构建编译系统的输出。此视图由表示打包代码的模块和组件组成,组件是由一个或多个模块组成的可执行或可部署单元。在Java中,模块是JAR文件,组件通常是WAR文件或可执行JAR文件。它们之间的关系包括模块之间的依赖关系以及组件和模块之间的组合关系。 进程视图:运行时的组件。每个元素都是一个进程,进程之间的关系代表进程间通信。 部署视图:进程如何映射到机器。此视图中的元素由(物理或虚拟)计算机和进程组成。机器之间的关系代表网络。该视图还描述了进程和机器之间的关系。 除了这四个视图以外,4+1中的+1是指场景,它负责把视图串联在一起。每个场景负责描述在一个视图中的多个架构元素如何协作,以完成一个请求。例如,在逻辑视图中的场景,展现了类是如何协作的。同样,在进程视图中的场景,展现了进程是如何协作的。 4+1视图是描述应用程序架构的绝佳方式。每一个视图都描述了架构的一个重要侧面。场景把视图中的元素如何协作串联在一起。现在我们来看看为什么架构是如此重要。 为什么架构如此重要 应用程序有两个层面的需求。第一类是功能性需求,这些需求决定一个应用程序做什么。这些通常都包含在用例(use case)或者用户故事(user story)中。应用的架构其实跟这些功能性需求没什么关系。功能性需求可以通过任意的架构来实现,甚至是非常糟糕的大泥球架构。 架构的重要性在于,它帮助应用程序满足了第二类需求:非功能性需求。我们把这类需求也称之为质量属性需求,或者简称为“能力”。这些非功能性需求决定一个应用程序在运行时的质量,比如可扩展性和可靠性。它们也决定了开发阶段的质量,包括可维护性、可测试性、可扩展性和可部署性。为应用程序所选择的架构将决定这些质量属性。 2 什么是架构的风格 在物理世界中,建筑物的建筑通常遵循特定的风格,例如维多利亚式、美国工匠式或装饰艺术式。每种风格都是一系列设计决策,限制了建筑的特征和建筑材料。建筑风格的概念也适用于软件。David Garlan和Mary Shaw这两位软件架构学科的先驱定义了如下架构风格: 因此,架构风格根据结构组织模式定义了一系列此类系统。更具体地说,架构风格确定可以在该风格的实例中使用的组件和连接器的词汇表,以及关于如何组合它们的一组约束。 特定的架构风格提供了有限的元素(组件)和关系(连接器),你可以从中定义应用程序架构的视图。应用程序通常使用多种架构风格的组合。例如,在本节的后面,我将描述单体架构是如何将实现视图构造为单个(可执行与可部署)组件的架构样式。微服务架构将应用程序构造为一组松散耦合的服务。 分层式架构风格 架构的典型例子是分层架构。分层架构将软件元素按“层”的方式组织。每个层都有明确定义的职责。分层架构还限制了层之间的依赖关系。每一层只能依赖于紧邻其下方的层(如果严格分层)或其下面的任何层。 可以将分层架构应用于前面讨论的四个视图中的任何一个。流行的三层架构是应用于逻辑视图的分层架构。它将应用程序的类组织到以下层中: 表现层:包含实现用户界面或外部API的代码。 业务逻辑层:包含业务逻辑。 数据持久化层:实现与数据库交互的逻辑。 分层架构是架构风格的一个很好的例子,但它确实有一些明显的弊端: 单个表现层:它无法展现应用程序可能不仅仅由单个系统调用的事实。 单一数据持久化层:它无法展现应用程序可能与多个数据库进行交互的事实。 将业务逻辑层定义为依赖于数据持久化层:理论上,这样的依赖性会妨碍你在没有数据库的情况下测试业务逻辑。 此外,分层架构错误地表示了精心设计的应用程序中的依赖关系。业务逻辑通常定义数据访问方法的接口或接口库。数据持久化层则定义了实现存储库接口的DAO类。换句话说,依赖关系与分层架构所描述的相反。 让我们看一下克服这些弊端的替代架构:六边形架构。 关于架构风格的六边形 六边形架构是分层架构风格的替代品。如图2-2所示,六边形架构风格选择以业务逻辑为中心的方式组织逻辑视图。应用程序具有一个或多个入站适配器,而不是表示层,它通过调用业务逻辑来处理来自外部的请求。同样,应用程序具有一个或多个出站适配器,而不是数据持久化层,这些出站适配器由业务逻辑调用并调用外部应用程序。此架构的一个关键特性和优点是业务逻辑不依赖于适配器。相反,各种适配器都依赖业务逻辑。 图2 六边形架构的一个示例,它由业务逻辑和一个或多个与外部系统通信的适配器组成。业务逻辑具有一个或多个端口。处理来自外部系统请求的入站适配器调用入站端口。出站适配器实现出站端口,并调用外部系统 业务逻辑具有一个或多个端口(port)。端口定义了一组操作,关于业务逻辑如何与外部交互。例如,在Java中,端口通常是Java接口。有两种端口:入站和出站端口。入站端口是业务逻辑公开的API,它使外部应用程序可以调用它。入站端口的一个实例是服务接口,它定义服务的公共方法。出站端口是业务逻辑调用外部系统的方式。出站端口的一个实例是存储库接口,它定义数据访问操作的集合。 业务逻辑的周围是适配器。与端口一样,有两种类型的适配器:入站和出站。入站适配器通过调用入站端口来处理来自外部世界的请求。入站适配器的一个实例是Spring MVC Controller,它实现一组REST接口(endpoint)或一组Web页面。另一个实例是订阅消息的消息代理客户端。多个入站适配器可以调用相同的入站端口。 出站适配器实现出站端口,并通过调用外部应用程序或服务处理来自业务逻辑的请求。出站适配器的一个实例是实现访问数据库的操作的数据访问对象(DAO)类。另一个实例是调用远程服务的代理类。出站适配器也可以发布事件。 六边形架构风格的一个重要好处是它将业务逻辑与适配器中包含的表示层和数据访问层的逻辑分离开来。业务逻辑不依赖于表示层逻辑或数据访问层逻辑。 由于这种分离,单独测试业务逻辑要容易得多。另一个好处是它更准确地反映了现代应用程序的架构。可以通过多个适配器调用业务逻辑,每个适配器实现特定的API或用户界面。业务逻辑还可以调用多个适配器,每个适配器调用不同的外部系统。六边形架构是描述微服务架构中每个服务的架构的好方法。 分层架构和六边形架构都是架构风格的实例。每个都定义了架构的构建块(元素),并对它们之间的关系施加了约束。六边形架构和分层架构(三层架构)构成了软件的逻辑视图。现在让我们将微服务架构定义为构成软件的实现视图的架构风格。 2.1.3 微服务架构是一种架构风格 前面已经讨论过4+1视图模型和架构风格,所以现在可以开始定义单体架构和微服务架构。它们都是架构风格。单体架构是一种架构风格,它的实现视图是单个组件:单个可执行文件或WAR文件。这个定义并没有说明其他的视图。例如,单体应用程序可以具有六边形架构风格的逻辑视图。 模式:单体架构将应用程序构建为单个可执行和可部署组件。请参阅:http://microservices.io/patterns/monolithic.html。 微服务架构也是一种架构风格。它的实现视图由多个组件构成:一组可执行文件或WAR文件。它的组件是服务,连接器是使这些服务能够协作的通信协议。每个服务都有自己的逻辑视图架构,通常也是六边形架构。图2-3显示了FTGO应用程序可能的微服务架构。此架构中的服务对应于业务功能,例如订单管理和餐馆管理。 模式:微服务架构将应用程序构建为松耦合、可独立部署的一组服务。请参阅:http://microservices.io/patterns/microservices.html。 图3 FTGO应用程序可能的微服务架构。它由众多服务组成 微服务架构强加的一个关键约束是服务松耦合。因此,服务之间的协作方式存在一定限制。为了解释这些限制,我将尝试定义什么是服务,解释松耦合意味着什么,并告诉你为什么这很重要。 什么是服务 服务是一个单一的、可独立部署的软件组件,它实现了一些有用的功能。图2-4显示了服务的外部视图,在此示例中是Order Service。服务具有API,为其客户端提供对功能的访问。有两种类型的操作:命令和查询。API由命令、查询和事件组成。命令如createOrder()执行操作并更新数据。查询,如findOrderById()检索数据。服务还发布由其客户端使用的事件,例如OrderCreated。 服务的API封装了其内部实现。与单体架构不同,开发人员无法绕过服务的API直接访问服务内部的方法或数据。因此,微服务架构强制实现了应用程序的模块化。 微服务架构中的每项服务都有自己的架构,可能还有独特的技术栈。但是典型的服务往往都具有六边形架构。其API由与服务的业务逻辑交互的适配器实现。操作适配器调用业务逻辑,事件适配器对外发布业务逻辑产生的事件。 图4 服务具有封装实现的API。API定义了由客户端调用的操作。有两种类型的操作:命令用来更新数据,查询用来检索数据。当服务的数据发生更改时,服务会发布可供客户端订阅的事件 在本书第12章讨论部署技术时,你将看到服务的实现视图可以采用多种形式。该组件可以是独立进程,在容器中运行的Web应用程序或OSGI包、云主机或Serverless技术,等等。但是,一个基本要求是服务具有API并且可以独立部署。 什么是松耦合 微服务架构的最核心特性是服务之间的松耦合性。服务之间的交互采用API完成,这样做就封装了服务的实现细节。这允许服务在不影响客户端的情况下,对实现方式做出修改。松耦合服务是改善开发效率、提升可维护性和可测试性的关键。小的、松耦合的服务更容易被理解、修改和测试。 我们通过API来实现松耦合服务之间的协调调用,这样就避免了外界对服务的数据库的直接访问和调用。服务自身的持久化数据就如同类的私有属性一样,是不对外的。保证数据的私有属性是实现松耦合的前提之一。这样做,就允许开发者修改服务的数据结构,而不用提前与其他服务的开发者互相协商。这样做在运行时也实现了更好的隔离。例如,一个服务的数据库加锁不会影响另外的服务。但是你稍后就会看到在服务间不共享数据库的弊端,特别是处理数据一致性和跨服务查询都变得更为复杂。 共享类库的角色 开发人员经常把一些通用的功能打包到库或模块中,以便多个应用程序可以重用它而无须复制代码。毕竟,如果没有Maven或npm库,我们今天的开发工作都会变得更困难。你可能也想在微服务架构中使用共享库。从表面上看,它似乎是减少服务中代码重复的好方法。但是你需要确保不会意外地在服务之间引入耦合。 例如,想象一下多个服务需要更新Order业务对象的场景。一种选择是将该功能打包为可供多个服务使用的库。一方面,使用库可以消除代码重复。另一方面,如果业务需求的变更影响了Order业务对象,开发者需要同时重建和重新部署所有使用了共享库的服务。更好的选择是把这些可能会更改的通用功能(例如Order管理)作为服务来实现,而不是共享库。 你应该努力使用共享库来实现不太可能改变的功能。例如,在典型的应用程序中,在每个服务中都实现一个通用的Money类(例如用来实现币种转换等固定功能)没有任何意义。相反,你应该创建一个供所有服务使用的共享库。 服务的大小并不重要 微服务这个术语的一个问题是会将你的关注点错误地聚焦在微上。它暗示服务应该非常小。其他基于大小的术语(如miniservice或nanoservice)也是如此。实际上,大小不是一个重要的考虑因素。 更好的目标是将精心设计的服务定义为能够由小团队开发的服务,并且交付时间最短,与其他团队协作最少。理论上,团队可能只负责单一服务,因此服务绝不是微小的。相反,如果服务需要大型团队或需要很长时间进行测试,那么拆分团队或服务可能是有意义的。另外,如果你因为其他服务的变更而不断需要同步更新自己负责的服务,或者你所负责的服务正在触发其他服务的同步更新,那么这表明服务没有实现松耦合。你构建的甚至可能是一个分布式的单体。 微服务架构把应用程序通过一些小的、松耦合的服务组织在一起。结果,这样的架构提升了开发阶段的效率,特别是可维护性、可测试性和可部署性,这也就让组织的软件开发速度更快。微服务架构也同时提升了应用程序的可扩展性,尽管这不是微服务的主要目标。为了使用微服务架构开发软件,你首先需要识别服务,并确定它们之间如何协作。现在我们来看看如何定义一个应用程序的微服务架构。 4 总结 架构决定了软件的各种非功能性因素,比如可维护性、可测试性、可部署性和可扩展性,它们会直接影响开发速度。 微服务架构是一种架构风格,它给应用程序带来了更高的可维护性、可测试性、可部署性和可扩展性。 文章来源:微信公众号 CSDN云计算
图1 Order Service具有六边形架构。它由业务逻辑和一个或多个与其他服务和外部应用程序连接的适配器组成 图1显示了一个典型的服务架构。业务逻辑是六边形架构的核心。业务逻辑的周围是入站和出站适配器。入站适配器处理来自客户端的请求并调用业务逻辑。出站适配器被业务逻辑调用,然后它们再调用其他服务和外部应用程序。 此服务由业务逻辑和以下适配器组成。REST API adapter:入站适配器,实现REST API,这些API会调用业务逻辑。OrderCommandHandlers:入站适配器,它接收来自消息通道的命令式消息,并调用业务逻辑。Database Adapter:由业务逻辑调用以访问数据库的出站适配器。Domain Event Publishing Adapter:将事件发布到消息代理的出站适配器。 业务逻辑通常是服务中最复杂的部分。在开发业务逻辑时,你应该以最适合应用程序的方式,精心地设计和组织业务逻辑。我确信大多数读者都经历过不得不维护别人的糟糕代码的挫败感。大多数企业应用程序都是用面向对象的语言编写的,例如Java,因此它们由类和方法组成。但是使用面向对象的语言并不能保证业务逻辑具有面向对象的设计。在开发业务逻辑时必须做出的关键决策是选用面向对象的方式,还是选用面向过程的方式。组织业务逻辑有两种主要模式:面向过程的事务脚本模式和面向对象的领域建模模式。 1 使用事务脚本模式设计业务逻辑 虽然我一直积极地倡导使用面向对象的方式,但在某些情况下使用面向对象的设计方法会有一种“杀鸡用牛刀”的感觉,例如在开发简单的业务逻辑时。在这种情况下,更好的方法是编写面向过程的代码,并使用Martin Fowler在《Patterns of EnterpriseApplication Architecture》一书中提到的事务脚本模式。你可以编写一个称为事务脚本的方法来处理来自表示层的每个请求,而不是进行任何面向对象的设计。如图2所示,这种方法的一个重要特征是实现行为的类与存储状态的类是分开的。 图2 将业务逻辑组织为事务脚本。在典型的基于事务脚本的设计中,一组类实现行为,另一组类负责存储状态。事务脚本通常被写成没有状态的类。脚本访问没有行为的数据类以完成持久化的任务 使用事务脚本模式时,脚本通常位于服务类中,在此示例中是OrderService类。每个服务类都有一个用于请求或系统操作的方法。这个方法实现该请求的业务逻辑。它使用数据访问对象(DAO)访问数据库,例如OrderDao。数据对象(在此示例中为Order类)是纯数据,几乎没有行为。 这种设计风格是高度面向过程的,仅仅依赖于面向对象编程(OOP)语言的少量功能。就好比你使用C或其他非OOP语言编写应用所能实现的功能。然而,在适当的时候,你不应该羞于使用面向过程的设计。这种方法适用于简单的业务逻辑。但这往往不是实现复杂业务逻辑的好方法。 2 使用领域模型模式设计业务逻辑 面向过程方法的可以简单迅速地搞定项目,这非常诱人。你可以专注编写业务逻辑代码,而无须仔细考虑如何设计和组织各种类。但是问题在于,如果业务逻辑变得复杂,你最终可能会得到噩梦般难以维护的代码。实际上,就像单体应用程序不断增长的趋势一样,事务脚本也存在同样的问题。因此,除非是编写一个非常简单的应用程序,否则你应该抵制编写面向过程代码的诱惑,使用领域模型模式,并进行面向对象的设计。 在面向对象的设计中,业务逻辑由对象模型和相对较小的一些类的网络组成。这些类通常直接对应于问题域中的概念。在这样的设计中,有些类只有状态或行为,但很多类同时包含状态和行为,这样的类都是精心设计的。图3显示了领域模型模式的示例。 图3 将业务逻辑组织为领域模型。大多数业务逻辑由具有状态和行为的类组成 与事务脚本模式一样,OrderService类具有针对每个请求或系统操作的方法。但是在使用领域模型模式时,服务方法通常很简单。因为服务方法几乎总是调用持久化领域对象,这些对象中包含大量的业务逻辑。例如,服务方法可以从数据库加载领域对象并调用其中一个方法。在这个例子中,Order类具有状态和行为。此外,它的状态是私有的,只能通过它的方法间接访问。 使用面向对象设计有许多好处。首先,这样的设计易于理解和维护。它不是由一个完成所有事情的大类组成,而是由许多小类组成,每个小类都有少量职责。此外,诸如Account、BankingTransaction和OverdraftPolicy这些类都密切地反映了现实世界,这使得它们在设计中的角色更容易理解。其次,我们的面向对象设计更容易测试:每个类都可以并且应该能够被独立测试。最后,面向对象的设计更容易扩展,因为它可以使用众所周知的设计模式,例如策略模式(Strategy pattern)和模板方法模式(Template methodpattern),这些设计模式定义了在不修改代码的情况下扩展组件的方法。 领域建模模式看似完美,但这种方法同样也存在许多问题,尤其是在微服务架构中。要解决这些问题,你需要使用称为领域驱动设计的思路来优化面向对象设计。 3 关于领域驱动设计 领域驱动设计(Domain-Driven Design,DDD)的概念产生于Eric Evans写的《Domain Driven Design》一书,DDD是对面向对象设计的改进,是开发复杂业务逻辑的一种方法。我在第2章介绍过DDD,领域驱动设计的子域概念有助于把应用程序分解为服务。使用DDD时,每个服务都有自己的领域模型,这就避免了在单个应用程序全局范围内的领域模型问题。子域和相关联的限界上下文的相关概念是两种战略性DDD模式。 DDD还有一些战术性模式,它们是领域模型的基本元素(building block)。每个模式都是一个类在领域模型中扮演的角色,并定义了类的特征。开发人员广泛采用的基本元素包括以下几种。实体(entity):具有持久化ID的对象。具有相同属性值的两个实体仍然是不同的对象。在Java EE应用程序中,使用JPA @Entity进行持久化的类通常是DDD实体。值对象(value object):作为值集合的对象。具有相同属性值的两个值对象可以互换使用。值对象的一个例子是Money类,它由币种和金额组成。工厂(factory):负责实现对象创建逻辑的对象或方法,该逻辑过于复杂,无法由类的构造函数直接完成。它还可以隐藏被实例化的具体类。工厂方法一般可实现为类的静态方法。存储库(repository):用来访问持久化实体的对象,存储库也封装了访问数据库的底层机制。服务(service):实现不属于实体或值对象的业务逻辑的对象。 许多开发人员都使用这些基本元素,有些基本元素是通过JPA和Spring框架等实现的。除了DDD纯粹主义者之外,还有一个被众人(包括我在内)忽略的基本元素:聚合。事实证明,在开发微服务时,聚合是一个非常有用的概念。在下一部分,我们来看一看,聚合如何解决经典面向对象设计的一些微妙问题。 本文摘自《微服务架构设计模式》,经出版方授权发布。 文章来源:微信公众号 技术琐话
作者:肖智清来源:大数据(ID:hzdashuju) 导读:本文介绍人工智能领域中强化学习的基础知识,阐述强化学习的学习方法。 强化学习(Reinforcement Learning,简称RL,又译为“增强学习”)这一名词来源于行为心理学,表示生物为了趋利避害而更频繁实施对自己有利的策略。例如,我每天工作中会根据策略决定做出各种动作。如果我的某种决定使我升职加薪,或者使我免遭处罚,那么我在以后的工作中会更多采用这样的策略。 据此,心理学家Ivan Pavlov在1927年发表的专著中用“强化”(reinforcement)这一名词来描述特定刺激使生物更趋向于采用某些策略的现象。强化行为的刺激可以称为“强化物”(reinforcer)。因为强化物导致策略的改变称为“强化学习”。 心理学家Jack Michael于1975年发表文章《Positive and negative reinforcement, a distinction that is no longer necessary》,说明了强化包括正强化(positive reinforcement)和负强化(negative reinforcement),其中正强化使得生物趋向于获得更多利益,负强化使得生物趋向于避免损害。 在前面例子中,升职加薪就是正强化,免遭处罚就是负强化。正强化和负强化都能够起到强化的效果。 人工智能(Artificial Intelligence,AI)领域中有许多类似的趋利避害的问题。例如,著名的围棋AI程序AlphaGo可以根据不同的围棋局势下不同的棋。 如果它下得好,它就会赢;如果下得不好,它就会输。它根据下棋的经验不断改进自己的棋艺,这就和行为心理学中的情况如出一辙。所以,人工智能借用了行为心理学的这一概念,把与环境交互中趋利避害的学习过程称为强化学习。 01 强化学习及其关键元素 在人工智能领域中,强化学习是一类特定的机器学习问题。在一个强化学习系统中,决策者可以观察环境,并根据观测做出行动。在行动之后,能够获得奖励。强化学习通过与环境的交互来学习如何最大化奖励。 例如,一个走迷宫的机器人在迷宫里游荡(见图1-1)。机器人观察周围的环境,并且根据观测来决定如何移动。错误的移动会让机器人浪费宝贵的时间和能量,正确的移动会让机器人成功走出迷宫。 ▲图1-1 机器人走迷宫 在这个例子中,机器人的移动就是它根据观测而采取的行动,浪费的时间能量和走出迷宫的成功就是给机器人的奖励(时间能量的浪费可以看作负奖励)。 强化学习的最大特点是在学习过程中没有正确答案,而是通过奖励信号来学习。在机器人走迷宫的例子中,机器人不会知道每次移动是否正确,只能通过花费的时间能量以及是否走出迷宫来判断移动的合理性。 一个强化学习系统中有两个关键元素:奖励和策略。 奖励(reward):奖励是强化学习系统的学习目标。学习者在行动后会接收到环境发来的奖励,而强化学习的目标就是要最大化在长时间里的总奖励。在机器人走迷宫的例子中,机器人花费的时间和能量就是负奖励,机器人走出迷宫就可以得到正奖励。 策略(policy):决策者会根据不同的观测决定采用不同的动作,这种从观测到动作的关系称为策略。强化学习的学习对象就是策略。强化学习通过改进策略以期最大化总奖励。策略可以是确定性的,也可以不是确定性的。在机器人走迷宫的例子中,机器人根据当前的策略来决定如何移动。 强化学习试图修改策略以最大化奖励。例如,机器人在学习过程中不断改进策略,使得以后能更快更省事地走出迷宫。 强化学习与监督学习和非监督学习有着本质的区别。 强化学习与监督学习的区别 对于监督学习,学习者知道每个动作的正确答案是什么,可以通过逐步比对来学习;对于强化学习,学习者不知道每个动作的正确答案,只能通过奖励信号来学习。 强化学习要最大化一段时间内的奖励,需要关注更加长远的性能。与此同时,监督学习希望能将学习的结果运用到未知的数据,要求结果可推广、可泛化;强化学习的结果却可以用在训练的环境中。所以,监督学习一般运用于判断、预测等任务,如判断图片的内容、预测股票价格等;而强化学习不适用于这样的任务。 强化学习与非监督学习的区别 非监督学习旨在发现数据之间隐含的结构;而强化学习有着明确的数值目标,即奖励。它们的研究目的不同。所以,非监督学习一般用于聚类等任务,而强化学习不适用于这样的任务。 02 强化学习的应用 基于强化学习的人工智能已经有了许多成功的应用。本节将介绍强化学习的一些成功案例,让你更直观地理解强化学习,感受强化学习的强大。 1. 电动游戏 电动游戏,主要指玩家需要根据屏幕画面的内容进行操作的游戏,包括主机游戏吃豆人(PacMan,见图1-2)、PC游戏星际争霸(StarCraft)、手机游戏Flappy Bird等。很多游戏需要得到尽可能高的分数,或是要在多方对抗中获得胜利。同时,对于这些游戏,很难获得在每一步应该如何操作的标准答案。 从这个角度看,这些游戏的游戏AI需要使用强化学习。基于强化学习,研发人员已经开发出了许多强大的游戏AI,能够超越人类能够得到的最佳结果。例如,在主机Atari 2600的数十个经典游戏中,基于强化学习的游戏AI已经在将近一半的游戏中超过人类的历史最佳结果。 ▲图1-2 街机游戏吃豆人(本图片改编自https://en.wikipedia.org/wiki/Pac-Man#Gameplay) 2. 棋盘游戏 棋盘游戏是围棋(见图1-3)、黑白翻转棋、五子棋等桌上游戏的统称。通过强化学习可以实现各种棋盘运动的AI。棋盘AI有着明确的目标—提高胜率,但是每一步往往没有绝对正确的答案,这正是强化学习所针对的场景。 Deepmind公司使用强化学习研发出围棋AI AlphaGo,于2016年3月战胜围棋顶尖选手李世石,于2017年5月战胜排名世界第一的围棋选手柯洁,引起了全社会的关注。 截至目前,最强的棋盘游戏AI是DeepMind在2018年12月发表的AlphaZero,它可以在围棋、日本将棋、国际象棋等多个棋盘游戏上达到最高水平,并远远超出人类的最高水平。 ▲图1-3 一局围棋棋谱(图中实心圆表示黑棋的棋子,空心圆表示白棋的棋子,圆里的数字记录棋子是在第几步被放在棋盘上,本图片改编自论文D. Silver, et al. Mastering the game of Go without human knowledge, Nature, 2017) 3. 自动驾驶 自动驾驶问题通过控制方向盘、油门、刹车等设备完成各种运输目标(见图1-4)。自动驾驶问题既可以在虚拟环境中仿真(比如在电脑里仿真),也可能在现实世界中出现。有些任务往往有着明确的目标(比如从一个指定地点到达另外一个指定地点),但是每一个具体的动作却没有正确答案作为参考。这正是强化学习所针对的任务。基于强化学习的控制策略可以帮助开发自动驾驶的算法。 ▲图1-4 自动驾驶(本图截取自仿真平台AirSimNH) 03 智能体/环境接口 强化学习问题常用智能体/环境接口(Agent-Environment Interface)来研究(见图1-5)。 ▲图1-5 智能体/环境接口 智能体/环境接口将系统划分为智能体和环境两个部分。 智能体(agent)是强化学习系统中的决策者和学习者,它可以做出决策和接受奖励信号。一个强化学习系统里可以有一个或多个智能体。我们并不需要对智能体本身进行建模,只需要了解它在不同环境下可以做出的动作,并接受奖励信号。 环境(environment)是强化系统中除智能体以外的所有事物,它是智能体交互的对象。环境本身可以是确定性的,也可以是不确定性的。环境可能是已知的,也可能是未知的。我们可以对环境建模,也可以不对环境建模。 智能体/环境接口的核心思想在于分隔主观可以控制的部分和客观不能改变的部分。例如,在工作的时候,我是决策者和学习者。我可以决定自己要做什么,并且能感知到获得的奖励。我的决策部分和学习部分就是智能体。 同时,我的健康状况、困倦程度、饥饿状况则是我不能控制的部分,这部分则应当视作环境。我可以根据我的健康状况、困倦程度和饥饿状况来进行决策。 注意:强化学习问题不一定要借助智能体/环境接口来研究。 在智能体/环境接口中,智能体和环境的交互主要有以下三个环节: 智能体观测环境,可以获得环境的观测(observation),记为O; 智能体根据观测做出决策,决定要对环境施加的动作(action),记为A; 环境受智能体动作的影响,改变自己的状态(state),记为S,并给出奖励(reward),记为R。 在这三个环节中,观测O、动作A和奖励R是智能体可以直接观测到的。 注意:状态、观测、动作不一定是数量(例如标量或矢量),也可以是“感觉到饿”、“吃饭”这样一般的量。奖励总是数量(而且往往是数量中的标量)。 绝大多数的强化学习问题是按时间顺序或因果顺序发生的问题。这类问题的特点是具有先后顺序,并且先前的状态和动作会影响后续的状态等。例如,在玩电脑游戏时,游戏随着时间不断进行,之前玩家的每个动作都可能会影响后续的局势。对于这样的问题,我们可以引入时间指标t,记t时刻的状态为St,观测为Ot,动作为At,奖励为Rt。 注意:用智能体/环境接口建模的问题并不一定要建模成和时间有关的问题。有些问题一共只需要和环境交互一次,就没有必要引入时间指标。例如,以不同的方式投掷一个给定的骰子并以点数作为奖励,就没有必要引入时间指标。 在很多任务中,智能体和环境是在离散的时间步骤上交互的,这样的问题可以将时间指标离散化,建模为离散时间智能体/环境接口。具体而言,假设交互的时间为t=0,1,2,3...。在t时刻,依次发生以下事情: 智能体观察环境得到观测Ot; 智能体根据观测决定做出动作At; 环境根据智能体的动作,给予智能体奖励Rt+1并进入下一步的状态St+1。 注意:智能体/环境接口问题不一定能时间上离散化。有些问题在时间上是连续的,需要使用偏微分方程来建模环境。连续时间的问题也可以近似为离散时间的问题。 在智能体/环境接口的基础上,研究人员常常将强化学习进一步建模为Markov决策过程。 04 强化学习的分类 强化学习的任务和算法多种多样,本节介绍一些常见的分类(见图1-6)。 ▲图1-6 强化学习的分类 1. 按任务分类 根据强化学习的任务和环境,可以将强化学习任务作以下分类。 单智能体任务(single agent task)和多智能体任务(multi-agent task) 顾名思义,根据系统中的智能体数量,可以将任务划分为单智能体任务和多智能体任务。单智能体任务中只有一个决策者,它能得到所有可以观察到的观测,并能感知全局的奖励值;多智能体任务中有多个决策者,它们只能知道自己的观测,感受到环境给它的奖励。 当然,在有需要的情况下,多个智能体间可以交换信息。在多智能体任务中,不同智能体奖励函数的不同会导致它们有不同的学习目标(甚至是互相对抗的)。 回合制任务(episodic task)和连续性任务(sequential task) 对于回合制任务,可以有明确的开始状态和结束状态。例如在下围棋的时候,刚开始棋盘空空如也,最后棋盘都摆满了,一局棋就可以看作是一个回合。下一个回合开始时,一切重新开始。也有一些问题没有明确的开始和结束,比如机房的资源调度。机房从启用起就要不间断地处理各种信息,没有明确的结束又重新开始的时间点。 离散时间环境(discrete time environment)和连续时间环境(continuous time environment) 如果智能体和环境的交互是分步进行的,那么就是离散时间环境。如果智能体和环境的交互是在连续的时间中进行的,那么就是连续时间环境。 离散动作空间(discrete action space)和连续动作空间(continuous action space) 这是根据决策者可以做出的动作数量来划分的。如果决策得到的动作数量是有限的,则为离散动作空间,否则为连续动作空间。例如,走迷宫机器人如果只有东南西北这4种移动方式,则其为离散动作空间;如果机器人向360°中的任意角度都可以移动,则为连续动作空间。 确定性环境任务(deterministic environment)和非确定性环境(stochastic environ-ment) 按照环境是否具有随机性,可以将强化学习的环境分为确定性环境和非确定性环境。例如,对于机器人走固定的某个迷宫的问题,只要机器人确定了移动方案,那么结果就总是一成不变的。这样的环境就是确定性的。但是,如果迷宫会时刻随机变化,那么机器人面对的环境就是非确定性的。 完全可观测环境(fully observable environment)和非完全可观测环境(partially observable environment) 如果智能体可以观测到环境的全部知识,则环境是完全可观测的;如果智能体只能观测到环境的部分知识,则环境是非完全可观测的。例如,围棋问题就可以看作是一个完全可观测的环境,因为我们可以看到棋盘的所有内容,并且假设对手总是用最优方法执行;扑克则不是完全可观测的,因为我们不知道对手手里有哪些牌。 2. 按算法分类 从算法角度,可以对强化学习算法作以下分类。 同策学习(on policy)和异策学习(off policy) 同策学习是边决策边学习,学习者同时也是决策者。异策学习则是通过之前的历史(可以是自己的历史也可以是别人的历史)进行学习,学习者和决策者不需要相同。在异策学习的过程中,学习者并不一定要知道当时的决策。例如,围棋AI可以边对弈边学习,这就算同策学习;围棋AI也可以通过阅读人类的对弈历史来学习,这就算异策学习。 有模型学习(model-based)和无模型学习(model free) 在学习的过程中,如果用到了环境的数学模型,则是有模型学习;如果没有用到环境的数学模型,则是无模型学习。对于有模型学习,可能在学习前环境的模型就已经明确,也可能环境的模型也是通过学习来获得。 例如,对于某个围棋AI,它在下棋的时候可以在完全了解游戏规则的基础上虚拟出另外一个棋盘并在虚拟棋盘上试下,通过试下来学习。这就是有模型学习。与之相对,无模型学习不需要关于环境的信息,不需要搭建假的环境模型,所有经验都是通过与真实环境交互得到。 回合更新(Monte Carlo update)和时序差分更新(temporal difference update) 回合制更新是在回合结束后利用整个回合的信息进行更新学习;而时序差分更新不需要等回合结束,可以综合利用现有的信息和现有的估计进行更新学习。 基于价值(value based)和基于策略(policy based) 基于价值的强化学习定义了状态或动作的价值函数,来表示到达某种状态或执行某种动作后可以得到的回报。基于价值的强化学习倾向于选择价值最大的状态或动作;基于策略的强化学习算法不需要定义价值函数,它可以为动作分配概率分布,按照概率分布来执行动作。 深度强化学习(Deep Reinforcement Learning,DRL)算法和非深度强化学习算法 如果强化学习算法用到了深度学习,则这种强化学习可以称为深度强化学习算法。 值得一提的是,强化学习和深度学习是两个独立的概念。一个学习算法是不是强化学习和它是不是深度学习算法是相互独立的(见图1-7)。 ▲图1-7 强化学习与深度学习之间的关系 如果一个算法解决了强化学习的问题,这个算法就是强化学习的算法;如果一个算法用到了深度神经网络,这个算法就是深度学习算法。一个强化学习算法可以是深度学习算法,也可以不是深度学习算法;一个深度学习算法可以是强化学习算法,也可以不是强化学习算法。 对于强化学习算法而言,在问题规模比较小时,能够获得精确解;当问题规模比较大时,常常使用近似的方法。深度学习则利用神经网络来近似复杂的输入/输出关系。对于规模比较大的强化学习问题,可以考虑利用深度学习来实现近似。 如果一个算法既是强化学习算法,又是深度学习算法,则可以称它是深度强化学习算法。例如,很多电动游戏AI需要读取屏幕显示并据此做出决策。对屏幕数据的解读可以采用卷积神经网络这一深度学习算法。这时,这个AI就用到了深度强化学习算法。 05 如何学习强化学习 本节介绍强化学习需要的预备知识,以及如何学习强化学习,本节中还提供了一些参考资料。 在正式学习强化学习前,需要了解一些预备的知识。在理论知识方面,你需要会概率论,了解概率、条件概率、期望等概念。要学习强化学习的最新进展,特别是AlphaGo等明星算法,你需要学习微积分和深度学习。在学习过程中往往需要编程实现来加深对强化学习的理解。这时你需要掌握一门程序设计语言,比如Python 3。 要学习强化学习理论,需要理解强化学习的概念,并了解强化学习的建模方法。目前绝大多数的研究将强化学习问题建模为Markov决策过程。Markov决策过程有几种固定的求解模式。规模不大的问题可以求得精确解,规模太大的问题往往只能求得近似解。对于近似算法,可以和深度学习结合,得到深度强化学习算法。最近引起广泛关注的明星算法,如AlphaGo使用的算法,都是深度强化学习算法。 在强化学习的学习和实际应用中,难免需要通过编程来实现强化学习算法。强化学习算法需要运行在环境中。Python扩展库Gym是最广泛使用的强化学习实验环境。 关于作者:肖智清,强化学习一线研发人员,清华大学工学博士,现就职于全球知名投资银行。擅长概率统计和机器学习,于近5年发表SCI/EI论文十余篇,是多个国际性知名期刊和会议审稿人。在国内外多项程序设计和数据科学竞赛上获得冠军。 本文摘编自《强化学习:原理与Python实现》,经出版方授权发布。 文章来源:微信公众号 大数据
导读:人工智能(Artificial Intelligence,AI)、大数据(Big Data)和云计算(Cloud Computing)是当前最受关注的技术,业内常常取这三个技术英文名的首字母将其合称为ABC。 最近10年,资本和媒体对这三种技术的热度按时间排序依次为:云计算、大数据和人工智能。事实上,若按照技术出现的时间排序,结果正好相反,人工智能出现最早,大数据其次,云计算则出现得最晚。 由于每种技术都能应用于各个领域,因此人们可以从不同的角度分别解读每种技术。作为同时在研发和使用这三种技术的机构负责人,作者将尝试从大数据的角度解释ABC的关系,并且阐述这三种技术对于企业、机构和人类社会的重要性。 作者:冯雷 姚延栋 高小明 杨瑜 如需转载请联系大数据(ID:hzdashuju) 人工智能是计算机科学的一个分支,它的主要研究目标是用计算机程序来表示人类智能。这个词最早是在1956年的达特茅斯会议上正式提出的。在达特茅斯会议正式提出“人工智能”这个概念之前,图灵和早期的计算机科学家一般用“机器智能”这个词。 需要强调的是,人工智能是建立在计算机之上。不管人工智能应用多么美妙和复杂,在图灵眼里都是图灵机上的一个程序(或者叫作可计算数,具体参考《从图灵机、图灵测试到人工智能:什么决定了AI能否取代人类?》)。 人工智能课程的主要目的是学习建立在模型之上的算法。这些算法和其他计算机领域的算法并无太大区别,只是这类算法专注在如图1-3所示的智能主体(Intelligent Agent)里面的模型。在人工智能领域,计算机科学家们试图建立模型使得智能主体能够观察周围环境并做出行动,就像人类的行为那样。 ▲图1-3 智能主体作为AI的主要研究对象 最近5年,由于智能主体模型在无人驾驶、聊天机器人和计算机视觉识别等应用的准确率的提升,人工智能的应用热度也随之提升。AlphaGo等棋类对弈让人工智能被公众津津乐道,因为计算资源和计算能力的提升,在限定时间内,对弈模型比人类棋手更具优势,这也引发了很多关于人工智能的讨论。 01 AI的发展史 自远古时代,人类一直希望能够创造一种类似于人类智能的机器,将人类从乏味的重复劳动中解放出来。 直到1936年,计算机科学的鼻祖图灵发表了名为《论可计算数》的论文,机器模拟人类智能的哲学话题才转变成一个可以像数学学科那样被论证的课题。在论文中,图灵构造了假想的机器来模仿人类。电影《模仿游戏》讲述的就是图灵如何构造假想的机器(计算机)来模仿人类的故事。 在那个时代,人工智能的概念还没有提出,人们更多地使用“机器智能”这个词来讨论计算机带来的智能。简单地说,图灵的论文证明了机器可以模仿人类智能,所以今天的无人驾驶、聊天机器人、棋类对弈和计算机视觉识别等应用都是图灵预见的,虽然他那时并没有足够的硬件条件测试这些应用。 在图灵提出图灵机后,多个机构便开始设计真正意义上的遵循通用图灵机模型架构的存储程序计算机(Stored-program Computer)。虽然第一台存储程序计算机(后文称作现代计算机)是谁先发明的至今仍有争议,但是影响较大的是冯·诺依曼提出的EDVAC(Electronic Discrete Variable Automatic Computer)。冯·诺依曼在后来也确认现代计算机的核心设计思想是受到通用图灵机的启发。 现代计算机发明以后,各种应用如雨后春笋一样蓬勃发展,但是真正把人工智能作为一个应用方向提出来还是在1956年的达特茅斯会议。 在20世纪40年代末现代计算机被发明后,从20世纪50年代开始,各个领域都开始关于“思考机器”(Thinking Machines)的讨论。各个领域的用词和方法的不同带来了很多混淆。于是,达特茅斯学院(Dartmouth College)年轻的助理教授麦卡锡(John McCarthy)决定召集一个会议澄清思考机器这个话题。 召集这样的会议需要赞助,聪明的麦卡锡找到了他在IBM公司的朋友罗切斯特(Nathaniel Rochester)和在普林斯顿大学的朋友闵斯基(Marvin Minsky)以及大师香农一起在1955年写了一份项目倡议。在倡议中,他使用了人工智能(Artificial Intelligence)这个词,避免和已经有的“思考机器”一词混淆。 这里值得一提的是闵斯基,麦卡锡和闵斯基后来在麻省理工学院领导了AI实验室,成就了麻省理工学院在人工智能领域首屈一指的地位。 会议在1956年举行,这里必须提到另外两位短期的参会者,来自卡内基·梅隆大学的纽厄尔(Alan Newell)和司马贺(Hubert Simon)。他们虽然只呆了一个礼拜,但是他们的报告中公布的一款程序“逻辑理论家”(Logic Theorist)代表了人工智能的另外一条路线。因为纽厄尔和司马贺的奠基工作,卡内基·梅隆大学成为人工智能的另一个重镇。 02 对AI应用的正确预期 达特茅斯会议的意义在于确立了“人工智能”(AI)作为计算机科学的一个研究领域,自那以后,AI在机器视觉、自然语言处理、无人驾驶等领域取得了长足发展。但是,“人工智能”这个概念常常被过度消费。过去,美国的学者用这个概念来申请政府研究经费,今天有不少公司用这个概念来从资本市场募资。 但实际上,AI的进展并不像很多人预言的那样乐观。 就棋类对弈而言,司马贺在20世纪50年代末就预言计算机能打败人类,但没有实现;20世纪60年代末,麦卡锡打赌说计算机将在10年内打败人类,结果他输了;国际象棋程序深蓝在“限定时间内”胜出人类直到20世纪90年代末才实现。围棋程序AlphaGo在“限定时间内”胜出人类则是在2017年实现的。 闵斯基在20世纪80年代末预言,二十年内可以解决自然语言处理问题,时至今日,各种AI应用在自然语言处理方面尚有极大差距。 如今的“无人驾驶”在商用中实际上更多起到“辅助驾驶”的作用,因为在实际的使用中仍出现过意外情况,从保证行车安全的角度,尚不能实现真正的“无人驾驶”。 人工智能最近一次的持续升温是被包括大数据和云计算在内的软硬件技术持续发展使得很多应用得以落地而驱动的(我们将在下一节中讨论ABC的关系)。从历史经验来看,也许是由于大众媒体和科幻电影的影响,AI界有种过于乐观的倾向。 但实际上,我们对于AI模型的精度应该抱有十分谨慎的态度,因为我们构建的神经网络在内的很多AI模型本质上还是经验模型,并不是一个严格的逻辑证明。这些模型的精度比起古典力学模型精度还差了很多。即使是古典力学模型,在微观量子世界也是失效的,所以对于这些模型的使用范围也要持谨慎态度。 当然,我们也不能对建立在经验模型上的AI应用持过度怀疑的态度,因为我们的大部分知识来自经验,事实证明,这些知识也是实用的。所以,AI是一个在不断前进的领域。 人工智能另外一个层面的讨论是机器能否超越人类?这个问题是令我们对于人工智能感到不安的原因。从计算机发明的第一天,图灵和其他伟大的数学家们就已经对这个话题进行过深入的讨论。 与大众传媒不同,数学家和计算机科学家们对这个问题的讨论是深层次的数学和逻辑层面的讨论。《从图灵机、图灵测试到人工智能:什么决定了AI能否取代人类?》着重讨论AI和人的关系,有决心探究这一问题的读者可以参考这篇文章。 03 ABC之间的关系 前面已经解释了ABC的概念,这里我们来讨论一下ABC之间的重要内在关系以及这些内在关系带来的可以赋能于商业的巨大技术产能。从技术角度上看,ABC之间有以下两层重要关系: 大量数据输入到大数据系统,从而改善大数据系统里建立的机器学习模型。 云计算提供的算力使得普通机构也可以在今天用大数据系统计算大量数据从而获得AI能力。 先看第一层关系。谷歌研究院的F. Pereira、P. Norvig和A. Halevy发表了一篇文章《数据的奇效》,解释了如何通过大量数据提高机器学习模型的准确率。早在谷歌之前,微软研究院的Michele Banko和Eric Brill在他们的论文《扩展到非常非常大文本来去除自然语言歧义》中,展示了使用海量数据后各个机器模型的准确率都有大幅度提高,如图1-6所示。 这一结论为机器学习和人工智能的问题求解指出了一个新方向:用大量数据和大数据计算来提高人工智能。对比一下自然语言翻译在最近10年因为利用大数据和计算所带来的进展,读者就能感觉到这种力量。 ▲图1-6 用海量数据后各个机器模型的准确率都有大幅度提高 再看第二层关系。云计算带来的巨大好处就是提供商品化的计算资源,以前只有政府机构和大型企业才能拥有的巨大计算资源,现在可以被一个创业公司所拥有。这个从量变到质变的过程使得我们可以重新审视一些计算机行业的难题。 计算资源的丰富使得大数据技术能够以更低的门槛被使用。云计算平民化了大数据技术,使得大数据技术被企业广泛采用,企业也利用大数据养成了保管数据的习惯,把数据当作未被开采的资源。大数据的普及给人工智能的分支——机器学习带来了意想不到的惊喜。 综合前面讨论的ABC的内在含义,当前的机器学习、人工智能可以朝着以下两个方向前进: 设计新的机器学习模型,在前人的模型上有所创新,改进模型效果。 使用已有的机器学习模型,但是利用前人所没有的数据量和云计算带来的计算能力来改进模型效果。 谷歌公司的Norvig曾经说过“我们没有更好的算法,但是有更多的数据”。显然,Norvig鼓励按第二种方法进行创新,当然,这不意味着用第一种方法创新不重要。但需要指出的是,第一种方法的创新门槛要远高于第二种,除了世界顶级的机构,普通机构很难拥有相应的资金、人才及配套的管理和文化来支撑第一种创新方法。 第二种方法对于传统的机构也是可以重复和实践的,按照已经有的方法论、成功案例和人才培训可以实现基于大数据和机器学习的高阶数字化转型。 前面讨论的ABC的关系可以总结成图1-7。云计算从量变到质变带来前所未有和平民化的计算资源。企业和互联网在数字化应用方面产生了大量的数据。这些数据和计算能力使得大数据技术普及到普通机构,而这些机构利用大数据来创建和改善现有的机器学习模型,带来更好的人工智能成效。 ▲图1-7 ABC之间的关系 AI带来的社会影响可能超过前三次技术革命。随着科技和商业不断推动AI技术前进,AI和人之间的关系是技术领袖、商业领袖和政策制定者们不得不思考的问题。 前面关于AI和人的关系的大部分讨论都没有系统化和逻辑化,因而不是一个学术讨论,《从图灵机、图灵测试到人工智能:什么决定了AI能否取代人类?》则会在邱奇和图灵的学术讨论上回顾并延伸到AI和人的讨论。这部分讨论非常硬科学但是对于那些有兴趣深入思考AI技术和人类关系的读者或者希望跳出AI框架内应用创新而成为系统创新者的读者,啃啃这根硬骨头定有“会当凌绝顶,一览众山小”的感觉。 本文摘编自《Greenplum:从大数据战略到实现》,经出版方授权发布。 文章来源:微信公众号 大数据
2019年09月