• 关于 java的循环且 的搜索结果

问题

为什么System.arraycopy是Java固有的?

保持可爱mmm 2020-02-08 19:13:52 0 浏览量 回答数 1

问题

【精品问答】Java实战200例(附源码)

珍宝珠 2020-02-14 11:55:46 13825 浏览量 回答数 8

问题

【精品问答】Java基础测试题答案

游客pklijor6gytpx 2019-12-01 22:02:11 2450 浏览量 回答数 2

Java学习路线 26门免费课程

排名第一的编程语言,从事云计算、大数据开发工作必备

问题

【Java学习全家桶】1460道Java热门问题,阿里百位技术专家答疑解惑

管理贝贝 2019-12-01 20:07:15 27612 浏览量 回答数 19

回答

Java集合容器主要有以下几类:1,内置容器:数组2,list容器:Vetor,Stack,ArrayList,LinkedList,CopyOnWriteArrayList(1.5),AttributeList(1.5),RoleList(1.5),RoleUnresolvedList(1.5),ConcurrentLinkedQueue(1.5),ArrayBlockingQueue(1.5),LinkedBlockingQueue(1.5),PriorityQueue(1.5),PriorityBlockingQueue(1.5),SynchronousQueue(1.5)3,set容器:HashSet(1.2),LinkedHashSet(1.4),TreeSet(1.2),CopyOnWriteArraySet(1.5),EnumSet(1.5),JobStateReasons。4,map容器:Hashtable,HashMap(1.2),TreeMap(1.2),LinkedHashMap(1.4),WeakHashMap(1.2),IdentityHashMap(1.4),ConcurrentMap(1.5),concurrentHashMap(1.5)。注意:Vector,Stack,Hashtable是Java1.2前的容器。虽然在Java2之前,Java是没有完整的集合框架的。它只有一些简单的可以自扩展的容器类。但是在Java2后他们还是被融入到了集合框架的,不过只是历史遗留而已。它们和1.2前应该还是有些变化的,虽然本质没什么变化。Set接口继承于Collection,但不允许重复,使用自己内部的一个排列机制。List接口继承Collection,允许重复,以元素安插的次序来放置元素,不会重新排列。Map接口是一组成对的键-值对象,即所持有的是key-value pairs。Map中不能有重复的key。拥有自己的内部排列机制。一、Java1.2之前的容器类库其实在Java2之前,Java是没有完整的集合框架的。它只有一些简单的可以自扩展的容器类,比如Vector,Stack,Hashtable等。Java1容器类库设计的一个重大失误是竟然没有对容器进行排序的工具。比如你想让Vector容器中的对象按字典顺序进行排序,你就要自己实现。1.1、Vectorjava.util.Vector中包含的元素可以通过一个整型的索引值取得,它的大小可以在添加或移除元素时自动增加或缩小。Vector的操作很简单,通过addElement()加入一个对象,用elementAt()取出它,还可以查询当前所保存的对象的个数size();另外还有一个Enumeration类提供了连续操作Vector中元素的方法,这可以通过Vector中的elements()方法来获取一个Enumeration类的对象,可以用一个While循环来遍历其中的元素。用hasMoreElements()检查其中是否还有更多的元素。用nextElement()获得下一个元素。Enumeration的用意在于使你能完全不用理会你要遍历的容器的基础结构,只关注你的遍历方法,这也就使得遍历方法的重用成为可能。由于这种思想的强大功能,所以在Java2中被保留下来,不过具体实现,方法名和内部算法都改变了,这就是Java2中的Iterator以及ListIterator类。然而Enumeration的功能却十分有限,比如只能朝一个方向进行,只能读取而不能更改等。更多内容请参考《Vector》1.2、Stackjava.util.Stack最常用的操作便是压入和弹出,最后压入的元素最先被弹出。它遵循后进先出(LIFO)原则。在Java中Stack的的用法也很简单,有push()压入一个元素,用pop()弹出一个元素。更多内容请参考《Stack容器》1.3、HashtableHashtable与Java2中的Map类似,可以看成一种关联或映射数组,可以将两个毫无关系的对象相关联。它的基本目标是实现两个对象之间进行关联。更多内容请参考《Hashtable》二、Java2中的容器类库自Java1.2之后Java版本统称为Java2,Java2中的容器类库才可以说是一种真正意义上的集合框架的实现。基本完全重新设计,但是又对Java1中的一些容器类库在新的设计上进行了保留,这主要是为了向下兼容的目的,当用 Java2开发程序时,应尽量避免使用它们,Java2的集合框架已经完全可以满足你的需求。在Java1中容器类库是同步化的,而 Java2中的容器类库都是非同步化,这可能是对执行效率进行考虑的结果。Java2中的集合框架提供了一套设计优良的接口和类,使程序员操作成批的数据或对象元素极为方便。这些接口和类有很多对抽象数据类型操作的API,而这是我们常用的且在数据结构中熟知的。例如Maps,Sets,Lists,Arrays等。并且Java用面向对象的设计对这些数据结构和算法进行了封装,这就极大的减化了程序员编程时的负担。程序员也可以以这个集合框架为基础,定义更高级别的数据抽象,比如栈、队列和线程安全的集合等,从而满足自己的需要。Java2的集合框架,抽其核心,主要有三类:List(包括List,Queue,BlockingQueue)、Set和Map。List和Set继承了Collection,而Map则独成一体。初看上去可能会对Map独成一体感到不解,它为什么不也继承Collection呢?但是这种设计是合理的。一个Map提供了通过Key对Map中存储的Value进行访问,也就是说它操作的都是成对的对象元素,比如put()和get()方法,而这是一个Set或List 所不就具备的。当然在需要时,你可以由keySet()方法或values()方法从一个Map中得到键的Set集或值的Collection集。集合框架中还有两个很实用的公用类:Collections和Arrays。Collections提供了对一个Collection容器进行诸如排序、复制、查找和填充等一些非常有用的方法, Arrays则是对一个数组进行类似的操作。2.1、CollectionCollection接口提供了一组操作成批对象的方法。(它只是个接口)它提供了基本操作如添加、删除。它也支持查询操作如是否为空isEmpty()方法等。为了支持对Collection进行独立操作,Java的集合框架给出了一个Iterator,它使得你可以泛型操作一个Collection,而不需知道这个 Collection的具体实现类型是什么。它的功能与Java1中的Enumeration类似,只是更易掌握和使用,功能也更强大。在建立集合框架时,Sun的开发团队考虑到需要提供一些灵活的接口,用来操作成批的元素,又为了设计的简便,就把那些对集合进行可选操作的方法与基本方法放到了一起。因为一个接口的实现者必须提供对接口中定义的所有方法的实现,这就需要一种途径让调用者知道它正在调用 的可选方法当前不支持。最后开发团队选择使用一种信号,也即抛出一种不支持操作例外(UnsupportedOperationException),如果你在使用一个Collection中遇到一个上述的例外,那就意味着你的操作失败,比如你对一个只读Collection添加一个元素时,你就会得到一个不支持操作例外。在你实现一个集合接口时,你可以很容易的在你不想让用户使用的方法中抛出UnsupportOperationException来告诉使用者这个方法当前没有实现,UnsupportOperationException是RuntimeException的一个扩展。另外Java2的容器类库还有一种Fail fast的机制。比如你正在用一个Iterator遍历一个容器中的对象,这时另外一个线程或进程对那个容器进行了修改,那么再用next()方法时可能会有灾难性的后果,而这是你不愿看到的,这时就会引发一个ConcurrentModificationException例外。这就是 fail-fast。

51干警网 2019-12-02 01:42:48 0 浏览量 回答数 0

问题

【精品问答】Java必备核心知识1000+(附源码)

问问小秘 2019-12-01 22:00:28 870 浏览量 回答数 1

问题

【精品问答】关于Java,那些入门级的面试题

问问小秘 2020-03-27 18:39:09 1073 浏览量 回答数 3

问题

【求助】使用POI生成excel的巨坑? 400 报错

爱吃鱼的程序员 2020-06-05 13:15:35 0 浏览量 回答数 1

回答

一、内存溢出类型 1、java.lang.OutOfMemoryError: PermGen space JVM管理两种类型的内存,堆和非堆。堆是给开发人员用的上面说的就是,是在JVM启动时创建;非堆是留给JVM自己用的,用来存放类的信息的。它和堆不同,运行期内GC不会释放空间。如果web app用了大量的第三方jar或者应用有太多的class文件而恰好MaxPermSize设置较小,超出了也会导致这块内存的占用过多造成溢出,或者tomcat热部署时侯不会清理前面加载的环境,只会将context更改为新部署的,非堆存的内容就会越来越多。 PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用中有很CLASS的话,就很可能出现PermGen space错误,这种错误常见在web服务器对JSP进行pre compile的时候。如果你的WEB APP下都用了大量的第三方jar, 其大小超过了jvm默认的大小(4M)那么就会产生此错误信息了。 一个最佳的配置例子:(经过本人验证,自从用此配置之后,再未出现过tomcat死掉的情况) set JAVA_OPTS=-Xms800m -Xmx800m -XX:PermSize=128M -XX:MaxNewSize=256m -XX:MaxPermSize=256m 2、java.lang.OutOfMemoryError: Java heap space 第一种情况是个补充,主要存在问题就是出现在这个情况中。其默认空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。如果内存剩余不到40%,JVM就会增大堆到Xmx设置的值,内存剩余超过70%,JVM就会减小堆到Xms设置的值。所以服务器的Xmx和Xms设置一般应该设置相同避免每次GC后都要调整虚拟机堆的大小。假设物理内存无限大,那么JVM内存的最大值跟操作系统有关,一般32位机是1.5g到3g之间,而64位的就不会有限制了。 注意:如果Xms超过了Xmx值,或者堆最大值和非堆最大值的总和超过了物理内存或者操作系统的最大限制都会引起服务器启动不起来。 垃圾回收GC的角色 JVM调用GC的频度还是很高的,主要两种情况下进行垃圾回收: 当应用程序线程空闲;另一个是java内存堆不足时,会不断调用GC,若连续回收都解决不了内存堆不足的问题时,就会报out of memory错误。因为这个异常根据系统运行环境决定,所以无法预期它何时出现。 根据GC的机制,程序的运行会引起系统运行环境的变化,增加GC的触发机会。 为了避免这些问题,程序的设计和编写就应避免垃圾对象的内存占用和GC的开销。显示调用System.GC()只能建议JVM需要在内存中对垃圾对象进行回收,但不是必须马上回收, 一个是并不能解决内存资源耗空的局面,另外也会增加GC的消耗。 二、JVM内存区域组成 简单的说java中的堆和栈 java把内存分两种:一种是栈内存,另一种是堆内存 1。在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配; 2。堆内存用来存放由new创建的对象和数组 在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由java虚拟机的自动垃圾回收器来管理 堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。缺点就是要在运行时动态分配内存,存取速度较慢; 栈的优势是存取速度比堆要快,缺点是存在栈中的数据大小与生存期必须是确定的无灵活性。 java堆分为三个区:New、Old和Permanent GC有两个线程: 新创建的对象被分配到New区,当该区被填满时会被GC辅助线程移到Old区,当Old区也填满了会触发GC主线程遍历堆内存里的所有对象。Old区的大小等于Xmx减去-Xmn java栈存放 栈调整:参数有+UseDefaultStackSize -Xss256K,表示每个线程可申请256k的栈空间 每个线程都有他自己的Stack 三、JVM如何设置虚拟内存 提示:在JVM中如果98%的时间是用于GC且可用的Heap size 不足2%的时候将抛出此异常信息。 提示:Heap Size 最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx选项设置为相同,而-Xmn为1/4的-Xmx值。 提示:JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。 默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。 提示:假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。 简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制, 这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了 提示:注意:如果Xms超过了Xmx值,或者堆最大值和非堆最大值的总和超过了物理内存或者操作系统的最大限制都会引起服务器启动不起来。 提示:设置NewSize、MaxNewSize相等,"new"的大小最好不要大于"old"的一半,原因是old区如果不够大会频繁的触发"主" GC ,大大降低了性能 JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64; 由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。 解决方法:手动设置Heap size 修改TOMCAT_HOME/bin/catalina.bat 在“echo "Using CATALINA_BASE: $CATALINA_BASE"”上面加入以下行: JAVA_OPTS="-server -Xms800m -Xmx800m -XX:MaxNewSize=256m" 四、性能检查工具使用 定位内存泄漏: JProfiler工具主要用于检查和跟踪系统(限于Java开发的)的性能。JProfiler可以通过时时的监控系统的内存使用情况,随时监视垃圾回收,线程运行状况等手段,从而很好的监视JVM运行情况及其性能。 1. 应用服务器内存长期不合理占用,内存经常处于高位占用,很难回收到低位; 2. 应用服务器极为不稳定,几乎每两天重新启动一次,有时甚至每天重新启动一次; 3. 应用服务器经常做Full GC(Garbage Collection),而且时间很长,大约需要30-40秒,应用服务器在做Full GC的时候是不响应客户的交易请求的,非常影响系统性能。 因为开发环境和产品环境会有不同,导致该问题发生有时会在产品环境中发生,通常可以使用工具跟踪系统的内存使用情况,在有些个别情况下或许某个时刻确实是使用了大量内存导致out of memory,这时应继续跟踪看接下来是否会有下降, 如果一直居高不下这肯定就因为程序的原因导致内存泄漏。 五、不健壮代码的特征及解决办法 1、尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。 对于仍然有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率; 2、我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域; String str = "aaa"; String str2 = "bbb"; String str3 = str + str2;//假如执行此次之后str ,str2以后再不被调用,那它就会被放在内存中等待Java的gc去回收,程序内过多的出现这样的情况就会报上面的那个错误,建议在使用字符串时能使用StringBuffer就不要用String,这样可以省不少开销; 3、尽量少用静态变量,因为静态变量是全局的,GC不会回收的; 4、避免集中创建对象尤其是大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境;显示的声明数组空间,而且申请数量还极大。 这是一个案例想定供大家警戒 使用jspsmartUpload作文件上传,运行过程中经常出现java.outofMemoryError的错误, 检查之后发现问题:组件里的代码 m_totalBytes = m_request.getContentLength(); m_binArray = new byte[m_totalBytes]; 问题原因是totalBytes这个变量得到的数极大,导致该数组分配了很多内存空间,而且该数组不能及时释放。解决办法只能换一种更合适的办法,至少是不会引发outofMemoryError的方式解决。 5、尽量运用对象池技术以提高系统性能;生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。 6、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。可以适当的使用hashtable,vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃 7、一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成 Out Of Memory Error 的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。 “答案来源于网络,供您参考” 希望以上信息可以帮到您!

