百度网盘java
提取码:qhhv 程序员
HashMap和Hashtable都实现了Map接口,但决定用哪个以前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。web
HashMap几乎能够等价于Hashtable,除了HashMap是非synchronized的,并能够接受null(HashMap能够接受为null的键值(key)和值(value),而Hashtable则不行)。面试
HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程能够共享一个Hashtable;而若是没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。算法
另外一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此当有其它线程改变了HashMap的结构(增长或者移除元素),将会抛出ConcurrentModificationException,但迭代器自己的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并非一个必定发生的行为,要看JVM。这条一样也是Enumeration和Iterator的区别。sql
因为Hashtable是线程安全的也是synchronized,因此在单线程环境下它比HashMap要慢。若是你不须要同步,只须要单一线程,那么使用HashMap性能要好过Hashtable。数据库
HashMap不能保证随着时间的推移Map中的元素次序是不变的。apache
要注意的一些重要术语:编程
sychronized意味着在一次仅有一个线程可以更改Hashtable。就是说任何线程要更新Hashtable时要首先得到同步锁,其它线程要等到同步锁被释放以后才能再次得到同步锁更新Hashtable。设计模式
Fail-safe和iterator迭代器相关。若是某个集合对象建立了Iterator或者ListIterator,而后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程能够经过set()方法更改集合对象是容许的,由于这并无从“结构上”更改集合。可是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。
结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。
咱们可否让HashMap同步?
HashMap能够经过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);
结论
Hashtable和HashMap有几个主要的不一样:线程安全以及速度。仅在你须要彻底的线程安全的时候使用Hashtable,而若是你使用Java 5或以上的话,请使用ConcurrentHashMap吧。
C语言:
Java语言:
c的垃圾回收是人工的,工做量大,可是可控性高。
java是自动化的,可是可控性不好,甚至有时会出现内存溢出的状况,
内存溢出也就是jvm分配的内存中对象过多,超出了最大可分配内存的大小。
提到java的垃圾回收机制就不得不提一个方法:
System.gc()用于调用垃圾收集器,在调用时,垃圾收集器将运行以回收未使用的内存空间。它将尝试释放被丢弃对象占用的内存。
然而System.gc()调用附带一个免责声明,没法保证对垃圾收集器的调用。
因此System.gc()并不能说是完美主动进行了垃圾回收。
做为java程序员仍是颇有必要了解一下gc,这也是面试过程当中常常出现的一道题目。
咱们从三个角度来理解gc。
1jvm怎么肯定哪些对象应该进行回收
2jvm会在何时进行垃圾回收的动做
3jvm究竟是怎么清楚垃圾对象的
对象是否会被回收的两个经典算法:引用计数法,和可达性分析算法。
简单的来讲就是判断对象的引用数量。实现方式:给对象共添加一个引用计数器,每当有引用对他进行引用时,计数器的值就加1,当引用失效,也就是不在执行此对象是,他的计数器的值就减1,若某一个对象的计数器的值为0,那么表示这个对象没有人对他进行引用,也就是意味着是一个失效的垃圾对象,就会被gc进行回收。
可是这种简单的算法在当前的jvm中并无采用,缘由是他并不能解决对象之间循环引用的问题。
假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种状况下因为他们的相互引用,从而是垃圾回收机制没法识别。
由于引用计数法的缺点有引入了可达性分析算法,经过判断对象的引用链是否可达来决定对象是否能够被回收。可达性分析算法是从离散数学中的图论引入的,程序把全部的引用关系看做一张图,经过一系列的名为GC Roots的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(就是从 GC Roots 到这个对象不可达)时,则证实此对象是不可用的。
如图:
1会在cpu空闲的时候自动进行回收
2在堆内存存储满了以后
3主动调用System.gc()后尝试进行回收
如何回收说的也就是垃圾收集的算法。
算法又有四个:标记-清除算法,复制算法,标记-整理算法,分代收集算法.
1 标记-清除算法。
这是最基础的一种算法,分为两个步骤,第一个步骤就是标记,也就是标记处全部须要回收的对象,标记完成后就进行统一的回收掉哪些带有标记的对象。这种算法优势是简单,缺点是效率问题,还有一个最大的缺点是空间问题,标记清除以后会产生大量不连续的内存碎片,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而形成内存空间浪费。
执行如图:
2复制算法。
复制将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况。只是这种算法的代价是将内存缩小为原来的一半。
复制算法的执行过程如图:
复制收集算法在对象存活率较高时就要执行较多的复制操做,效率将会变低。更关键的是,浪费了一半的空间。
标记-整理算法:
标记整理算法与标记清除算法很类似,但最显著的区别是:标记清除算法仅对不存活的对象进行处理,剩余存活对象不作任何处理,形成内存碎片;而标记整理算法不只对不存活对象进行处理清除,还对剩余的存活对象进行整理,从新整理,所以其不会产生内存碎片。
分代收集算法:
分代收集算法是一种比较智能的算法,也是如今jvm使用最多的一种算法,他自己其实不是一个新的算法,而是他会在具体的场景自动选择以上三种算法进行垃圾对象回收。
那么如今的重点就是分代收集算法中说的自动根据具体场景进行选择。这个具体场景究竟是什么场景。
场景其实指的是针对jvm的哪个区域,1.7以前jvm把内存分为三个区域:新生代,老年代,永久代。
了解过场景以后再结合分代收集算法得出结论: 一、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法。只须要付出少许存活对象的复制成本就能够完成收集。 二、老年代中由于对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
注意:
在jdk8的时候java废弃了永久代,可是并不意味着咱们以上的结论失效,由于java提供了与永久代相似的叫作“元空间”的技术。
废弃永久代的缘由:因为永久代内存常常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryErroy。元空间的本质和永久代相似。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。也就是不局限与jvm能够使用系统的内存。理论上取决于32位/64位系统可虚拟的内存大小。
GC垃圾回收:
jvm按照对象的生命周期,将内存按“代”划分(将堆划分为多个地址池):新生代、老年代和持久代(jdk1.8后移除持久代);
在JVM中程序(PC)计数器、JAVA栈、本地方法栈3个区域随线程而生、随线程而灭,所以这几个区域的内存分配和回收都具有肯定性,就不须要过多考虑回收的问题,由于方法结束或者线程结束时,内存天然就跟随着回收了。而堆和方法区则不同,这部份内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。
java中新建立的对象会先被放在新生代区域,该区域对象使用频繁,jvm会在该区域使用不一样算法回收必定的短时间对象,若是某些对象使用次数达到必定限制后,那么该对象就会被放入老年代区域,老年代区域要比新生代区域更大一些(堆内存大部分分配给了老年代区域),而持久代保存的是类的元数据、常量、类静态变量等。
方法区和永久代的区别:
对于方法区和永久代的区别的话,人们一直将它们看做一个部件,其实永久代实现了方法区,比做java中类的话,永久代就是接口实现类,方法区就是接口。
finalize()和System.gc()方法介绍:
提到GC就要提到finalize()方法,该方法是在jvm肯定了一个对象符合GC的条件下执行的,用于对一些外部资源的释放等操做,可是什么时候对这个对象回收咱们就不知道了;须要注意的是在jvm调用了该方法后,这个符合GC的对象也不必定最后就被回收了,由于在执行了finalize()方法后因为在方法体给对该方法进行了一些操做,使得该对象不符合GC的条件,例如将一个引用指向这个对象,最终致使该对象不会被GC,但这也只能求这个对象依次。
一样还有System.gc()方法,这个方法的调用,jvm也不会当即执行对对象的回收,gc()仅仅是提醒jvm能够回收该方法了,但实际上要根据jvm内存需求来肯定何实回收这个能够回收的对象。
那么gc()和finalize()的区别是什么呢?
首先finalize()方法是jvm调用的,可是在回收期间不必定每一个对象都会调用这个方法进行收尾工做,这也是这个方法不被提倡使用的缘由。而System.gc()方法能够人为调用进行标记一个对象能够被回收。
最后咱们从什么时候回收对象比较,finalize()标记的对象是在被标记后的第二次回收时进行回收,而System.gc()方法没有这种规定,它只是被标记,什么时候回收由jvm决定。
代码示例:
public class Test {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用");
}
public static void main(String[] args){
Test test = new Test();
test=null;
System.gc();
}
}
分析:
咱们这里建立了Test类并重写了finalize()方法,而后我在主方法里建立了一个Test对象,并使其引用为空(此时符合回收条件)咱们先调用System.gc()
结果:
调用
咱们发现执行了finalize()方法,OK,咱们如今将System.gc()注释掉,咱们会发现并无输出“调用”,也就是没有调用finalize()方法,这就是不必定每一个垃圾对象jvm都会自动调用finalize()方法。
1)消费端弄丢了数据
惟一可能致使消费者弄丢数据的状况,就是说,你那个消费到了这个消息,而后消费者那边自动提交了offset,让kafka觉得你已经消费好了这个消息,其实你刚准备处理这个消息,你还没处理,你本身就挂了,此时这条消息就丢咯。
这不是同样么,你们都知道kafka会自动提交offset,那么只要关闭自动提交offset,在处理完以后本身手动提交offset,就能够保证数据不会丢。可是此时确实仍是会重复消费,好比你刚处理完,还没提交offset,结果本身挂了,此时确定会重复消费一次,本身保证幂等性就行了。
生产环境碰到的一个问题,就是说咱们的kafka消费者消费到了数据以后是写到一个内存的queue里先缓冲一下,结果有的时候,你刚把消息写入内存queue,而后消费者会自动提交offset。
而后此时咱们重启了系统,就会致使内存queue里还没来得及处理的数据就丢失了
2)kafka弄丢了数据
这块比较常见的一个场景,就是kafka某个broker宕机,而后从新选举partiton的leader时。你们想一想,要是此时其余的follower恰好还有些数据没有同步,结果此时leader挂了,而后选举某个follower成leader以后,他不就少了一些数据?这就丢了一些数据啊。
生产环境也遇到过,咱们也是,以前kafka的leader机器宕机了,将follower切换为leader以后,就会发现说这个数据就丢了
因此此时通常是要求起码设置以下4个参数:
给这个topic设置replication.factor参数:这个值必须大于1,要求每一个partition必须有至少2个副本
在kafka服务端设置min.insync.replicas参数:这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟本身保持联系,没掉队,这样才能确保leader挂了还有一个follower吧
在producer端设置acks=all:这个是要求每条数据,必须是写入全部replica以后,才能认为是写成功了
在producer端设置retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了
咱们生产环境就是按照上述要求配置的,这样配置以后,至少在kafka broker端就能够保证在leader所在broker发生故障,进行leader切换时,数据不会丢失
3)生产者会不会弄丢数据
若是按照上述的思路设置了ack=all,必定不会丢,要求是,你的leader接收到消息,全部的follower都同步到了消息以后,才认为本次写成功了。若是没知足这个条件,生产者会自动不断的重试,重试无限次。
ZooKeeper是个集群,内部有多个server,每一个server均可以链接多个client,每一个client均可以修改server中的数据
ZooKeeper能够保证每一个server内的数据彻底一致,是如何实现的呢?
答:数据一致性是靠Paxos算法保证的,Paxos能够说是分布式一致性算法的鼻祖,是ZooKeeper的基础
Paxos的基本思路:(深刻解读zookeeper一致性原理)
假设有一个社团,其中有团员、议员(决议小组成员)两个角色
团员能够向议员申请提案来修改社团制度
议员坐在一块儿,拿出本身收到的提案,对每一个提案进行投票表决,超过半数经过便可生效
为了秩序,规定每一个提案都有编号ID,按顺序自增
每一个议员都有一个社团制度笔记本,上面记着全部社团制度,和最近处理的提案编号,初始为0
投票经过的规则:
新提案ID 是否大于 议员本中的ID,是议员举手赞同
若是举手人数大于议员人数的半数,即让新提案生效
例如:
刚开始,每一个议员本子上的ID都为0,如今有一个议员拿出一个提案:团费降为100元,这个提案的ID自增为1
每一个议员都和本身ID对比,一看 1>0,举手赞同,同时修改本身本中的ID为1
发出提案的议员一看超过半数赞成,就宣布:1号提案生效
而后全部议员都修改本身笔记本中的团费为100元
之后任何一个团员咨询任何一个议员:"团费是多少?",议员能够直接打开笔记本查看,并回答:团费为100元
可能会有极端的状况,就是多个议员一块儿发出了提案,就是并发的状况
例如
刚开始,每一个议员本子上的编号都为0,如今有两个议员(A和B)同时发出了提案,那么根据自增规则,这两个提案的编号都为1,但只会有一个被先处理
假设A的提案在B的上面,议员们先处理A提案并经过了,这时,议员们的本子上的ID已经变为了1,接下来处理B的提案,因为它的ID是1,不大于议员本子上的ID,B提案就被拒绝了,B议员须要从新发起提案
上面就是Paxos的基本思路,对照ZooKeeper,对应关系就是:
团员 -client
议员 -server
议员的笔记本 -server中的数据
提案 -变动数据的请求
提案编号 -zxid(ZooKeeper Transaction Id)
提案生效 -执行变动数据的操做
ZooKeeper中还有一个leader的概念,就是把发起提案的权利收紧了,之前是每一个议员均可以发起提案,如今有了leader,你们就不要七嘴八舌了,先把提案都交给leader,由leader一个个发起提案
Paxos算法就是经过投票、全局编号机制,使同一时刻只有一个写操做被批准,同时并发的写操做要去争取选票,只有得到过半数选票的写操做才会被批准,因此永远只会有一个写操做获得批准,其余的写操做竞争失败只好再发起一轮投票
1)一致性保证
Zookeeper是一种高性能、可扩展的服务。Zookeeper的读写速度很是快,而且读的速度要比写的速度更快。另外,在进行读操做的时候,ZooKeeper依然可以为旧的数据提供服务。这些都是因为ZooKeepe所提供的一致性保证,它具备以下特色:
顺序一致性
客户端的更新顺序与它们被发送的顺序相一致。
原子性
更新操做要么成功要么失败,没有第三种结果。
单系统镜像
不管客户端链接到哪个服务器,客户端将看到相同的ZooKeeper视图。
可靠性
一旦一个更新操做被应用,那么在客户端再次更新它以前,它的值将不会改变。。这个保证将会产生下面两种结果:
1.若是客户端成功地得到了正确的返回代码,那么说明更新已经成果。若是不可以得到返回代码(因为通讯错误、超时等等),那么客户端将不知道更新操做是否生效。
2.当从故障恢复的时候,任何客户端可以看到的执行成功的更新操做将不会被回滚。
实时性
在特定的一段时间内,客户端看到的系统须要被保证是实时的(在十几秒的时间里)。在此时间段内,任何系统的改变将被客户端看到,或者被客户端侦测到。
给予这些一致性保证,ZooKeeper更高级功能的设计与实现将会变得很是容易,例如:leader选举、队列以及可撤销锁等机制的实现。
2)Leader选举
ZooKeeper须要在全部的服务(能够理解为服务器)中选举出一个Leader,而后让这个Leader来负责管理集群。此时,集群中的其它服务器则成为此Leader的Follower。而且,当Leader故障的时候,须要ZooKeeper可以快速地在Follower中选举出下一个Leader。这就是ZooKeeper的Leader机制,下面咱们将简单介绍在ZooKeeper中,Leader选举(Leader Election)是如何实现的。
此操做实现的核心思想是:首先建立一个EPHEMERAL目录节点,例如“/election”。而后。每个ZooKeeper服务器在此目录下建立一个SEQUENCE|EPHEMERAL 类型的节点,例如“/election/n_”。在SEQUENCE标志下,ZooKeeper将自动地为每个ZooKeeper服务器分配一个比前一个分配的序号要大的序号。此时建立节点的ZooKeeper服务器中拥有最小序号编号的服务器将成为Leader。
在实际的操做中,还须要保障:当Leader服务器发生故障的时候,系统可以快速地选出下一个ZooKeeper服务器做为Leader。一个简单的解决方案是,让全部的follower监视leader所对应的节点。当Leader发生故障时,Leader所对应的临时节点将会自动地被删除,此操做将会触发全部监视Leader的服务器的watch。这样这些服务器将会收到Leader故障的消息,并进而进行下一次的Leader选举操做。可是,这种操做将会致使“从众效应”的发生,尤为当集群中服务器众多而且带宽延迟比较大的时候,此种状况更为明显。
在Zookeeper中,为了不从众效应的发生,它是这样来实现的:每个follower对follower集群中对应的比本身节点序号小一号的节点(也就是全部序号比本身小的节点中的序号最大的节点)设置一个watch。只有当follower所设置的watch被触发的时候,它才进行Leader选举操做,通常状况下它将成为集群中的下一个Leader。很明显,此Leader选举操做的速度是很快的。由于,每一次Leader选举几乎只涉及单个follower的操做。
1. map过程产生大量对象致使内存溢出
这种溢出的缘由是在单个map中产生了大量的对象致使的。
例如:rdd.map(x=>for(i <- 1 to 10000) yield i.toString),这个操做在rdd中,每一个对象都产生了10000个对象,这确定很容易产生内存溢出的问题。针对这种问题,在不增长内存的状况下,能够经过减小每一个Task的大小,以便达到每一个Task即便产生大量的对象Executor的内存也可以装得下。具体作法能够在会产生大量对象的map操做以前调用repartition方法,分区成更小的块传入map。例如:rdd.repartition(10000).map(x=>for(i <- 1 to 10000) yield i.toString)。
面对这种问题注意,不能使用rdd.coalesce方法,这个方法只能减小分区,不能增长分区,不会有shuffle的过程。
数据不平衡除了有可能致使内存溢出外,也有可能致使性能的问题,解决方法和上面说的相似,就是调用repartition从新分区。这里就再也不累赘了。
这是我最近才遇到的一个问题,由于hdfs中不适合存小问题,因此Spark计算后若是产生的文件过小,咱们会调用coalesce合并文件再存入hdfs中。可是这会致使一个问题,例如在coalesce以前有100个文件,这也意味着可以有100个Task,如今调用coalesce(10),最后只产生10个文件,由于coalesce并非shuffle操做,这意味着coalesce并非按照我本来想的那样先执行100个Task,再将Task的执行结果合并成10个,而是从头到位只有10个Task在执行,本来100个文件是分开执行的,如今每一个Task同时一次读取10个文件,使用的内存是原来的10倍,这致使了OOM。解决这个问题的方法是令程序按照咱们想的先执行100个Task再将结果合并成10个文件,这个问题一样能够经过repartition解决,调用repartition(10),由于这就有一个shuffle的过程,shuffle先后是两个Stage,一个100个分区,一个是10个分区,就能按照咱们的想法执行。
shuffle内存溢出的状况能够说都是shuffle后,单个文件过大致使的。在Spark中,join,reduceByKey这一类型的过程,都会有shuffle的过程,在shuffle的使用,须要传入一个partitioner,大部分Spark中的shuffle操做,默认的partitioner都是HashPatitioner,默认值是父RDD中最大的分区数,这个参数经过spark.default.parallelism控制(在spark-sql中用spark.sql.shuffle.partitions) , spark.default.parallelism参数只对HashPartitioner有效,因此若是是别的Partitioner或者本身实现的Partitioner就不能使用spark.default.parallelism这个参数来控制shuffle的并发量了。若是是别的partitioner致使的shuffle内存溢出,就须要从partitioner的代码增长partitions的数量。
在standalone的模式下若是配置了–total-executor-cores 和 –executor-memory 这两个参数,可是没有配置–executor-cores这个参数的话,就有可能致使,每一个Executor的memory是同样的,可是cores的数量不一样,那么在cores数量多的Executor中,因为可以同时执行多个Task,就容易致使内存溢出的状况。这种状况的解决方法就是同时配置–executor-cores或者spark.executor.cores参数,确保Executor资源分配均匀。
这个比较特殊,这里说记录一下,遇到过一种状况,相似这样rdd.flatMap(x=>for(i <- 1 to 1000) yield (“key”,”value”))致使OOM,可是在一样的状况下,使用rdd.flatMap(x=>for(i <- 1 to 1000) yield “key”+”value”)就不会有OOM的问题,这是由于每次(“key”,”value”)都产生一个Tuple对象,而”key”+”value”,无论多少个,都只有一个对象,指向常量池。具体测试以下:
这个例子说明(“key”,”value”)和(“key”,”value”)在内存中是存在不一样位置的,也就是存了两份,可是”key”+”value”虽然出现了两次,可是只存了一份,在同一个地址,这用到了JVM常量池的知识.因而乎,若是RDD中有大量的重复数据,或者Array中须要存大量重复数据的时候咱们均可以将重复数据转化为String,可以有效的减小内存使用.
高快省的排序算法
有没有既不浪费空间又能够快一点的排序算法呢?那就是“快速排序”啦!光听这个名字是否是就以为很高端呢。
假设咱们如今对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数做为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来作啥的了)。为了方便,就让第一个数6做为基准数吧。接下来,须要将这个序列中全部比基准数大的数放在6的右边,比基准数小的数放在6的左边,相似下面这种排列:
3 1 2 5 4 6 9 7 10 8
在初始状态下,数字6在序列的第1位。咱们的目标是将6挪到序列中间的某个位置,假设这个位置是k。如今就须要寻找这个k,而且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想想,你有办法能够作到这点吗?
方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,而后交换他们。这里能够用两个变量i和j,分别指向序列最左边和最右边。咱们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。
首先哨兵j开始出动。由于此处设置的基准数是最左边的数,因此须要让哨兵j先出动,这一点很是重要(请本身想想为何)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
如今交换哨兵i和哨兵j所指向的元素的值。交换以后的序列以下:
6 1 2 5 9 3 4 7 10 8
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,知足要求)以后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,知足要求)以后停了下来。此时再次进行交换,交换以后的序列以下:
6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,知足要求)以后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。咱们将基准数6和3进行交换。交换以后的序列以下:
3 1 2 5 4 6 9 7 10 8
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。如今基准数6已经归位,它正好处在序列的第6位。此时咱们已经将原来的序列,以6为分界点拆分红了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还须要分别处理这两个序列。由于6左边和右边的序列目前都仍是很混乱的。不过没关系,咱们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列便可。如今先来处理6左边的序列现吧。
左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧
若是你模拟的没有错,调整完毕以后的序列的顺序应该是:
2 1 3 5 4
OK,如今3已经归位。接下来须要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕以后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不须要进行任何处理。至此咱们对序列“2 1”已所有处理完毕,获得序列是“1 2”。序列“5 4”的处理也仿照此方法,最后获得的序列以下:
1 2 3 4 5 6 9 7 10 8
对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会获得这样的序列,以下
1 2 3 4 5 6 7 8 9 10
到此,排序彻底结束。细心的同窗可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到全部的数都归位为止,排序就结束了。下面上个霸气的图来描述下整个算法的处理过程。
这是为何呢?
快速排序之所比较快,由于相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数所有放到基准点的左边,将大于等于基准点的数所有放到基准点的右边。这样在每次交换的时候就不会像冒泡排序同样每次只能在相邻的数之间进行交换,交换的距离就大的多了。所以总的比较和交换次数就少了,速度天然就提升了。固然在最坏的状况下,仍多是相邻的两个数进行了交换。所以快速排序的最差时间复杂度和冒泡排序是同样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫作“二分”的思想。咱们后面还会遇到“二分”思想,到时候再聊。先上代码,以下
public class QuickSort {
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
//temp就是基准位
temp = arr[low];
while (i<j) {
//先看右边,依次往左递减
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=arr[i]&&i<j) {
i++;
}
//若是知足条件则交换
if (i<j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}
public static void main(String[] args){
int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
quickSort(arr, 0, arr.length-1);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
输出为
1
2
2
3
4
4
7
7
8
9
10
19
62
举例:
drop table if exists wc_groupsend_rp;
create external table wc_groupsend_rp (
imid string, --设备ID
wcid string, --微信号
wcname string, --微信名
wcgroupName string, --群名称
rpamount double, --红包金额
rpid string, --红包标识
rpcount int, --红包数量
rptype int, --红包类型 好比1拼手气红包,2为普通红包,3为指定人领取红包
giverpdt string, --发红包时间
setuprpdt string, --建立红包时间 点击红包按钮的时间 paydt string, --支付时间
) COMMENT '群发红包表'
PARTITIONED BY (`giverpdt` string)
row format delimited fields terminated by '\t';
drop table if exists wc_groupcash_rp;
create external table wc_groupcash_rp (
rpid string, --红包标识
imid string, --设备ID
wcid string, --微信号
wcname string, --微信名
wcgroupName string, --群名称
cashdt stirng, --红包领取时间 每领取一次更新一条数据
cashcount int, --领取人数
cashamount double, --领取金额
cashwcid string, --领取人的微信
cashwcname string, --领取人微信昵称
cashsum double, --已领取总金额
) COMMENT '红包领取表'
PARTITIONED BY (`rpid` string)
row format delimited fields terminated by '\t';
1)使用foreachPartitions替代foreach。
原理相似于“使用mapPartitions替代map”,也是一次函数调用处理一个partition的全部数据,而不是一次函数调用处理一条数据。在实践中发现,foreachPartitions类的算子,对性能的提高仍是颇有帮助的。好比在foreach函数中,将RDD中全部数据写MySQL,那么若是是普通的foreach算子,就会一条数据一条数据地写,每次函数调用可能就会建立一个数据库链接,此时就势必会频繁地建立和销毁数据库链接,性能是很是低下;可是若是用foreachPartitions算子一次性处理一个partition的数据,那么对于每一个partition,只要建立一个数据库链接便可,而后执行批量插入操做,此时性能是比较高的。实践中发现,对于1万条左右的数据量写MySQL,性能能够提高30%以上。
2)设置num-executors参数
参数说明:该参数用于设置Spark做业总共要用多少个Executor进程来执行。Driver在向YARN集群管理器申请资源时,YARN集群管理器会尽量按照你的设置来在集群的各个工做节点上,启动相应数量的Executor进程。这个参数很是之重要,若是不设置的话,默认只会给你启动少许的Executor进程,此时你的Spark做业的运行速度是很是慢的。
参数调优建议:该参数设置的太少,没法充分利用集群资源;设置的太多的话,大部分队列可能没法给予充分的资源。针对数据交换的业务场景,建议该参数设置1-5。
3)设置executor-memory参数
参数说明:该参数用于设置每一个Executor进程的内存。Executor内存的大小,不少时候直接决定了Spark做业的性能,并且跟常见的JVM OOM异常也有直接的关联。
参数调优建议:针对数据交换的业务场景,建议本参数设置在512M及如下。
4) executor-cores
参数说明:该参数用于设置每一个Executor进程的CPU core数量。这个参数决定了每一个Executor进程并行执行task线程的能力。由于每一个CPU core同一时间只能执行一个task线程,所以每一个Executor进程的CPU core数量越多,越可以快速地执行完分配给本身的全部task线程。
参数调优建议:Executor的CPU core数量设置为2~4个较为合适。建议,若是是跟他人共享一个队列,那么num-executors * executor-cores不要超过队列总CPU core的1/3~1/2左右比较合适,避免影响其余人的做业运行。
5) driver-memory
参数说明:该参数用于设置Driver进程的内存。
参数调优建议:Driver的内存一般来讲不设置,或者设置512M如下就够了。惟一须要注意的一点是,若是须要使用collect算子将RDD的数据所有拉取到Driver上进行处理,那么必须确保Driver的内存足够大,不然会出现OOM内存溢出的问题。
6) spark.default.parallelism
参数说明:该参数用于设置每一个stage的默认task数量。这个参数极为重要,若是不设置可能会直接影响你的Spark做业性能。
参数调优建议:若是不设置这个参数, Spark本身根据底层HDFS的block数量来设置task的数量,默认是一个HDFS block对应一个task。Spark官网建议的设置原则是,设置该参数为num-executors * executor-cores的2~3倍较为合适,此时能够充分地利用Spark集群的资源。针对数据交换的场景,建议此参数设置为1-10。
7) spark.storage.memoryFraction
参数说明:该参数用于设置RDD持久化数据在Executor内存中能占的比例,默认是0.6。也就是说,默认Executor 60%的内存,能够用来保存持久化的RDD数据。根据你选择的不一样的持久化策略,若是内存不够时,可能数据就不会持久化,或者数据会写入磁盘。
参数调优建议:若是Spark做业中,有较多的RDD持久化操做,该参数的值能够适当提升一些,保证持久化的数据可以容纳在内存中。避免内存不够缓存全部的数据,致使数据只能写入磁盘中,下降了性能。可是若是Spark做业中的shuffle类操做比较多,而持久化操做比较少,那么这个参数的值适当下降一些比较合适。若是发现做业因为频繁的gc致使运行缓慢(经过spark web ui能够观察到做业的gc耗时),意味着task执行用户代码的内存不够用,那么一样建议调低这个参数的值。针对数据交换的场景,建议下降此参数值到0.2-0.4。
8) spark.shuffle.memoryFraction
参数说明:该参数用于设置shuffle过程当中一个task拉取到上个stage的task的输出后,进行聚合操做时可以使用的Executor内存的比例,默认是0.2。也就是说,Executor默认只有20%的内存用来进行该操做。shuffle操做在进行聚合时,若是发现使用的内存超出了这个20%的限制,那么多余的数据就会溢写到磁盘文件中去,此时就会极大地下降性能。
参数调优建议:若是Spark做业中的RDD持久化操做较少,shuffle操做较多时,建议下降持久化操做的内存占比,提升shuffle操做的内存占比比例,避免shuffle过程当中数据过多时内存不够用,必须溢写到磁盘上,下降了性能。若是发现做业因为频繁的gc致使运行缓慢,意味着task执行用户代码的内存不够用,那么一样建议调低这个参数的值。针对数据交换的场景,建议此值设置为0.1或如下。
资源参数参考示例
如下是一份spark-submit命令的示例,能够参考一下,并根据本身的实际状况进行调节:
./bin/spark-submit \
--master yarn-cluster \
--num-executors 1 \
--executor-memory 512M \
--executor-cores 2 \
--driver-memory 512M \
--conf spark.default.parallelism=2 \
--conf spark.storage.memoryFraction=0.2 \
--conf spark.shuffle.memoryFraction=0.1 \
首先它们有哪些共同点?flink和spark都是apache 软件基金会(ASF)旗下顶级项目,都是通用数据处理平台。它们能够应用在不少的大数据应用和处理环境。而且有以下扩展:
而且二者都可在不依赖于其余环境的状况下运行于standalone模式,或是运行在基于hadoop(YARN,HDFS)之上,因为它们均是运行于内存,因此他们表现的都比hadoop要好不少。
然而它们在实现上仍是有不少不一样点:
在spark 1.5.x以前的版本,数据集的大小不能大于机器的内存数。
Flink在进行集合的迭代转换时能够是循环或是迭代计算处理。这使得Join算法、对分区的连接和重用以及排序能够选择最优算法。固然flink也是一个很强大的批处理工具。flink的流式处理的是真正的流处理。流式数据一但进入就实时进行处理,这就容许流数据灵活地在操做窗口。它甚至能够在使用水印的流数中处理数据(It is even capable of handling late data in streams by the use of watermarks)。此外,flink的代码执行引擎还对现有使用storm,mapreduce等有很强的兼容性。
Spark 在另外一方面是基于 弹性分布式数据集(RDD),这(主要的)给于spark基于内存内数据结构的函数式编程。它能够经过固定的内存给于大批量的计算。spark streaming 把流式数据封装成小的批处理,也就是它收集在一段时间内到达的全部数据,并在收集的数据上运行一个常规批处理程序。同时一边收集下一个小的批处理数据。
代理模式是什么
代理模式是一种设计模式,简单说便是在不改变源码的状况下,实现对目标对象的功能扩展。
好比有个歌手对象叫Singer,这个对象有一个唱歌方法叫sing()。
public class Singer{
public void sing(){
System.out.println("唱一首歌");
}
}
假如你但愿,经过你的某种方式生产出来的歌手对象,在唱歌先后还要想观众问好和答谢,也即对目标对象Singer的sing方法进行功能扩展。
public void sing(){
System.out.println("向观众问好");
System.out.println("唱一首歌");
System.out.println("谢谢你们");
}
可是每每你又不能直接对源代码进行修改,多是你但愿原来的对象还保持原来的样子,又或许你提供的只是一个可插拔的插件,甚至你有可能都不知道你要对哪一个目标对象进行扩展。这时就须要用到java的代理模式了。网上好多用生活中的经理人的例子来解释“代理”,看似通俗易懂,但我以为不适合程序员去理解。程序员应该从代码的本质入手。
Java的三种代理模式
想要实现以上的需求有三种方式,这一部分咱们只看三种模式的代码怎么写,先不涉及实现原理的部分。
1.静态代理
public interface ISinger {
voidsing();
}
/**
* 目标对象实现了某一接口
*/
public class Singer implements ISinger{
public void sing(){
System.out.println("唱一首歌");
}
}
/**
* 代理对象和目标对象实现相同的接口
*/
public class SingerProxy implementsI Singer{
//接收目标对象,以便调用sing方法
private ISinger target;
public UserDaoProxy(ISinger target){
this.target=target;
}
//对目标对象的sing方法进行功能扩展
public void sing() {
System.out.println("向观众问好");
target.sing();
System.out.println("谢谢你们");
}
}
测试
/**
* 测试类
*/
public classTest {
public static void main(String[] args) {
//目标对象
ISinger target =newSinger();
//代理对象
ISinger proxy =newSingerProxy(target);
//执行的是代理的方法
proxy.sing();
}
}
总结:其实这里作的事情无非就是,建立一个代理类SingerProxy,继承了ISinger接口并实现了其中的方法。只不过这种实现特地包含了目标对象的方法,正是这种特征使得看起来像是“扩展”了目标对象的方法。假使代理对象中只是简单地对sing方法作了另外一种实现而没有包含目标对象的方法,也就不能算做代理模式了。因此这里的包含是关键。
缺点:这种实现方式很直观也很简单,但其缺点是代理对象必须提早写出,若是接口层发生了变化,代理对象的代码也要进行维护。若是能在运行时动态地写出代理对象,不但减小了一大批代理类的代码,也少了不断维护的烦恼,不过运行时的效率一定受到影响。这种方式就是接下来的动态代理。
2.动态代理(也叫JDK代理)
跟静态代理的前提同样,依然是对Singer对象进行扩展
public interface ISinger {
void sing();
}
/**
* 目标对象实现了某一接口
*/
public class Singer implements ISinger{
public void sing(){
System.out.println("唱一首歌");
}
}
这回直接上测试,因为java底层封装了实现细节(以后会详细讲),因此代码很是简单,格式也基本上固定。
调用Proxy类的静态方法newProxyInstance便可,该方法会返回代理类对象
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
接收的三个参数依次为:
·ClassLoader loader:指定当前目标对象使用类加载器,写法固定
·Class<?>[] interfaces:目标对象实现的接口的类型,写法固定
·InvocationHandler h:事件处理接口,需传入一个实现类,通常直接使用匿名内部类