牧明 2019-12-02 02:16:21 0 浏览量 回答数 0

回答

一.Lock接口(java.util.concurrent.locks): void lock():获取锁,阻塞方式;如果资源已被其他线程锁定,那么lock将会阻塞直到获取锁,锁阻塞期间不受线程的Interrupt的影响,在获取锁成功后,才会检测线程的interrupt状态,如果interrupt=true,则抛出异常。 unlock():释放锁 tryLock():尝试获取锁,并发环境中"闯入"行为,如果有锁可用,直接获取锁并返回true,否则范围false. lockInterruptibly():尝试获取锁,并支持"中断"请求。与lock的区别时,此方法的开始、结束和执行过程中,都会不断检测线程的interrupt状态,如果线程被中断,则立即抛出异常;而不像lock方法那样只会在获取锁之后才检测。 二.Lock接口实现类 Lock直接实现,只有3个类:ReentrantLock和WriteLock/ReadLock;这三种锁;Lock和java的synchronized(内置锁)的功能一致,均为排他锁. ReentrantLock为重入排他锁,对于同一线程,如果它已经持有了锁,那么将不会再次获取锁,而直接可以使用. ReentrantReadWriteLock并没有继承ReentrantLock,而是一个基于Lock接口的单独实现.它实现了 ReadWriteLock,即读写分离锁,是一种采用锁分离技巧的API. 尽管在API级别ReentrantReadWriteLock和ReentrantLock没有直接继承关系,但是ReentrantReadWriteLock中的ReadLock和WriteLock都具有ReentrantLock的全部语义(简单说,就是把ReentrantLock的代码copy了一下.),即锁的可重入性.WriteLock支持Condition(条件),ReadLock不支持. Lock的实现类中,都包含了2中锁等待策略:公平和非公平;其实他们的实现也非常简单,底层都是使用了queue来维持锁请求顺序.[参考:http://shift-alt-ctrl.iteye.com/blog/1839142] 公平锁,就是任何锁请求,首先将请求加入队列,然后再有队列机制来决定,是阻塞还是分配锁. 非公平,就是允许"闯入",当然公平锁,也无法干扰"闯入",对于任何锁请求,首先检测锁状态是否可用,如果可用直接获取,否则加入队列.. ReentrantLock本质上和synchronized修饰词是同一语义,如果一个线程lock()之后,其他线程进行lock时必须阻塞,直到当前线程的前续线程unlock.[执行lock操作时,将会被队列化(假如在公平模式下),获取lock的线程都将具有前续/后继线程,前续线程就是当前线程之前执行lock操作而阻塞的线程,后继线程就是当前线程之后执行lock操作的线程;那么对于unlock操作就是"解锁"信号的传递,如果当前线程unlock,那么将会触发后继线程被"唤醒",即它因为lock操作阻塞状态被解除.];这是ReentrantLock的基本原理,但是当ReentrantLock在Conditon情况下,事情就变得更加复杂.[参加下述] 三.Condition:锁条件 Condition与Lock形成happen-before关系。Condition将Object的监视器方法(wait,notify,notifyAll)分解成截然不同的对象,以便通过这些对象与任意Lock实现组合。使Lock具有等待“集合”的特性,或者“类型”;Lock替代了synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。(synchronized + object.wait对应Lock + Condition.await) Condition又称条件队列,为线程提供了一个含义,以便在某种状态条件现在可能为true的其他线程通知它之前,一直挂起该线程。即多个线程,其中一个线程因为某个条件而阻塞,其他线程当“条件”满足时,则“通知”哪些阻塞的线程。这,几乎和object中wait和notify的机制一样。 Condition和wait一样,阻塞时也将原子性的释放锁(间接执行了release()方法)。并挂起线程。Condition必须与Lock形成关系,只有获取lock权限的,才能进行Condition操作。Condition底层基于AQS实现,条件阻塞,将以队列的方式,LockSupport支持。其实现类有ConditionObject,这也是Lock.newCondition()的返回实际类型,在等待 Condition 时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为 Condition 应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。 void await() throws InterruptedException:当前线程阻塞,并原子性释放对象锁。如下条件将触发线程唤醒: 当线程被中断(支持中断响应), 其他线程通过condition.signal()方法,且碰巧选中当前线程唤醒 其他线程通过condition.signalAll()方法 发生虚假唤醒 底层实现,await()方法将当前线程信息添加到Conditon内部维护的"await"线程队列的尾部(此队列的目的就是为singal方法保持亟待唤醒的线程的顺序),然后释放锁(执行tryRelease()方法,注意此处释放锁,仅仅是释放了锁信号,并不是unlock,此时其他线程仍不能获取锁--lock方法阻塞),然后使用LockSupport.park(this)来强制剥夺当前线程执行权限。await方法会校验线程的中断标记。 由此可见,await()方法执行之后,因为已经"归还"了锁信号,那么其他线程此时执行lock方法,将不再阻塞.. void awaitUninterruptibly():阻塞,直到被唤醒。此方法不响应线程中断请求。即当线程被中断时,它将继续等待,直到接收到signal信号(你应该能想到"陷阱"),当最终从此方法返回时,仍然将设置其中断状态。 void signal()/signalAll():唤醒一个/全部await的线程。 对于signal()方法而言,底层实现为,遍历await"线程队列,找出此condition上最先阻塞的线程,并将此阻塞线程unpark.至此为止,我们似乎发现"锁信号"丢失了,因为在线程await时通过tryRelease时释放了一次信号.那么被signal成功的线程,首先执行一次acquire(增加锁信号),然后校验自己是否被interrupted,如果锁信号获取成功且线程状态正常,此时才正常的从await()方法退出.经过这么复杂的分析,终于明白了ReentrantLock + Condition情况下,锁状态变更和线程控制的来龙去脉... Java代码 收藏代码 //////例子: private Lock lock = new ReentrantLock(); private Condition full = lock.newCondition(); private Condition empty = lock.newCondition(); public Object take(){ lock.lock(); try{ while(isEmpty()){ empty.await() } Object o = get() full.signalAll(); return o; }finally{ lock.unlock(); } } public void put(Object o){ lock.lock(); try{ while(isFull()){ full.await(); } put(o); empty.signalAll(); }finally{ lock.unlock(); } } 四.机制 Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。注意,Lock 实例只是普通的对象,其本身可以在 synchronized 语句中作为目标使用。获取 Lock 实例的监视器锁与调用该实例的任何 lock() 方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock 实例。 Lock接口具有的方法: void lock():获取锁,阻塞直到获取。 void lockInterruptibly() throws InterrutedException:获取锁,阻塞直到获取成功,支持中断响应。 boolean tryLock():尝试获取锁,返回是否获取的结果。如果碰巧获取成功,则返回true,此时已经持有锁。 boolean tryLock(long time,TimeUnit) throws InterruptedException:尝试获取锁,获取成功返回true,超时时且没有获取锁则返回false。 void unlock():释放锁。约定只有持有锁者才能释放锁,否则抛出异常。 void newCondition():返回绑定到lock的条件。 五.ReadWriteLock ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer(写锁),读取锁可以由多个 reader 线程同时保持(共享锁)。写入锁是独占的。所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果也要保持与相关 readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。 与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。 Lock readLock():返回读锁。 Lock writeLock():返回写锁。 六.ReentrantLock ReentrantLock,重入排它锁,它和synchronized具有相同的语义以及在监视器上具有相同的行为,但是功能更加强大。 ReetrantLock将由最近成功获得锁且还没有释放锁的线程标记为“锁占有者”;当锁没有被线程持有时,调用lock方法将会成功获取锁并返回,如果当前线程为锁持有者,再次调用lock将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ReentrantLock的构造方法,允许接收一个“公平策略”参数,“公平策略”下,多个线程竞争获取锁时,将会以队列化锁请求者,并将锁授予队列的head。在“非公平策略”下,则不完全保证锁获取的顺序,允许闯入行为(tryLock)。 ReentrantLock基于AQS机制,锁信号量为1,如果信号量为1且当前锁持有者不为自己,则不能获取锁。释放锁时,如果当前锁持有者不是自己,也将抛出“IllegalMonitorStateException”。由此可见,对于ReentrantLock,lock和release方法是需要组合出现。 七.ReentrantReadWriteLock:可重入读写分离锁 重入性 :当前线程可以重新获取相应的“读锁”或者“写锁”,在写入线程保持的所有写入锁都已经释放后,才允许重入reader(读取线程)使用它们。writer线程可以获取读锁,但是reader线程却不能直接获取写锁。 锁降级:重入还允许写入锁降级为读锁,其实现方式为:先获取写入锁,然后获取读取锁,最后释放写入锁。但是读取锁不能升级为写入锁。 Conditon的支持:只有写入锁支持conditon,对于读取锁,newConditon方法直接抛出UnsupportedOperationException。 ReentrantReadWriteLock目前在java api中无直接使用。ReentrantReadWriteLock并没有继承自 ReentrantLock,而是单独重新实现。其内部仍然支持“公平性”“非公平性”策略。 ReentrantReadWriteLock基于AQS,但是AQS只有一个state来表示锁的状态,所以如果一个state表示2种类型的锁状态,它做了一个很简单的策略,“位运算”,将一个int类型的state拆分为2个16位段,左端表示readlock锁引用计数,右端16位表示write锁。在readLock、writeLock进行获取锁或者释放锁时,均是通过有效的位运算和位控制,来达到预期的效果。 八.ReadLock void lock():获取读取锁,伪代码如下: Java代码 收藏代码 //如果当前已经有“写锁”,且持有写锁者不是当前线程(如果是当前线程,则支持写锁,降级为读锁),则获取锁失败 //即任何读锁的获取,必须等待队列中的写锁释放 //c为实际锁引用量(exclusiveCount方法实现为:c & ((1<<16) -1) if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current) return -1; //CAS操作,操作state的左端16位。 if(CAS(c,c + (1<<16))){ return 1; } void unlock():释放read锁,即共享锁,伪代码如下: Java代码 收藏代码 //CAS锁引用 for (;;) { int c = getState(); int nextc = c - (1<<16);//位操作,释放一个锁。 if (compareAndSetState(c, nextc)) return nextc == 0; } 九.WriteLock void lock():获取写入锁,伪代码如下: Java代码 收藏代码 //当前线程 Thread current = Thread.currentThread(); //实际的锁引用state int c = getState(); //右端16位,通过位运算获取“写入锁”的state int w = exclusiveCount(c); //如果有锁引用 if (c != 0) { //且所引用不是自己 if (w == 0 || current != getExclusiveOwnerThread()){ return false; } } //如果写入锁state为0,且CAS成功,则设置state和独占线程信息 if ((w == 0 && writerShouldBlock(current)) ||!compareAndSetState(c, c + acquires)){ return false; } setExclusiveOwnerThread(current); return true; void unlock():释放写入锁,伪代码如下: Java代码 收藏代码 //计算释放锁的信号量 int nextc = getState() - releases; //对于写入锁,则校验当前线程是否为锁持有者,否则不可以释放(死锁) if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); //释放锁,且重置独占线程信息 if (exclusiveCount(nextc) == 0) { setExclusiveOwnerThread(null); setState(nextc); return true; } else { setState(nextc); return false; } 十.LockSupport:用来创建锁和其他同步类的基本线程阻塞原语。 底层基于hotspot的实现unsafe。park 和 unpark 方法提供了阻塞和解除阻塞线程的有效方法。三种形式的 park(即park,parkNanos(Object blocker,long nanos),parkUntil(Object blocker,long timestamp)) 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。 在锁实现中提供的作为 blocker 的普通参数是 this。 static void park(Object blocker):阻塞当前线程,直到如下情况发生: 其他线程,调用unpark方法,并将此线程作为目标而唤醒 其他线程中断当前线程此方法不报告,此线程是何种原因被放回,需要调用者重新检测,而且此方法也经常在while循环中执行 Java代码 收藏代码 while(//condition,such as:queue.isEmpty){ LockSupport.park(queue);//此时queue对象作为“阻塞”点传入,以便其他监控工具查看,queue的状态 //检测当前线程是否已经中断。 if(Thread.interrupted()){ break; } } void getBlocker(Thread t):返回提供最近一次尚未解除阻塞的park的阻塞点。可以返回null。 void unpark(Thread t):解除指定线程阻塞,使其可用。参数null则无效果。 LockSupport实例(不过不建议在实际代码中直接使用LockSupport,很多时候,你可以使用锁来控制): Java代码 收藏代码 /////////////Demo public class LockSupportTestMain { /** * @param args */ public static void main(String[] args) throws Exception{ System.out.println("Hear!"); BlockerObject blocker = new BlockerObject(); LThread tp = new LThread(blocker, false); LThread tt = new LThread(blocker, true); tp.start(); tt.start(); Thread.sleep(1000); } static class LThread extends Thread{ private BlockerObject blocker; boolean take; LThread(BlockerObject blocker,boolean take){ this.blocker = blocker; this.take = take; } @Override public void run(){ if(take){ while(true){ Object o = blocker.take(); if(o != null){ System.out.println(o.toString()); } } }else{ Object o = new Object(); System.out.println("put,,," + o.toString()); blocker.put(o); } } } static class BlockerObject{ Queue<Object> inner = new LinkedList<Object>(); Queue<Thread> twaiters = new LinkedList<Thread>(); Queue<Thread> pwaiters = new LinkedList<Thread>(); public void put(Object o){ inner.offer(o); pwaiters.offer(Thread.currentThread()); Thread t = twaiters.poll(); if(t != null){ LockSupport.unpark(t); } System.out.println("park"); LockSupport.park(Thread.currentThread()); System.out.println("park is over"); } public Object take(){ Thread t = pwaiters.poll(); if(t != null){ System.out.println("unpark"); LockSupport.unpark(t); System.out.println("unpark is OK"); } //twaiters.offer(Thread.currentThread()); return inner.poll(); } } } 备注:有时候会疑惑wait()/notify() 和Unsafe.park()/unpark()有什么区别?区别是wait和notify是Object类的方法,它们首选需要获得“对象锁”,并在synchronized同步快中执行。park和unpark怎不需要这么做。wait和park都是有当前线程发起,notify和unpark都是其他线程发起。wait针对的是对象锁,park针对的线程本身,但是最终的效果都是导致当前线程阻塞。Unsafe不建议开发者直接使用。

景凌凯 2020-04-24 16:41:16 0 浏览量 回答数 0

回答

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 java 死锁产生的四个必要条件: 1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用 2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。 3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。 4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。 当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。 解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现。 而如果不恰当的使用了锁,且出现同时要锁多个对象时,会出现死锁情况,如下: LockTest.java 文件 import java.util.Date; public class LockTest { public static String obj1 = "obj1"; public static String obj2 = "obj2"; public static void main(String[] args) { LockA la = new LockA(); new Thread(la).start(); LockB lb = new LockB(); new Thread(lb).start(); } } class LockA implements Runnable{ public void run() { try { System.out.println(new Date().toString() + " LockA 开始执行"); while(true){ synchronized (LockTest.obj1) { System.out.println(new Date().toString() + " LockA 锁住 obj1"); Thread.sleep(3000); // 此处等待是给B能锁住机会 synchronized (LockTest.obj2) { System.out.println(new Date().toString() + " LockA 锁住 obj2"); Thread.sleep(60 * 1000); // 为测试,占用了就不放 } } } } catch (Exception e) { e.printStackTrace(); } } } class LockB implements Runnable{ public void run() { try { System.out.println(new Date().toString() + " LockB 开始执行"); while(true){ synchronized (LockTest.obj2) { System.out.println(new Date().toString() + " LockB 锁住 obj2"); Thread.sleep(3000); // 此处等待是给A能锁住机会 synchronized (LockTest.obj1) { System.out.println(new Date().toString() + " LockB 锁住 obj1"); Thread.sleep(60 * 1000); // 为测试,占用了就不放 } } } } catch (Exception e) { e.printStackTrace(); } } } 以上代码运行输出结果为: Tue May 05 10:51:06 CST 2015 LockB 开始执行 Tue May 05 10:51:06 CST 2015 LockA 开始执行 Tue May 05 10:51:06 CST 2015 LockB 锁住 obj2 Tue May 05 10:51:06 CST 2015 LockA 锁住 obj1 此时死锁产生。 为了解决这个问题,我们不使用显示的去锁,我们用信号量去控制。 信号量可以控制资源能被多少线程访问,这里我们指定只能被一个线程访问,就做到了类似锁住。而信号量可以指定去获取的超时时间,我们可以根据这个超时时间,去做一个额外处理。 对于无法成功获取的情况,一般就是重复尝试,或指定尝试的次数,也可以马上退出。 来看下如下代码: UnLockTest.java 文件 import java.util.Date; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class UnLockTest { public static String obj1 = "obj1"; public static final Semaphore a1 = new Semaphore(1); public static String obj2 = "obj2"; public static final Semaphore a2 = new Semaphore(1); public static void main(String[] args) { LockAa la = new LockAa(); new Thread(la).start(); LockBb lb = new LockBb(); new Thread(lb).start(); } } class LockAa implements Runnable { public void run() { try { System.out.println(new Date().toString() + " LockA 开始执行"); while (true) { if (UnLockTest.a1.tryAcquire(1, TimeUnit.SECONDS)) { System.out.println(new Date().toString() + " LockA 锁住 obj1"); if (UnLockTest.a2.tryAcquire(1, TimeUnit.SECONDS)) { System.out.println(new Date().toString() + " LockA 锁住 obj2"); Thread.sleep(60 * 1000); // do something }else{ System.out.println(new Date().toString() + "LockA 锁 obj2 失败"); } }else{ System.out.println(new Date().toString() + "LockA 锁 obj1 失败"); } UnLockTest.a1.release(); // 释放 UnLockTest.a2.release(); Thread.sleep(1000); // 马上进行尝试,现实情况下do something是不确定的 } } catch (Exception e) { e.printStackTrace(); } } } class LockBb implements Runnable { public void run() { try { System.out.println(new Date().toString() + " LockB 开始执行"); while (true) { if (UnLockTest.a2.tryAcquire(1, TimeUnit.SECONDS)) { System.out.println(new Date().toString() + " LockB 锁住 obj2"); if (UnLockTest.a1.tryAcquire(1, TimeUnit.SECONDS)) { System.out.println(new Date().toString() + " LockB 锁住 obj1"); Thread.sleep(60 * 1000); // do something }else{ System.out.println(new Date().toString() + "LockB 锁 obj1 失败"); } }else{ System.out.println(new Date().toString() + "LockB 锁 obj2 失败"); } UnLockTest.a1.release(); // 释放 UnLockTest.a2.release(); Thread.sleep(10 * 1000); // 这里只是为了演示,所以tryAcquire只用1秒,而且B要给A让出能执行的时间,否则两个永远是死锁 } } catch (Exception e) { e.printStackTrace(); } } } 以上实例代码输出结构为: Tue May 05 10:59:13 CST 2015 LockA 开始执行 Tue May 05 10:59:13 CST 2015 LockB 开始执行 Tue May 05 10:59:13 CST 2015 LockB 锁住 obj2 Tue May 05 10:59:13 CST 2015 LockA 锁住 obj1 Tue May 05 10:59:14 CST 2015LockB 锁 obj1 失败 Tue May 05 10:59:14 CST 2015LockA 锁 obj2 失败 Tue May 05 10:59:15 CST 2015 LockA 锁住 obj1 Tue May 05 10:59:15 CST 2015 LockA 锁住 obj2

问问小秘 2020-02-13 17:54:01 0 浏览量 回答数 0

问题

MaxCompute百问集锦(持续更新20171011)

隐林 2019-12-01 20:19:23 38430 浏览量 回答数 18

回答

追加:目测代码逻辑完全没错,只是K值的问题,long类型的K值因为不断的相乘,超过long的上限值,恭喜,在某一次相乘的时候,k值duang一下变成0了,所以,换一种方法吧,题主可以自己断点测试一下,最后k是变成0的######回复 @月生无界 : 客气客气######回复 @月影南溪 : 感谢提出######数据溢出不会变0的###### 月生无界正确地说出了long 型的取值范围。特此,我将从前写的代码展示如下, 来表明JAVA不同类型的变量的取值范围: public class Limits{        public static void main(String args[]){ /* 打印六种数字基本类型变量的最大值和最小值 */   System.out.println("长型最大值 LONG_Max: " + Long.MAX_VALUE); System.out.println("长型最小值 LONG_Min: " + Long.MIN_VALUE); System.out.println("整型最大值 Int_Max: " + Integer.MAX_VALUE); System.out.println("整型最小值 Int_Min: " + Integer.MIN_VALUE); System.out.println("短型最大值 SHORT_Max: " + Short.MAX_VALUE); System.out.println("短型最小值 SHORT_Min: " + Short.MIN_VALUE); System.out.println("字节型最大值 BYTE_Max: " + Byte.MAX_VALUE); System.out.println("字节型最小值 BYTE_Min: " + Byte.MIN_VALUE); //System.out.println("浮点型最大值 FLOAT_Max: " + Float.MAX_VALUE); //System.out.println("浮点型最小值 FLOAT_Min: " + Float.MIN_VALUE); //System.out.println("双精度型最大值 DOUBLE_Max: " + Double.MAX_VALUE); //System.out.println("双精度型最小值 DOUBLE_Min: " + Double.MIN_VALUE);        } } 输出:   长型最大值 LONG_Max: 9223372036854775807 长型最小值 LONG_Min: -9223372036854775808 整型最大值 Int_Max: 2147483647 整型最小值 Int_Min: -2147483648 短型最大值 SHORT_Max: 32767 短型最小值 SHORT_Min: -32768 字节型最大值 BYTE_Max: 127 字节型最小值 BYTE_Min: -128 ..........   就拿计算阶乘为例,以下代码,可以检查JAVA 输出数据的有效性。 public class Factoria { public static void main(String args[]) { //主方法代码块开始 int iFactoria=1;    //用整型存储阶乘 long lFactoria=1; //用长型存储阶乘 for (int i=1; i<17;i++){ //用for循环语句输出1到16的阶乘    iFactoria *=i;  //将i的阶乘存入整型变量    lFactoria *=i;  //将i的阶乘存入长型变量    /* 分别输出存于整型变量和长型变量的阶乘 */    System.out.printf(" %d 的阶乘:\t %10d(int), %15d(long)\n",        i, iFactoria, lFactoria);        }    System.out.printf("最大整型:%12d, 最大长型: %d\n",        Integer.MAX_VALUE,Long.MAX_VALUE);   }  //主方法 main 代码块结束结束 }  // 类 Factoria 定义结束   输出:   1 的阶乘:                1(int),               1(long)  2 的阶乘:                2(int),               2(long)  3 的阶乘:                6(int),               6(long)  4 的阶乘:               24(int),              24(long)  5 的阶乘:              120(int),             120(long)  6 的阶乘:              720(int),             720(long)  7 的阶乘:             5040(int),            5040(long)  8 的阶乘:            40320(int),           40320(long)  9 的阶乘:           362880(int),          362880(long)  10 的阶乘:         3628800(int),         3628800(long)  11 的阶乘:        39916800(int),        39916800(long)  12 的阶乘:       479001600(int),       479001600(long)  13 的阶乘:      1932053504(int),      6227020800(long)  14 的阶乘:      1278945280(int),     87178291200(long)  15 的阶乘:      2004310016(int),   1307674368000(long)  16 的阶乘:      2004189184(int),  20922789888000(long) 最大整型:  2147483647, 最大长型: 9223372036854775807   这里, *      阶乘指从1乘以2乘以3乘以4一直乘到所要求的数。N的阶乘可表示为n!=1×2×3×……×n 或 n!=n×(n-1)! *      用整型(int), 13的阶乘是: 6227020800,超过了 整型变量 int可以表示的最大正整数: 2147483647。 因此,13 或更大的阶乘数据,不能用整型int 表示。以上输出结果表明,用整型int变量存储的阶乘数据,若阶数超过12, 均不正确。 *      以上用长型变量输出的阶乘,尚且是正确的。但,它也有个限度, 17以上的阶乘, 就是“垃圾”了。 *     数学家定义,0!=1,所以0!=1! ######算法有问题,怎么判断素数,这个数学问题先搞清楚,然后再写程序,要不然全是乱的###### package test; public class Test { public static void main(String[] args) { String num = "素数:"; for(int i=2;i<=1000;i++){ //特殊值处理 if(i == 2){ num += i+","; //System.out.println("素数:"+i); }else{ //素数判断条件,从2开始除,取余,如果余值为0,表示不是素数,跳出这个数的循环判断, for(int j=2;j<i;j++){ if(i%j == 0){ break; } //判断是否是素数,能除到比该值小一,且余数不为0,肯定是素数 if(i%j != 0 && j == i-1){ num += i+","; //System.out.println("素数:"+i); } } } } System.out.println(num); } } //好人都是直接贴代码的 ###### 埃拉托色尼筛选法(Sieve of Eratosthenes) 也可以尝试。 import java.util.*; public class Eratosthenes{ // 埃拉托色尼筛选法 public static void main(String args[]){ int i,j; boolean b[]=new boolean[50]; for(i=0;i<b.length;i++) b[i]=true; //将数组的元素全部赋以true for ( i = 2; i < b.length; i++ ) // 从下标2开始递增循环 if ( b[ i ] ==true){// 每次找到值为true的元素 for (j =i+1;j < b.length;j++ ){ /* 就用其下标作为除数,去除往后余下的元素的下标*/ if (j%i == 0 ) //一旦能除尽 b[j] = false;// 将对应的元素值改为false } } for (i=2;i<b.length;i++ )//从2起,打印50以内的质数 if (b[ i ]) //若元素值为true System.out.printf("%4d", i);// 打印出该元素的下标 } } ###### 我已经将 tcxu 和 月生无界 所出示的代码,翻译成 PHP, 运行结果证明两种算法有效。 http://www.oschina.net/code/snippet_2756874_56652 ###### 查看楼主的代码发现, 你应当把 7 行的右花括号”}“,移到17行后边。这样,你的意向就对了: 从 第 9 行 至 第 17 行 处理 (k==0)的情况。从 18行 至 21 行,处理的是 (k != 0 的情况) ######k*(i%j)数值过大溢出了。 for(long j=2;j<i;j++){ k=i%j if(k==0) System.out.print(" "+i+"不是素数,有约数:"); break } ###### 不明白 ”k*(i%j)数值过大溢出了” 的情况 是什么情况? 指的是 这里的数值过大? 超过了 long型所能存储的最大数值 (2的63次方减 1)? 这里的数值并不大呀。 我这里没有安装Java环境,所以,参照楼主的代码,写出java脚本 代码,JavaScript 如下: 测试证明,楼主确实应当把 11 行 的 右花括号 ’ } ‘,移到 17 行:System.out.println(); 的后面。 <html> <head> <meta charset="gb2312"> <title>求1000以内的素数</title> <style> </style> </head> <body> <script>  var n=1; for (var i=1; i<1000;i++){ var k=1; for (var j=2;j<i;j++){ k=k*(i%j); } if (k==0){ //处理 不是素数的情况 document.write( i + " 不是素数,有约数: "); for (var j=2;j<i;j++) if (i%j==0){ document.write( j + " "); } document.write("<br>"); } else if (k !=0){ //处理素数的情况 document.write( "第 " + n +  " 个素数是:" + i + "<br>"); n++; } } </script> </body> </html> ######long取值范围:-9223372036854775808 -到9223372036854775807,明天再测试一下,long的最大值再乘其他数在代码中是否会变成0返回###### 引用来自“tcxu”的评论 月生无界正确地说出了long 型的取值范围。特此,我将从前写的代码展示如下, 来表明JAVA不同类型的变量的取值范围: public class Limits{        public static void main(String args[]){ /* 打印六种数字基本类型变量的最大值和最小值 */   System.out.println("长型最大值 LONG_Max: " + Long.MAX_VALUE); System.out.println("长型最小值 LONG_Min: " + Long.MIN_VALUE); System.out.println("整型最大值 Int_Max: " + Integer.MAX_VALUE); System.out.println("整型最小值 Int_Min: " + Integer.MIN_VALUE); System.out.println("短型最大值 SHORT_Max: " + Short.MAX_VALUE); System.out.println("短型最小值 SHORT_Min: " + Short.MIN_VALUE); System.out.println("字节型最大值 BYTE_Max: " + Byte.MAX_VALUE); System.out.println("字节型最小值 BYTE_Min: " + Byte.MIN_VALUE); //System.out.println("浮点型最大值 FLOAT_Max: " + Float.MAX_VALUE); //System.out.println("浮点型最小值 FLOAT_Min: " + Float.MIN_VALUE); //System.out.println("双精度型最大值 DOUBLE_Max: " + Double.MAX_VALUE); //System.out.println("双精度型最小值 DOUBLE_Min: " + Double.MIN_VALUE);        } } 输出:   长型最大值 LONG_Max: 9223372036854775807 长型最小值 LONG_Min: -9223372036854775808 整型最大值 Int_Max: 2147483647 整型最小值 Int_Min: -2147483648 短型最大值 SHORT_Max: 32767 短型最小值 SHORT_Min: -32768 字节型最大值 BYTE_Max: 127 字节型最小值 BYTE_Min: -128 ..........   就拿计算阶乘为例,以下代码,可以检查JAVA 输出数据的有效性。 public class Factoria { public static void main(String args[]) { //主方法代码块开始 int iFactoria=1;    //用整型存储阶乘 long lFactoria=1; //用长型存储阶乘 for (int i=1; i<17;i++){ //用for循环语句输出1到16的阶乘    iFactoria *=i;  //将i的阶乘存入整型变量    lFactoria *=i;  //将i的阶乘存入长型变量    /* 分别输出存于整型变量和长型变量的阶乘 */    System.out.printf(" %d 的阶乘:\t %10d(int), %15d(long)\n",        i, iFactoria, lFactoria);        }    System.out.printf("最大整型:%12d, 最大长型: %d\n",        Integer.MAX_VALUE,Long.MAX_VALUE);   }  //主方法 main 代码块结束结束 }  // 类 Factoria 定义结束   输出:   1 的阶乘:                1(int),               1(long)  2 的阶乘:                2(int),               2(long)  3 的阶乘:                6(int),               6(long)  4 的阶乘:               24(int),              24(long)  5 的阶乘:              120(int),             120(long)  6 的阶乘:              720(int),             720(long)  7 的阶乘:             5040(int),            5040(long)  8 的阶乘:            40320(int),           40320(long)  9 的阶乘:           362880(int),          362880(long)  10 的阶乘:         3628800(int),         3628800(long)  11 的阶乘:        39916800(int),        39916800(long)  12 的阶乘:       479001600(int),       479001600(long)  13 的阶乘:      1932053504(int),      6227020800(long)  14 的阶乘:      1278945280(int),     87178291200(long)  15 的阶乘:      2004310016(int),   1307674368000(long)  16 的阶乘:      2004189184(int),  20922789888000(long) 最大整型:  2147483647, 最大长型: 9223372036854775807   这里, *      阶乘指从1乘以2乘以3乘以4一直乘到所要求的数。N的阶乘可表示为n!=1×2×3×……×n 或 n!=n×(n-1)! *      用整型(int), 13的阶乘是: 6227020800,超过了 整型变量 int可以表示的最大正整数: 2147483647。 因此,13 或更大的阶乘数据,不能用整型int 表示。以上输出结果表明,用整型int变量存储的阶乘数据,若阶数超过12, 均不正确。 *      以上用长型变量输出的阶乘,尚且是正确的。但,它也有个限度, 17以上的阶乘, 就是“垃圾”了。 *     数学家定义,0!=1,所以0!=1! 真有耐心,我只做了一个简单的测试,发现一些有趣的事情,希望得到正确的解答 上代码 long min = -9223372036854775808L; long max = 9223372036854775807L; System.out.println(min*1+","+min*2+","+min*3+","+min*4); System.out.println(max*1+","+max*2+","+max*3+","+max*4); 结果:-9223372036854775808,0,-9223372036854775808,0 9223372036854775807,-2,9223372036854775805,-4 long的最小最大值从1乘到4,会出现各种结果,不是很懂其中的原理

爱吃鱼的程序员 2020-06-03 16:40:47 0 浏览量 回答数 0

回答

遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么? 遍历方式有以下几种: for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。 foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。如果没有实现该接口,表示不支持 Random Access,如LinkedList。 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。 说一下 ArrayList 的优缺点 ArrayList的优点如下: ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。ArrayList 在顺序添加一个元素的时候非常方便。 ArrayList 的缺点如下: 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。插入元素的时候,也需要做一次元素复制操作,缺点同上。 ArrayList 比较适合顺序添加、随机访问的场景。 如何实现数组和 List 之间的转换? 数组转 List:使用 Arrays. asList(array) 进行转换。List 转数组:使用 List 自带的 toArray() 方法。 代码示例: ArrayList 和 LinkedList 的区别是什么? 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。 补充:数据结构基础之双向链表 双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。 ArrayList 和 Vector 的区别是什么? 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。性能:ArrayList 在性能方面要优于 Vector。扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。 Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。 Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? ArrayList、LinkedList、Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。 Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。 LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。 多线程场景下如何使用 ArrayList? ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样: 为什么 ArrayList 的 elementData 加上 transient 修饰? ArrayList 中的数组定义如下: private transient Object[] elementData; 再看一下 ArrayList 的定义: public class ArrayList extends AbstractList implements List<E>, RandomAccess, Cloneable, java.io.Serializable 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现: 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。 List 和 Set 的区别 List , Set 都是继承自Collection 接口 List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。 Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。 另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。 Set和List对比 Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。 List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变 Set接口 说一下 HashSet 的实现原理? HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。 HashSet如何检查重复?HashSet是如何保证数据不可重复的? 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。 HashSet 中的add ()方法会使用HashMap 的put()方法。 HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。 以下是HashSet 部分源码: hashCode()与equals()的相关规定: 如果两个对象相等,则hashcode一定也是相同的 两个对象相等,对两个equals方法返回true 两个对象有相同的hashcode值,它们也不一定是相等的 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖 hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。 ** ==与equals的区别** ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同 ==是指对内存地址进行比较 equals()是对字符串的内容进行比较3.==指引用是否相同 equals()指的是值是否相同 HashSet与HashMap的区别 Queue BlockingQueue是什么? Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。 在 Queue 中 poll()和 remove()有什么区别? 相同点:都是返回第一个元素,并在队列中删除返回的对象。 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。 代码示例: Queue queue = new LinkedList (); queue. offer("string"); // add System. out. println(queue. poll()); System. out. println(queue. remove()); System. out. println(queue. size()); Map接口 说一下 HashMap 的实现原理? HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 HashMap 基于 Hash 算法实现的 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn) HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。 JDK1.8之前 JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 JDK1.8之后 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 JDK1.7 VS JDK1.8 比较 JDK1.8主要解决或优化了一下问题: resize 扩容优化引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 HashMap的put方法的具体流程? 当我们put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。 putVal方法执行流程图 ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤; ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。 HashMap的扩容操作是怎么实现的? ①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容; ②.每次扩展的时候,都是扩展2倍; ③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上 HashMap是怎么解决哈希冲突的? 答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行; 什么是哈希? Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。 什么是哈希冲突? 当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。 HashMap的数据结构 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突: 这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化 hash()函数 上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下: static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或) } 这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动); JDK1.8新增红黑树 通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn); 总结 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的: 使用链地址法(使用散列表)来链接拥有相同hash值的数据;使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;引入红黑树进一步降低遍历的时间复杂度,使得遍历更快; **能否使用任何类作为 Map 的 key? **可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点: 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。 为什么HashMap中String、Integer这样的包装类适合作为K? 答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况; 如果使用Object作为HashMap的Key,应该怎么办呢? 答:重写hashCode()和equals()方法 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞; 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性; HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标 答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置; 那怎么解决呢? HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均; 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题; HashMap 的长度为什么是2的幂次方 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 这个算法应该如何设计呢? 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 那为什么是两次扰动呢? 答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的; HashMap 与 HashTable 有什么区别? 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它; 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。 **初始容量大小和每次扩充容量大小的不同 **: ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 如何决定使用 HashMap 还是 TreeMap? 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。 HashMap 和 ConcurrentHashMap 的区别 ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。) HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。 ConcurrentHashMap 和 Hashtable 的区别? ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 两者的对比图: HashTable: JDK1.7的ConcurrentHashMap: JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点): 答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。 ConcurrentHashMap 底层具体实现知道吗?实现原理是什么? JDK1.7 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下: 一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。 JDK1.8 在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。 结构如下: 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount; 辅助工具类 Array 和 ArrayList 有何区别? Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。 对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。 如何实现 Array 和 List 之间的转换? Array 转 List: Arrays. asList(array) ;List 转 Array:List 的 toArray() 方法。 comparable 和 comparator的区别? comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort(). 方法如何比较元素? TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。 Collections 工具类的 sort 方法有两种重载的形式, 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

剑曼红尘 2020-03-24 14:41:57 0 浏览量 回答数 0

问题

十大经典排序算法最强总结(内含代码实现)

游客pklijor6gytpx 2020-01-09 14:44:55 1240 浏览量 回答数 2

问题

用位运算来解下八皇后问题 6月11日 【今日算法】

游客ih62co2qqq5ww 2020-06-15 16:24:16 2 浏览量 回答数 1

回答

重试作用: 对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。 远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。 比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。 优雅的重试机制要具备几点: 无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用 优雅重试共性和原理: 正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介。 约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。 都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。 Spring-tryer和guava-tryer工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。 优雅重试适用场景: 功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。 对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。 对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。 优雅重试解决思路: 切面方式 这个思路比较清晰,在需要添加重试的方法上添加一个用于重试的自定义注解,然后在切面中实现重试的逻辑,主要的配置参数则根据注解中的选项来初始化 优点: 真正的无侵入 缺点: 某些方法无法被切面拦截的场景无法覆盖(如spring-aop无法切私有方法,final方法) 直接使用aspecj则有些小复杂;如果用spring-aop,则只能切被spring容器管理的bean 消息总线方式 这个也比较容易理解,在需要重试的方法中,发送一个消息,并将业务逻辑作为回调方法传入;由一个订阅了重试消息的consumer来执行重试的业务逻辑 优点: 重试机制不受任何限制,即在任何地方你都可以使用 利用EventBus框架,可以非常容易把框架搭起来 缺点: 业务侵入,需要在重试的业务处,主动发起一条重试消息 调试理解复杂(消息总线方式的最大优点和缺点,就是过于灵活了,你可能都不知道什么地方处理这个消息,特别是新的童鞋来维护这段代码时) 如果要获取返回结果,不太好处理, 上下文参数不好处理 模板方式 优点: 简单(依赖简单:引入一个类就可以了; 使用简单:实现抽象类,讲业务逻辑填充即可;) 灵活(这个是真正的灵活了,你想怎么干都可以,完全由你控制) 缺点: 强侵入 代码臃肿 把这个单独捞出来,主要是某些时候我就一两个地方要用到重试,简单的实现下就好了,也没有必用用到上面这么重的方式;而且我希望可以针对代码快进行重试 这个的设计还是非常简单的,基本上代码都可以直接贴出来,一目了然: 复制代码 public abstract class RetryTemplate { private static final int DEFAULT_RETRY_TIME = 1; private int retryTime = DEFAULT_RETRY_TIME; private int sleepTime = 0;// 重试的睡眠时间 public int getSleepTime() { return sleepTime; } public RetryTemplate setSleepTime(int sleepTime) { if(sleepTime < 0) { throw new IllegalArgumentException("sleepTime should equal or bigger than 0"); } this.sleepTime = sleepTime; return this; } public int getRetryTime() { return retryTime; } public RetryTemplate setRetryTime(int retryTime) { if (retryTime <= 0) { throw new IllegalArgumentException("retryTime should bigger than 0"); } this.retryTime = retryTime; return this; } /** * 重试的业务执行代码 * 失败时请抛出一个异常 * * todo 确定返回的封装类,根据返回结果的状态来判定是否需要重试 * * @return */ protected abstract Object doBiz() throws Exception; //预留一个doBiz方法由业务方来实现,在其中书写需要重试的业务代码,然后执行即可 public Object execute() throws InterruptedException { for (int i = 0; i < retryTime; i++) { try { return doBiz(); } catch (Exception e) { log.error("业务执行出现异常,e: {}", e); Thread.sleep(sleepTime); } } return null; } public Object submit(ExecutorService executorService) { if (executorService == null) { throw new IllegalArgumentException("please choose executorService!"); } return executorService.submit((Callable) () -> execute()); } } 复制代码 使用示例: 复制代码 public void retryDemo() throws InterruptedException { Object ans = new RetryTemplate() { @Override protected Object doBiz() throws Exception { int temp = (int) (Math.random() * 10); System.out.println(temp); if (temp > 3) { throw new Exception("generate value bigger then 3! need retry"); } return temp; } }.setRetryTime(10).setSleepTime(10).execute(); System.out.println(ans); } 复制代码 spring-retry Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。 在分布式系统中,为了保证数据分布式事务的强一致性,在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,就不方便了。 还有一种方式,是开发者自己编写重试机制,但是大多不够优雅。 缺陷 spring-retry 工具虽能优雅实现重试,但是存在两个不友好设计: 一个是重试实体限定为 Throwable 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体, 但 sping-retry框架必须强制转换为Throwable子类。 另一个是重试根源的断言对象使用的是 doWithRetry 的 Exception 异常实例,不符合正常内部断言的返回设计。 Spring Retry 提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,当抛出相关异常后执行重试, 如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。只读操作可以重试,幂等写操作可以重试,但是非幂等写操作不能重试,重试可能导致脏写,或产生重复数据。 @Recover 注解在使用时无法指定方法,如果一个类中多个重试方法,就会很麻烦。 spring-retry 结构 BackOff:补偿值,一般指失败后多久进行重试的延迟值。 Sleeper:暂停应用的工具,通常用来应用补偿值。 RetryState:重试状态,通常包含一个重试的键值。 RetryCallback:封装你需要重试的业务逻辑(上文中的doSth) RecoverCallback:封装了多次重试都失败后你需要执行的业务逻辑(上文中的doSthWhenStillFail) RetryContext:重试语境下的上下文,代表了能被重试动作使用的资源。可用于在多次Retry或者Retry 和Recover之间传递参数或状态(在多次doSth或者doSth与doSthWhenStillFail之间传递参数) RetryOperations: 定义了“重试”的模板(重试的API),要求传入RetryCallback,可选传入RecoveryCallback; RetryTemplate :RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy。 RetryListener:用来监控Retry的执行情况,并生成统计信息。 RetryPolicy:重试的策略或条件,可以简单的进行多次重试,可以是指定超时时间进行重试(上文中的someCondition),决定失败能否重试。 BackOffPolicy: 重试的回退策略,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器),当然这段时间可以是0,也可以是固定的,可以是随机的(参见tcp的拥塞控制算法中的回退策略)。回退策略在上文中体现为wait(); RetryPolicy提供了如下策略实现: NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试; AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环; SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略; TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试; CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate delegate:是真正判断是否重试的策略,当重试失败时,则执行熔断策略;应该配置基于次数的SimpleRetryPolicy或者基于超时的TimeoutRetryPolicy策略,且策略都是全局模式,而非局部模式,所以要注意次数或超时的配置合理性。 openTimeout:openWindow,配置熔断器电路打开的超时时间,当超过openTimeout之后熔断器电路变成半打开状态(主要有一次重试成功,则闭合电路); resetTimeout:timeout,配置重置熔断器重新闭合的超时时间 CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行。 BackOffPolicy 提供了如下策略实现: NoBackOffPolicy:无退避算法策略,即当重试时是立即重试; FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper(指定等待策略,默认是Thread.sleep,即线程休眠)、backOffPeriod(休眠时间,默认1秒); UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod、maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒; ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier。initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier; ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数,固定乘数可能会引起很多服务同时重试导致DDos,使用随机休眠时间来避免这种情况。 RetryTemplate主要流程实现: 复制代码 //示例一 public void upload(final Map<String, Object> map) throws Exception { // 构建重试模板实例 RetryTemplate retryTemplate = new RetryTemplate(); // 设置重试策略,主要设置重试次数 SimpleRetryPolicy policy =         new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)); // 设置重试回退操作策略,主要设置重试间隔时间 FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy(); fixedBackOffPolicy.setBackOffPeriod(100); retryTemplate.setRetryPolicy(policy); retryTemplate.setBackOffPolicy(fixedBackOffPolicy); // 通过RetryCallback 重试回调实例包装正常逻辑逻辑,第一次执行和重试执行执行的都是这段逻辑 final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, Exception>() { //RetryContext 重试操作上下文约定,统一spring-try包装 public Object doWithRetry(RetryContext context) throws Exception { System.out.println("do some thing"); Exception e = uploadToOdps(map); System.out.println(context.getRetryCount()); throw e;//这个点特别注意,重试的根源通过Exception返回 } }; // 通过RecoveryCallback 重试流程正常结束或者达到重试上限后的退出恢复操作实例 final RecoveryCallback recoveryCallback = new RecoveryCallback() { public Object recover(RetryContext context) throws Exception { System.out.println("do recory operation"); return null; } }; try { // 由retryTemplate 执行execute方法开始逻辑执行 retryTemplate.execute(retryCallback, recoveryCallback); } catch (Exception e) { e.printStackTrace(); } } //示例二 protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,RecoveryCallback recoveryCallback,   RetryState state) throws E, ExhaustedRetryException { //重试策略 RetryPolicy retryPolicy = this.retryPolicy; //退避策略 BackOffPolicy backOffPolicy = this.backOffPolicy; //重试上下文,当前重试次数等都记录在上下文中 RetryContext context = open(retryPolicy, state); try { //拦截器模式,执行RetryListener#open boolean running = doOpenInterceptors(retryCallback, context); //判断是否可以重试执行 while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { try {//执行RetryCallback回调 return retryCallback.doWithRetry(context); } catch (Throwable e) {//异常时,要进行下一次重试准备 //遇到异常后,注册该异常的失败次数 registerThrowable(retryPolicy, state, context, e); //执行RetryListener#onError doOnErrorInterceptors(retryCallback, context, e); //如果可以重试,执行退避算法,比如休眠一小段时间后再重试 if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) { backOffPolicy.backOff(backOffContext); } //state != null && state.rollbackFor(context.getLastThrowable()) //在有状态重试时,如果是需要执行回滚操作的异常,则立即抛出异常 if (shouldRethrow(retryPolicy, context, state)) { throw RetryTemplate. wrapIfNecessary(e); } } //如果是有状态重试,且有GLOBAL_STATE属性,则立即跳出重试终止;       //当抛出的异常是非需要执行回滚操作的异常时,才会执行到此处,CircuitBreakerRetryPolicy会在此跳出循环; if (state != null && context.hasAttribute(GLOBAL_STATE)) { break; } } //重试失败后,如果有RecoveryCallback,则执行此回调,否则抛出异常 return handleRetryExhausted(recoveryCallback, context, state); } catch (Throwable e) { throw RetryTemplate. wrapIfNecessary(e); } finally { //清理环境 close(retryPolicy, context, state, lastException == null || exhausted); //执行RetryListener#close,比如统计重试信息 doCloseInterceptors(retryCallback, context, lastException); } } 复制代码 有状态or无状态 无状态重试,是在一个循环中执行完重试策略,即重试上下文保持在一个线程上下文中,在一次调用中进行完整的重试策略判断。如远程调用某个查询方法时是最常见的无状态重试: 复制代码 RetryTemplate template = new RetryTemplate(); //重试策略:次数重试策略 RetryPolicy retryPolicy = new SimpleRetryPolicy(3); template.setRetryPolicy(retryPolicy); //退避策略:指数退避策略 ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(100); backOffPolicy.setMaxInterval(3000); backOffPolicy.setMultiplier(2); backOffPolicy.setSleeper(new ThreadWaitSleeper()); template.setBackOffPolicy(backOffPolicy); //当重试失败后,抛出异常 String result = template.execute(new RetryCallback<String, RuntimeException>() { @Override public String doWithRetry(RetryContext context) throws RuntimeException { throw new RuntimeException("timeout"); } }); //当重试失败后,执行RecoveryCallback String result = template.execute(new RetryCallback<String, RuntimeException>() { @Override public String doWithRetry(RetryContext context) throws RuntimeException { System.out.println("retry count:" + context.getRetryCount()); throw new RuntimeException("timeout"); } }, new RecoveryCallback () { @Override public String recover(RetryContext context) throws Exception { return "default"; } }); 复制代码 有状态重试,有两种情况需要使用有状态重试,事务操作需要回滚、熔断器模式。 事务操作需要回滚场景时,当整个操作中抛出的是数据库异常DataAccessException,则不能进行重试需要回滚,而抛出其他异常则可以进行重试,可以通过RetryState实现: 复制代码 //当前状态的名称,当把状态放入缓存时,通过该key查询获取 Object key = "mykey"; //是否每次都重新生成上下文还是从缓存中查询,即全局模式(如熔断器策略时从缓存中查询) boolean isForceRefresh = true; //对DataAccessException进行回滚 BinaryExceptionClassifier rollbackClassifier = new BinaryExceptionClassifier(Collections.<Class<? extends Throwable>>singleton(DataAccessException.class)); RetryState state = new DefaultRetryState(key, isForceRefresh, rollbackClassifier); String result = template.execute(new RetryCallback<String, RuntimeException>() { @Override public String doWithRetry(RetryContext context) throws RuntimeException { System.out.println("retry count:" + context.getRetryCount()); throw new TypeMismatchDataAccessException(""); } }, new RecoveryCallback () { @Override public String recover(RetryContext context) throws Exception { return "default"; } }, state); 复制代码 RetryTemplate中在有状态重试时,回滚场景时直接抛出异常处理代码: //state != null && state.rollbackFor(context.getLastThrowable()) //在有状态重试时,如果是需要执行回滚操作的异常,则立即抛出异常 if (shouldRethrow(retryPolicy,context, state)) { throw RetryTemplate. wrapIfNecessary(e); } 熔断器场景。在有状态重试时,且是全局模式,不在当前循环中处理重试,而是全局重试模式(不是线程上下文),如熔断器策略时测试代码如下所示。 复制代码 RetryTemplate template = new RetryTemplate(); CircuitBreakerRetryPolicy retryPolicy = new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(3)); retryPolicy.setOpenTimeout(5000); retryPolicy.setResetTimeout(20000); template.setRetryPolicy(retryPolicy); for (int i = 0; i < 10; i++) { try { Object key = "circuit"; boolean isForceRefresh = false; RetryState state = new DefaultRetryState(key, isForceRefresh); String result = template.execute(new RetryCallback<String, RuntimeException>() { @Override public String doWithRetry(RetryContext context) throws RuntimeException { System.out.println("retry count:" + context.getRetryCount()); throw new RuntimeException("timeout"); } }, new RecoveryCallback () { @Override public String recover(RetryContext context) throws Exception { return "default"; } }, state); System.out.println(result); } catch (Exception e) { System.out.println(e); } } 复制代码 为什么说是全局模式呢?我们配置了isForceRefresh为false,则在获取上下文时是根据key “circuit”从缓存中获取,从而拿到同一个上下文。 Object key = "circuit"; boolean isForceRefresh = false; RetryState state = new DefaultRetryState(key,isForceRefresh); 如下RetryTemplate代码说明在有状态模式下,不会在循环中进行重试。 if (state != null && context.hasAttribute(GLOBAL_STATE)) { break; } 判断熔断器电路是否打开的代码: 复制代码 public boolean isOpen() { long time = System.currentTimeMillis() - this.start; boolean retryable = this.policy.canRetry(this.context); if (!retryable) {//重试失败 //在重置熔断器超时后,熔断器器电路闭合,重置上下文 if (time > this.timeout) { this.context = createDelegateContext(policy, getParent()); this.start = System.currentTimeMillis(); retryable = this.policy.canRetry(this.context); } else if (time < this.openWindow) { //当在熔断器打开状态时,熔断器电路打开,立即熔断 if ((Boolean) getAttribute(CIRCUIT_OPEN) == false) { setAttribute(CIRCUIT_OPEN, true); } this.start = System.currentTimeMillis(); return true; } } else {//重试成功 //在熔断器电路半打开状态时,断路器电路闭合,重置上下文 if (time > this.openWindow) { this.start = System.currentTimeMillis(); this.context = createDelegateContext(policy, getParent()); } } setAttribute(CIRCUIT_OPEN, !retryable); return !retryable; } 复制代码 从如上代码可看出spring-retry的熔断策略相对简单: 当重试失败,且在熔断器打开时间窗口[0,openWindow) 内,立即熔断; 当重试失败,且在指定超时时间后(>timeout),熔断器电路重新闭合; 在熔断器半打开状态[openWindow, timeout] 时,只要重试成功则重置上下文,断路器闭合。 注解介绍 @EnableRetry 表示是否开始重试。 序号 属性 类型 默认值 说明 1 proxyTargetClass boolean false 指示是否要创建基于子类的(CGLIB)代理,而不是创建标准的基于Java接口的代理。当proxyTargetClass属性为true时,使用CGLIB代理。默认使用标准JAVA注解 @Retryable 标注此注解的方法在发生异常时会进行重试 序号 属性 类型 默认值 说明 1 interceptor String ”” 将 interceptor 的 bean 名称应用到 retryable() 2 value class[] {} 可重试的异常类型 3 include class[] {} 和value一样,默认空,当exclude也为空时,所有异常都重试 4 exclude class[] {} 指定异常不重试,默认空,当include也为空时,所有异常都重试 5 label String ”” 统计报告的唯一标签。如果没有提供,调用者可以选择忽略它,或者提供默认值。 6 maxAttempts int 3 尝试的最大次数(包括第一次失败),默认为3次。 7 backoff @Backoff @Backoff() 重试补偿机制,指定用于重试此操作的backoff属性。默认为空 @Backoff 不设置参数时,默认使用FixedBackOffPolicy(指定等待时间),重试等待1000ms 序号 属性 类型 默认值 说明 1 delay long 0 指定延迟后重试 ,如果不设置则默认使用 1000 milliseconds 2 maxDelay long 0 最大重试等待时间 3 multiplier long 0 指定延迟的倍数,比如delay=5000l,multiplier=2时,第一次重试为5秒后,第二次为10秒,第三次为20秒(大于0生效) 4 random boolean false 随机重试等待时间 @Recover 用于恢复处理程序的方法调用的注释。返回类型必须与@retryable方法匹配。 可抛出的第一个参数是可选的(但是没有它的方法只会被调用)。 从失败方法的参数列表按顺序填充后续的参数。 用于@Retryable重试失败后处理方法,此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。 说明: 使用了@Retryable的方法不能在本类被调用,不然重试机制不会生效。也就是要标记为@Service,然后在其它类使用@Autowired注入或者@Bean去实例才能生效。 要触发@Recover方法,那么在@Retryable方法上不能有返回值,只能是void才能生效。 使用了@Retryable的方法里面不能使用try...catch包裹,要在发放上抛出异常,不然不会触发。 在重试期间这个方法是同步的,如果使用类似Spring Cloud这种框架的熔断机制时,可以结合重试机制来重试后返回结果。 Spring Retry不只能注入方式去实现,还可以通过API的方式实现,类似熔断处理的机制就基于API方式实现会比较宽松。 转载于:https://www.cnblogs.com/whatarewords/p/10656514.html

养狐狸的猫 2019-12-02 02:11:54 0 浏览量 回答数 0

回答

简介 如果您听说过 Node,或者阅读过一些文章,宣称 Node 是多么多么的棒,那么您可能会想:“Node 究竟是什么东西?”尽管不是针对所有人的,但 Node 可能是某些人的正确选择。 为试图解释什么是 Node.js,本文探究了它能解决的问题,它如何工作,如何运行一个简单应用程序,最后,Node 何时是和何时不是一个好的解决方案。本文不涉及如何编写一个复杂的 Node 应用程序,也不是一份全面的 Node 教程。阅读本文应该有助于您决定是否应该学习 Node,以便将其用于您的业务。 Node 旨在解决什么问题? Node 公开宣称的目标是 “旨在提供一种简单的构建可伸缩网络程序的方法”。当前的服务器程序有什么问题?我们来做个数学题。在 Java™ 和 PHP 这类语言中,每个连接都会生成一个新线程,每个新线程可能需要 2 MB 配套内存。在一个拥有 8 GB RAM 的系统上,理论上最大的并发连接数量是 4,000 个用户。随着您的客户端基础的增长,您希望您的 web 应用程序支持更多用户,这样,您必须添加更多服务器。当然,这会增加业务成本,尤其是服务器成本、运输成本和人工成本。除这些成本上升外,还有一个技术问题:用户可能针对每个请求使用不同的服务器,因此,任何共享资源都必须在所有服务器之间共享。例如,在 Java 中,静态变量和缓存需要在每个服务器上的 JVMs 之间共享。这就是整个 web 应用程序架构中的瓶颈:一个服务器能够处理的并发连接的最大数量。 Node 解决这个问题的方法是:更改连接连接到服务器的方式。每个连接都创建一个进程,该进程不需要配套内存块,而不是为每个连接生成一个新的 OS 线程(并向其分配一些配套内存)。Node 声称它绝不会死锁,因为它根本不允许使用锁,它不会直接阻塞 I/O 调用。Node 还宣称,运行它的服务器能支持数万个并发连接。事实上,Node 通过将整个系统中的瓶颈从最大连接数量更改到单个系统的流量来改变服务器面貌。 现在您有了一个能处理数万条并发连接的程序,那么您能通过 Node 实际构建什么呢?如果您有一个 web 应用程序需要处理这么多连接,那将是一件很 “恐怖” 的事!那是一种 “如果您有这个问题,那么它根本不是问题” 的问题。在回答上面的问题之前,我们先看看 Node 如何工作以及它被设计的如何运行。 Node 肯定不是什么 没错,Node 是一个服务器程序。但是,它肯定不 像 Apache 或 Tomcat。那些服务器是独立服务器产品,可以立即安装并部署应用程序。通过这些产品,您可以在一分钟内启动并运行一个服务器。Node 肯定不是这种产品。Apache 能添加一个 PHP 模块来允许开发人员创建动态 web 页,使用 Tomcat 的程序员能部署 JSPs 来创建动态 web 页。Node 肯定不是这种类型。 在 Node 的早期阶段(当前是 version 0.4.6),它还不是一个 “运行就绪” 的服务器程序,您还不能安装它,向其中放置文件,拥有一个功能齐全的 web 服务器。即使是要实现 web 服务器在安装完成后启动并运行这个基本功能,也还需要做大量工作。 Node 如何工作 Node 本身运行 V8 JavaScript。等等,服务器上的 JavaScript?没错,您没有看错。服务器端 JavaScript 是一个相对较新的概念,这个概念是大约两年前在 developerWorks 上讨论 Aptana Jaxer 产品时提到的(参见 参考资料)。尽管 Jaxer 一直没有真正流行,但这个理念本身并不是遥不可及的 — 为何不能在服务器上使用客户机上使用的编程语言? 什么使 V8?V8 JavaScript 引擎是 Google 用于他们的 Chrome 浏览器的底层 JavaScript 引擎。很少有人考虑 JavaScript 在客户机上实际做了些什么?实际上,JavaScript 引擎负责解释并执行代码。使用 V8,Google 创建了一个以 C++ 编写的超快解释器,该解释器拥有另一个独特特征;您可以下载该引擎并将其嵌入任何 应用程序。它不仅限于在一个浏览器中运行。因此,Node 实际上使用 Google 编写的 V8 JavaScript 引擎并将其重建为在服务器上使用。太完美了!既然已经有一个不错的解决方案可用,为何还要创建一种新语言呢? 事件驱动编程 许多程序员接受的教育使他们认为,面向对象编程是完美的编程设计,而对其他编程方法不屑一顾。Node 使用一个所谓的事件驱动编程模型。 清单 1. 客户端上使用 jQuery 的事件驱动编程 复制代码 代码如下: // jQuery code on the client-side showing how Event-Driven programming works // When a button is pressed, an Event occurs - deal with it // directly right here in an anonymous function, where all the // necessary variables are present and can be referenced directly $("#myButton").click(function(){ if ($("#myTextField").val() != $(this).val()) alert("Field must match button text"); }); 实际上,服务器端和客户端没有任何区别。没错,这没有按钮点击操作,也没有向文本字段键入的操作,但在一个更高的层面上,事件正在 发生。一个连接被建立 — 事件!数据通过连接接收 — 事件!数据通过连接停止 — 事件! 为什么这种设置类型对 Node 很理想?JavaScript 是一种很棒的事件驱动编程语言,因为它允许匿名函数和闭包,更重要的是,任何写过代码的人都熟悉它的语法。事件发生时调用的回调函数可以在捕获事件处编写。这样,代码容易编写和维护,没有复杂的面向对象框架,没有接口,没有在上面架构任何内容的潜能。只需监听事件,编写一个回调函数,然后,事件驱动编程将照管好一切! 示例 Node 应用程序 最后,我们来看一些代码!让我们将讨论过的所有内容综合起来,创建我们的第一个 Node 应用程序。由于我们已经知道,Node 对于处理高流量应用程序很理想,我们就来创建一个非常简单的 web 应用程序 — 一个为实现最大速度而构建的应用程序。下面是 “老板” 交代的关于我们的样例应用程序的具体要求:创建一个随机数字生成器 RESTful API。这个应用程序应该接受一个输入:一个名为 “number” 的参数。然后,应用程序返回一个介于 0 和该参数之间的随机数字,并将生成的数字返回调用者。由于 “老板” 希望它成为一个广泛流行的应用程序,因此它应该能处理 50,000 个并发用户。我们来看看代码: 清单 2. Node 随机数字生成器 复制代码 代码如下: // these modules need to be imported in order to use them. // Node has several modules. They are like any #include // or import statement in other languages var http = require("http"); var url = require("url"); // The most important line in any Node file. This function // does the actual process of creating the server. Technically, // Node tells the underlying operating system that whenever a // connection is made, this particular callback function should be // executed. Since we're creating a web service with REST API, // we want an HTTP server, which requires the http variable // we created in the lines above. // Finally, you can see that the callback method receives a 'request' // and 'response' object automatically. This should be familiar // to any PHP or Java programmer. http.createServer(function(request, response) { // The response needs to handle all the headers, and the return codes // These types of things are handled automatically in server programs // like Apache and Tomcat, but Node requires everything to be done yourself response.writeHead(200, {"Content-Type": "text/plain"}); // Here is some unique-looking code. This is how Node retrives // parameters passed in from client requests. The url module // handles all these functions. The parse function // deconstructs the URL, and places the query key-values in the // query object. We can find the value for the "number" key // by referencing it directly - the beauty of JavaScript. var params = url.parse(request.url, true).query; var input = params.number; // These are the generic JavaScript methods that will create // our random number that gets passed back to the caller var numInput = new Number(input); var numOutput = new Number(Math.random() * numInput).toFixed(0); // Write the random number to response response.write(numOutput); // Node requires us to explicitly end this connection. This is because // Node allows you to keep a connection open and pass data back and forth, // though that advanced topic isn't discussed in this article. response.end(); // When we create the server, we have to explicitly connect the HTTP server to // a port. Standard HTTP port is 80, so we'll connect it to that one. }).listen(80); // Output a String to the console once the server starts up, letting us know everything // starts up correctly console.log("Random Number Generator Running..."); 将上面的代码放到一个名为 “random.js” 的文件中。现在,要启动这个应用程序并运行它(进而创建 HTTP 服务器并监听端口 80 上的连接),只需在您的命令提示中输入以下命令:% node random.js。下面是服务器已经启动并运行时它看起来的样子: 复制代码 代码如下: root@ubuntu:/home/moila/ws/mike# node random.js Random Number Generator Running... 访问应用程序 应用程序已经启动并运行。Node 正在监听任何连接,我们来测试一下。由于我们创建了一个简单的 RESTful API,我们可以使用我们的 web 浏览器来访问这个应用程序。键入以下地址(确保您完成了上面的步骤):localhost/?number=27。 您的浏览器窗口将更改到一个介于 0 到 27 之间的随机数字。单击浏览器上的 “重新载入” 按钮,将得到另一个随机数字。就是这样,这就是您的第一个 Node 应用程序! Node 对什么有好处? 到此为止,应该能够回答 “Node 是什么” 这个问题了,但您可能还不清楚什么时候应该使用它。这是一个需要提出的重要问题,因为 Node 对有一些东西有好处,但相反,对另一些东西而言,目前 Node 可能不是一个好的解决方案。您需要小心决定何时使用 Node,因为在错误的情况下使用它可能会导致一个多余编码的 LOT。 它对什么有好处? 正如您此前所看到的,Node 非常适合以下情况:您预计可能有很高的流量,而在响应客户端之前服务器端逻辑和处理所需不一定是巨大的。Node 表现出众的典型示例包括: 1.RESTful API 提供 RESTful API 的 web 服务接收几个参数,解析它们,组合一个响应,并返回一个响应(通常是较少的文本)给用户。这是适合 Node 的理想情况,因为您可以构建它来处理数万条连接。它还不需要大量逻辑;它只是从一个数据库查找一些值并组合一个响应。由于响应是少量文本,入站请求时少量文本,因此流量不高,一台机器甚至也可以处理最繁忙的公司的 API 需求。 2.Twitter 队列 想像一下像 Twitter 这样的公司,它必须接收 tweets 并将其写入一个数据库。实际上,每秒几乎有数千条 tweets 达到,数据库不可能及时处理高峰时段需要的写入数量。Node 成为这个问题的解决方案的重要一环。如您所见,Node 能处理数万条入站 tweets。它能迅速轻松地将它们写入一个内存排队机制(例如 memcached),另一个单独进程可以从那里将它们写入数据库。Node 在这里的角色是迅速收集 tweet 并将这个信息传递给另一个负责写入的进程。想象一下另一种设计 — 一个常规 PHP 服务器自己试图处理对数据库的写入 — 每个 tweet 将在写入数据库时导致一个短暂的延迟,这是因为数据库调用正在阻塞通道。由于数据库延迟,一台这样设计的机器每秒可能只能处理 2000 条入站 tweets。每秒 100 万条 tweets 需要 500 个服务器。相反,Node 能处理每个连接而不会阻塞通道,从而能捕获尽可能多的 tweets。一个能处理 50,000 条 tweets 的 Node 机器只需要 20 个服务器。 3.映像文件服务器 一个拥有大型分布式网站的公司(比如 Facebook 或 Flickr)可能会决定将所有机器只用于服务映像。Node 将是这个问题的一个不错的解决方案,因为该公司能使用它编写一个简单的文件检索器,然后处理数万条连接。Node 将查找映像文件,返回文件或一个 404 错误,然后什么也不用做。这种设置将允许这类分布式网站减少它们服务映像、.js 和 .css 文件等静态文件所需的服务器数量。 它对什么有坏处? 当然,在某些情况下,Node 并非理想选择。下面是 Node 不擅长的领域: 1.动态创建的页 目前,Node 没有提供一种默认方法来创建动态页。例如,使用 JavaServer Pages (JSP) 技术时,可以创建一个在这样的 JSP 代码段中包含循环的 index.jsp 页。Node 不支持这类动态的、HTML 驱动的页面。同样,Node 不太适合作为 Apache 和 Tomcat 这样的网页服务器。因此,如果您想在 Node 中提供这样一个服务器端解决方案,必须自己编写整个解决方案。PHP 程序员不想在每次部署 web 应用程序时都编写一个针对 Apache 的 PHP 转换器,当目前为止,这正是 Node 要求您做的。 2. 关系数据库重型应用程序 Node 的目的是快速、异步和非阻塞。数据库并不一定分享这些目标。它们是同步和阻塞的,因为读写时对数据库的调用在结果生成之前将一直阻塞通道。因此,一个每个请求都需要大量数据库调用、大量读取、大量写入的 web 应用程序非常不适合 Node,这是因为关系数据库本身就能抵销 Node 的众多优势。(新的 NoSQL 数据库更适合 Node,不过那完全是另一个主题了。) 结束语 问题是 “什么是 Node.js?” 应该已经得到解答。阅读本文之后,您应该能通过几个清晰简洁的句子回答这个问题。如果这样,那么您已经走到了许多编码员和程序员的前面。我和许多人都谈论过 Node,但它们对 Node 究竟是什么一直很迷惑。可以理解,他们具有的是 Apache 的思维方式 — 服务器是一个应用程序,将 HTML 文件放入其中,一切就会正常运转。而 Node 是目的驱动的。它是一个软件程序,使用 JavaScript 来允许程序员轻松快速地创建快速、可伸缩的 web 服务器。Apache 是运行就绪的,而 Node 是编码就绪的。 Node 完成了它提供高度可伸缩服务器的目标。它并不分配一个 “每个连接一个线程” 模型,而是使用一个 “每个连接一个流程” 模型,只创建每个连接需要的内存。它使用 Google 的一个非常快速的 JavaScript 引擎:V8 引擎。它使用一个事件驱动设计来保持代码最小且易于阅读。所有这些因素促成了 Node 的理想目标 — 编写一个高度可伸缩的解决方案变得比较容易。 与理解 Node 是 什么同样重要的是,理解它不是 什么。Node 并不是 Apache 的一个替代品,后者旨在使 PHP web 应用程序更容易伸缩。事实确实如此。在 Node 的这个初始阶段,大量程序员使用它的可能性不大,但在它能发挥作用的场景中,它的表现非常好。 将来应该期望从 Node 得到什么呢?这也许是本文引出的最重要的问题。既然您知道了它现在的作用,您应该会想知道它下一步将做什么。在接下来的一年中,我期待着 Node 提供与现有的第三方支持库更好地集成。现在,许多第三方程序员已经研发了用于 Node 的插件,包括添加文件服务器支持和 MySQL 支持。希望 Node 开始将它们集成到其核心功能中。最后,我还希望 Node 支持某种动态页面模块,这样,您就可以在 HTML 文件中执行在 PHP 和 JSP(也许是一个 NSP,一个 Node 服务器页)中所做的操作。最后,希望有一天会出现一个 “部署就绪” 的 Node 服务器,可以下载和安装,只需将您的 HTML 文件放到其中,就像使用 Apache 或 Tomcat 那样。Node 现在还处于初始阶段,但它发展得很快,可能不久就会出现在您的视野中。 答案来源于网络

养狐狸的猫 2019-12-02 02:17:03 0 浏览量 回答数 0

问题

从一道面试题谈谈一线大厂码农应该具备的基本能力 7月16日 【今日算法】

游客ih62co2qqq5ww 2020-07-22 13:45:47 118 浏览量 回答数 1

问题

【javascript学习全家桶】934道javascript热门问题,阿里百位技术专家答疑解惑

管理贝贝 2019-12-01 20:07:22 6202 浏览量 回答数 1

问题

【python学习全家桶】263道python热门问题,阿里百位技术专家答疑解惑

管理贝贝 2019-12-01 20:07:21 7217 浏览量 回答数 2

回答

92题 一般来说,建立INDEX有以下益处:提高查询效率;建立唯一索引以保证数据的唯一性;设计INDEX避免排序。 缺点,INDEX的维护有以下开销:叶节点的‘分裂’消耗;INSERT、DELETE和UPDATE操作在INDEX上的维护开销;有存储要求;其他日常维护的消耗:对恢复的影响,重组的影响。 需要建立索引的情况:为了建立分区数据库的PATITION INDEX必须建立; 为了保证数据约束性需要而建立的INDEX必须建立; 为了提高查询效率,则考虑建立(是否建立要考虑相关性能及维护开销); 考虑在使用UNION,DISTINCT,GROUP BY,ORDER BY等字句的列上加索引。 91题 作用:加快查询速度。原则:(1) 如果某属性或属性组经常出现在查询条件中,考虑为该属性或属性组建立索引;(2) 如果某个属性常作为最大值和最小值等聚集函数的参数,考虑为该属性建立索引;(3) 如果某属性经常出现在连接操作的连接条件中,考虑为该属性或属性组建立索引。 90题 快照Snapshot是一个文件系统在特定时间里的镜像,对于在线实时数据备份非常有用。快照对于拥有不能停止的应用或具有常打开文件的文件系统的备份非常重要。对于只能提供一个非常短的备份时间而言,快照能保证系统的完整性。 89题 游标用于定位结果集的行,通过判断全局变量@@FETCH_STATUS可以判断是否到了最后,通常此变量不等于0表示出错或到了最后。 88题 事前触发器运行于触发事件发生之前,而事后触发器运行于触发事件发生之后。通常事前触发器可以获取事件之前和新的字段值。语句级触发器可以在语句执行前或后执行,而行级触发在触发器所影响的每一行触发一次。 87题 MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。具体原因为:MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。 86题 建立索引的时候一般要考虑到字段的使用频率,经常作为条件进行查询的字段比较适合。如果需要建立联合索引的话,还需要考虑联合索引中的顺序。此外也要考虑其他方面,比如防止过多的所有对表造成太大的压力。这些都和实际的表结构以及查询方式有关。 85题 存储过程是一组Transact-SQL语句,在一次编译后可以执行多次。因为不必重新编译Transact-SQL语句,所以执行存储过程可以提高性能。触发器是一种特殊类型的存储过程,不由用户直接调用。创建触发器时会对其进行定义,以便在对特定表或列作特定类型的数据修改时执行。 84题 存储过程是用户定义的一系列SQL语句的集合,涉及特定表或其它对象的任务,用户可以调用存储过程,而函数通常是数据库已定义的方法,它接收参数并返回某种类型的值并且不涉及特定用户表。 83题 减少表连接,减少复杂 SQL,拆分成简单SQL。减少排序:非必要不排序,利用索引排序,减少参与排序的记录数。尽量避免 select *。尽量用 join 代替子查询。尽量少使用 or,使用 in 或者 union(union all) 代替。尽量用 union all 代替 union。尽量早的将无用数据过滤:选择更优的索引,先分页再Join…。避免类型转换:索引失效。优先优化高并发的 SQL,而不是执行频率低某些“大”SQL。从全局出发优化,而不是片面调整。尽可能对每一条SQL进行 explain。 82题 如果条件中有or,即使其中有条件带索引也不会使用(要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引)。对于多列索引,不是使用的第一部分,则不会使用索引。like查询是以%开头。如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。如果mysql估计使用全表扫描要比使用索引快,则不使用索引。例如,使用<>、not in 、not exist,对于这三种情况大多数情况下认为结果集很大,MySQL就有可能不使用索引。 81题 主键不能重复,不能为空,唯一键不能重复,可以为空。建立主键的目的是让外键来引用。一个表最多只有一个主键,但可以有很多唯一键。 80题 空值('')是不占用空间的,判断空字符用=''或者<>''来进行处理。NULL值是未知的,且占用空间,不走索引;判断 NULL 用 IS NULL 或者 is not null ,SQL 语句函数中可以使用 ifnull ()函数来进行处理。无法比较 NULL 和 0;它们是不等价的。无法使用比较运算符来测试 NULL 值,比如 =, <, 或者 <>。NULL 值可以使用 <=> 符号进行比较,该符号与等号作用相似,但对NULL有意义。进行 count ()统计某列的记录数的时候,如果采用的 NULL 值,会被系统自动忽略掉,但是空值是统计到其中。 79题 HEAP表是访问数据速度最快的MySQL表,他使用保存在内存中的散列索引。一旦服务器重启,所有heap表数据丢失。BLOB或TEXT字段是不允许的。只能使用比较运算符=,<,>,=>,= <。HEAP表不支持AUTO_INCREMENT。索引不可为NULL。 78题 如果想输入字符为十六进制数字,可以输入带有单引号的十六进制数字和前缀(X),或者只用(Ox)前缀输入十六进制数字。如果表达式上下文是字符串,则十六进制数字串将自动转换为字符串。 77题 Mysql服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。 76题 在缺省模式下,MYSQL是autocommit模式的,所有的数据库更新操作都会即时提交,所以在缺省情况下,mysql是不支持事务的。但是如果你的MYSQL表类型是使用InnoDB Tables 或 BDB tables的话,你的MYSQL就可以使用事务处理,使用SET AUTOCOMMIT=0就可以使MYSQL允许在非autocommit模式,在非autocommit模式下,你必须使用COMMIT来提交你的更改,或者用ROLLBACK来回滚你的更改。 75题 它会停止递增,任何进一步的插入都将产生错误,因为密钥已被使用。 74题 创建索引的时候尽量使用唯一性大的列来创建索引,由于使用b+tree做为索引,以innodb为例,一个树节点的大小由“innodb_page_size”,为了减少树的高度,同时让一个节点能存放更多的值,索引列尽量在整数类型上创建,如果必须使用字符类型,也应该使用长度较少的字符类型。 73题 当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下: 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读。垂直分区: 根据数据库里面数据表的相关性进行拆分。简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。水平分区: 保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。水平拆分可以支撑非常大的数据量。 72题 乐观锁失败后会抛出ObjectOptimisticLockingFailureException,那么我们就针对这块考虑一下重试,自定义一个注解,用于做切面。针对注解进行切面,设置最大重试次数n,然后超过n次后就不再重试。 71题 一致性非锁定读讲的是一条记录被加了X锁其他事务仍然可以读而不被阻塞,是通过innodb的行多版本实现的,行多版本并不是实际存储多个版本记录而是通过undo实现(undo日志用来记录数据修改前的版本,回滚时会用到,用来保证事务的原子性)。一致性锁定读讲的是我可以通过SELECT语句显式地给一条记录加X锁从而保证特定应用场景下的数据一致性。 70题 数据库引擎:尤其是mysql数据库只有是InnoDB引擎的时候事物才能生效。 show engines 查看数据库默认引擎;SHOW TABLE STATUS from 数据库名字 where Name='表名' 如下;SHOW TABLE STATUS from rrz where Name='rrz_cust';修改表的引擎alter table table_name engine=innodb。 69题 如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值;当然了,这个前提是,键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到相应的数据;如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索;同理,哈希索引也没办法利用索引完成排序,以及like ‘xxx%’ 这样的部分模糊查询(这种部分模糊查询,其实本质上也是范围查询);哈希索引也不支持多列联合索引的最左匹配规则;B+树索引的关键字检索效率比较平均,不像B树那样波动幅度大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞问题。 68题 decimal精度比float高,数据处理比float简单,一般优先考虑,但float存储的数据范围大,所以范围大的数据就只能用它了,但要注意一些处理细节,因为不精确可能会与自己想的不一致,也常有关于float 出错的问题。 67题 datetime、timestamp精确度都是秒,datetime与时区无关,存储的范围广(1001-9999),timestamp与时区有关,存储的范围小(1970-2038)。 66题 Char使用固定长度的空间进行存储,char(4)存储4个字符,根据编码方式的不同占用不同的字节,gbk编码方式,不论是中文还是英文,每个字符占用2个字节的空间,utf8编码方式,每个字符占用3个字节的空间。Varchar保存可变长度的字符串,使用额外的一个或两个字节存储字符串长度,varchar(10),除了需要存储10个字符,还需要1个字节存储长度信息(10),超过255的长度需要2个字节来存储。char和varchar后面如果有空格,char会自动去掉空格后存储,varchar虽然不会去掉空格,但在进行字符串比较时,会去掉空格进行比较。Varbinary保存变长的字符串,后面不会补\0。 65题 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。 64题 建立索引的时候一般要考虑到字段的使用频率,经常作为条件进行查询的字段比较适合。如果需要建立联合索引的话,还需要考虑联合索引中的顺序。此外也要考虑其他方面,比如防止过多的所有对表造成太大的压力。这些都和实际的表结构以及查询方式有关。 63题 存储过程是一些预编译的SQL语句。1、更加直白的理解:存储过程可以说是一个记录集,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。2、存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量T_SQL语句 ,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全。 62题 密码散列、盐、用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样可以节省空间且提高检索效率。 61题 推荐使用自增ID,不要使用UUID。因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降。总之,在数据量大一些的情况下,用自增主键性能会好一些。 60题 char是一个定长字段,假如申请了char(10)的空间,那么无论实际存储多少内容。该字段都占用10个字符,而varchar是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用了多长的空间。在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用char,否则应该尽量使用varchar。例如存储用户MD5加密后的密码,则应该使用char。 59题 一. read uncommitted(读取未提交数据) 即便是事务没有commit,但是我们仍然能读到未提交的数据,这是所有隔离级别中最低的一种。 二. read committed(可以读取其他事务提交的数据)---大多数数据库默认的隔离级别 当前会话只能读取到其他事务提交的数据,未提交的数据读不到。 三. repeatable read(可重读)---MySQL默认的隔离级别 当前会话可以重复读,就是每次读取的结果集都相同,而不管其他事务有没有提交。 四. serializable(串行化) 其他会话对该表的写操作将被挂起。可以看到,这是隔离级别中最严格的,但是这样做势必对性能造成影响。所以在实际的选用上,我们要根据当前具体的情况选用合适的。 58题 B+树的高度一般为2-4层,所以查找记录时最多只需要2-4次IO,相对二叉平衡树已经大大降低了。范围查找时,能通过叶子节点的指针获取数据。例如查找大于等于3的数据,当在叶子节点中查到3时,通过3的尾指针便能获取所有数据,而不需要再像二叉树一样再获取到3的父节点。 57题 因为事务在修改页时,要先记 undo,在记 undo 之前要记 undo 的 redo, 然后修改数据页,再记数据页修改的 redo。 Redo(里面包括 undo 的修改) 一定要比数据页先持久化到磁盘。 当事务需要回滚时,因为有 undo,可以把数据页回滚到前镜像的状态,崩溃恢复时,如果 redo log 中事务没有对应的 commit 记录,那么需要用 undo把该事务的修改回滚到事务开始之前。 如果有 commit 记录,就用 redo 前滚到该事务完成时并提交掉。 56题 redo log是物理日志,记录的是"在某个数据页上做了什么修改"。 binlog是逻辑日志,记录的是这个语句的原始逻辑,比如"给ID=2这一行的c字段加1"。 redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。 redo log是循环写的,空间固定会用完:binlog 是可以追加写入的。"追加写"是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。 最开始 MySQL 里并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog日志只能用于归档。而InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统,也就是 redo log 来实现 crash-safe 能力。 55题 重做日志(redo log)      作用:确保事务的持久性,防止在发生故障,脏页未写入磁盘。重启数据库会进行redo log执行重做,达到事务一致性。 回滚日志(undo log)  作用:保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 二进 制日志(binlog)    作用:用于主从复制,实现主从同步;用于数据库的基于时间点的还原。 错误日志(errorlog) 作用:Mysql本身启动,停止,运行期间发生的错误信息。 慢查询日志(slow query log)  作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功。 一般查询日志(general log)    作用:记录数据库的操作明细,默认关闭,开启后会降低数据库性能 。 中继日志(relay log) 作用:用于数据库主从同步,将主库发来的bin log保存在本地,然后从库进行回放。 54题 MySQL有三种锁的级别:页级、表级、行级。 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 死锁: 是指两个或两个以上的进程在执行过程中。因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。 那么对应的解决死锁问题的关键就是:让不同的session加锁有次序。死锁的解决办法:1.查出的线程杀死。2.设置锁的超时时间。3.指定获取锁的顺序。 53题 当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性(脏读,不可重复读,幻读等),可能产生死锁。 乐观锁:乐观锁不是数据库自带的,需要我们自己去实现。 悲观锁:在进行每次操作时都要通过获取锁才能进行对相同数据的操作。 共享锁:加了共享锁的数据对象可以被其他事务读取,但不能修改。 排他锁:当数据对象被加上排它锁时,一个事务必须得到锁才能对该数据对象进行访问,一直到事务结束锁才被释放。 行锁:就是给某一条记录加上锁。 52题 Mysql是关系型数据库,MongoDB是非关系型数据库,数据存储结构的不同。 51题 关系型数据库优点:1.保持数据的一致性(事务处理)。 2.由于以标准化为前提,数据更新的开销很小。 3. 可以进行Join等复杂查询。 缺点:1、为了维护一致性所付出的巨大代价就是其读写性能比较差。 2、固定的表结构。 3、高并发读写需求。 4、海量数据的高效率读写。 非关系型数据库优点:1、无需经过sql层的解析,读写性能很高。 2、基于键值对,数据没有耦合性,容易扩展。 3、存储数据的格式:nosql的存储格式是key,value形式、文档形式、图片形式等等,文档形式、图片形式等等,而关系型数据库则只支持基础类型。 缺点:1、不提供sql支持,学习和使用成本较高。 2、无事务处理,附加功能bi和报表等支持也不好。 redis与mongoDB的区别: 性能:TPS方面redis要大于mongodb。 可操作性:mongodb支持丰富的数据表达,索引,redis较少的网络IO次数。 可用性:MongoDB优于Redis。 一致性:redis事务支持比较弱,mongoDB不支持事务。 数据分析:mongoDB内置了数据分析的功能(mapreduce)。 应用场景:redis数据量较小的更性能操作和运算上,MongoDB主要解决海量数据的访问效率问题。 50题 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。 49题 分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。 48题 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6种策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: 1.定时去清理过期的缓存; 2.当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,可以根据应用场景来权衡。 47题 Redis提供了两种方式来作消息队列: 一个是使用生产者消费模式模式:会让一个或者多个客户端监听消息队列,一旦消息到达,消费者马上消费,谁先抢到算谁的,如果队列里没有消息,则消费者继续监听 。另一个就是发布订阅者模式:也是一个或多个客户端订阅消息频道,只要发布者发布消息,所有订阅者都能收到消息,订阅者都是平等的。 46题 Redis的数据结构列表(list)可以实现延时队列,可以通过队列和栈来实现。blpop/brpop来替换lpop/rpop,blpop/brpop阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。Redis的有序集合(zset)可以用于实现延时队列,消息作为value,时间作为score。Zrem 命令用于移除有序集中的一个或多个成员,不存在的成员将被忽略。当 key 存在但不是有序集类型时,返回一个错误。 45题 1.热点数据缓存:因为Redis 访问速度块、支持的数据类型比较丰富。 2.限时业务:expire 命令设置 key 的生存时间,到时间后自动删除 key。 3.计数器:incrby 命令可以实现原子性的递增。 4.排行榜:借助 SortedSet 进行热点数据的排序。 5.分布式锁:利用 Redis 的 setnx 命令进行。 6.队列机制:有 list push 和 list pop 这样的命令。 44题 一致哈希 是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 K/n 个关键字重新映射,其中K是关键字的数量, n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。 43题 RDB的优点:适合做冷备份;读写服务影响小,reids可以保持高性能;重启和恢复redis进程,更加快速。RDB的缺点:宕机会丢失最近5分钟的数据;文件特别大时可能会暂停数毫秒,或者甚至数秒。 AOF的优点:每个一秒执行fsync操作,最多丢失1秒钟的数据;以append-only模式写入,没有任何磁盘寻址的开销;文件过大时,不会影响客户端读写;适合做灾难性的误删除的紧急恢复。AOF的缺点:AOF日志文件比RDB数据快照文件更大,支持写QPS比RDB支持的写QPS低;比RDB脆弱,容易有bug。 42题 对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。Redis的操作之所以是原子性的,是因为Redis是单线程的。而在程序中执行多个Redis命令并非是原子性的,这也和普通数据库的表现是一样的,可以用incr或者使用Redis的事务,或者使用Redis+Lua的方式实现。对Redis来说,执行get、set以及eval等API,都是一个一个的任务,这些任务都会由Redis的线程去负责执行,任务要么执行成功,要么执行失败,这就是Redis的命令是原子性的原因。 41题 (1)twemproxy,使用方式简单(相对redis只需修改连接端口),对旧项目扩展的首选。(2)codis,目前用的最多的集群方案,基本和twemproxy一致的效果,但它支持在节点数改变情况下,旧节点数据可恢复到新hash节点。(3)redis cluster3.0自带的集群,特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持节点设置从节点。(4)在业务代码层实现,起几个毫无关联的redis实例,在代码层,对key进行hash计算,然后去对应的redis实例操作数据。这种方式对hash层代码要求比较高,考虑部分包括,节点失效后的代替算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。 40题 (1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件 (2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次 (3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内 (4) 尽量避免在压力很大的主库上增加从库 (5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3...这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。 39题 比如订单管理,热数据:3个月内的订单数据,查询实时性较高;温数据:3个月 ~ 12个月前的订单数据,查询频率不高;冷数据:1年前的订单数据,几乎不会查询,只有偶尔的查询需求。热数据使用mysql进行存储,需要分库分表;温数据可以存储在ES中,利用搜索引擎的特性基本上也可以做到比较快的查询;冷数据可以存放到Hive中。从存储形式来说,一般情况冷数据存储在磁带、光盘,热数据一般存放在SSD中,存取速度快,而温数据可以存放在7200转的硬盘。 38题 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 37题 分层架构设计,有一条准则:站点层、服务层要做到无数据无状态,这样才能任意的加节点水平扩展,数据和状态尽量存储到后端的数据存储服务,例如数据库服务或者缓存服务。显然进程内缓存违背了这一原则。 36题 更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。 35题 redis分布式锁加锁过程:通过setnx向特定的key写入一个随机值,并同时设置失效时间,写值成功既加锁成功;redis分布式锁解锁过程:匹配随机值,删除redis上的特点key数据,要保证获取数据、判断一致以及删除数据三个操作是原子的,为保证原子性一般使用lua脚本实现;在此基础上进一步优化的话,考虑使用心跳检测对锁的有效期进行续期,同时基于redis的发布订阅优雅的实现阻塞式加锁。 34题 volatile-lru:当内存不足以容纳写入数据时,从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。 volatile-ttl:当内存不足以容纳写入数据时,从已设置过期时间的数据集中挑选将要过期的数据淘汰。 volatile-random:当内存不足以容纳写入数据时,从已设置过期时间的数据集中任意选择数据淘汰。 allkeys-lru:当内存不足以容纳写入数据时,从数据集中挑选最近最少使用的数据淘汰。 allkeys-random:当内存不足以容纳写入数据时,从数据集中任意选择数据淘汰。 noeviction:禁止驱逐数据,当内存使用达到阈值的时候,所有引起申请内存的命令会报错。 33题 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。 32题 缓存击穿,一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。如何避免:在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。 31题 缓存雪崩,是指在某一个时间段,缓存集中过期失效。大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。而缓存服务器某个节点宕机或断网,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。如何避免:1.redis高可用,搭建redis集群。2.限流降级,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。3.数据预热,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间。 30题 缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。一些恶意的请求会故意查询不存在的 key,请求量很大,对数据库造成压力,甚至压垮数据库。 如何避免:1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后清理缓存。2:对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 bitmap 过滤。 29题 1.memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类型。 2.redis 的速度比 memcached 快很多。 3.redis 可以持久化其数据。 4.Redis支持数据的备份,即master-slave模式的数据备份。 5.Redis采用VM机制。 6.value大小:redis最大可以达到1GB,而memcache只有1MB。 28题 Spring Boot 推荐使用 Java 配置而非 XML 配置,但是 Spring Boot 中也可以使用 XML 配置,通过spring提供的@ImportResource来加载xml配置。例如:@ImportResource({"classpath:some-context.xml","classpath:another-context.xml"}) 27题 Spring像一个大家族,有众多衍生产品例如Spring Boot,Spring Security等等,但他们的基础都是Spring的IOC和AOP,IOC提供了依赖注入的容器,而AOP解决了面向切面的编程,然后在此两者的基础上实现了其他衍生产品的高级功能。Spring MVC是基于Servlet的一个MVC框架,主要解决WEB开发的问题,因为 Spring的配置非常复杂,各种xml,properties处理起来比较繁琐。Spring Boot遵循约定优于配置,极大降低了Spring使用门槛,又有着Spring原本灵活强大的功能。总结:Spring MVC和Spring Boot都属于Spring,Spring MVC是基于Spring的一个MVC框架,而Spring Boot是基于Spring的一套快速开发整合包。 26题 YAML 是 "YAML Ain't a Markup Language"(YAML 不是一种标记语言)的递归缩写。YAML 的配置文件后缀为 .yml,是一种人类可读的数据序列化语言,可以简单表达清单、散列表,标量等数据形态。它通常用于配置文件,与属性文件相比,YAML文件就更加结构化,而且更少混淆。可以看出YAML具有分层配置数据。 25题 Spring Boot有3种热部署方式: 1.使用springloaded配置pom.xml文件,使用mvn spring-boot:run启动。 2.使用springloaded本地加载启动,配置jvm参数-javaagent:<jar包地址> -noverify。 3.使用devtools工具包,操作简单,但是每次需要重新部署。 用

游客ih62co2qqq5ww 2020-03-27 23:56:48 0 浏览量 回答数 0
阿里云大学 云服务器ECS com域名 网站域名whois查询 开发者平台 小程序定制 小程序开发 国内短信套餐包 开发者技术与产品 云数据库 图像识别 开发者问答 阿里云建站 阿里云备案 云市场 万网 阿里云帮助文档 免费套餐 开发者工具 SQL审核 小程序开发制作 视频内容分析 企业网站制作 视频集锦 代理记账服务 2020阿里巴巴研发效能峰会 企业建站模板 云效成长地图 高端建站 人工智能 阿里云云栖号 云栖号案例 云栖号直播