java基础编程题题

1、Java面向对象的三个特征与含义 

三大特征是:封装、继承和多态

    封装是指将某事物的属性和行为包装到对象中,这个对象只对外公布需要公开的属性和行为,而这个公布也是可以有选择性的公布给其它对象。在Java中能使用private、protected、public三种修饰符或不用(即默认defalut)对外部对象访问该对象的属性和行为进行限制。

    继承是子对象可以继承父对象的属性和行为,亦即父对象拥有的属性和行为,其子对象也就拥有了这些属性和行为。这非常类似大自然中的物种遗传。

多态不是很好解释:更倾向于使用java中的固定用法,即overriding(覆盖)和overload(过载)。多态则是体现在overriding(覆盖)上,而overload(过载)则不属于面向对象中多态的范畴,因为overload(过载)概念在非面向对象中也存在。overriding(覆盖)是面向对象中的多态,因为overriding(覆盖)是与继承紧密联系,是面向对象所特有的。多态是指父对象中的同一个行为能在其多个子对象中有不同的表现。也就是说子对象可以使用重写父对象中的行为,使其拥有不同于父对象和其它子对象的表现,这就是overriding(覆盖)。

在子类构造器中使用super()显示调用父类的构造方法,super()必须写在子类构造方法的第一行,否则编译不通过; 

属性:this属性表示找到本类的属性,如果本类没有找到则继续查找父类;

方法:this方法表示找到本类的方法,如果本类没有找到则继续查找父类;

构造:必须放在构造方法的首行,不能与super关键字同时出现;

属性:super属性直接在子类之中查找父类中的指定属性,不再查找子类本身属性;

方法:super方法直接在子类之中查找父类中的指定方法,不再查找子类本身方法;

构造:必须放在构造方法首行,不能与this关键字同时出现。

(1)调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。

(2)super从子类中调用父类的构造方法,this()在同一类内调用其它方法。

(3)super()和this()均需放在构造方法内第一行。

(4)尽管可以用this调用一个构造器,但却不能调用两个。

(5)this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。

(7)从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

1)public(公共的):表明该成员变量或方法对所有类或对象都是可见的,所有类或对象都可以直接访问;

2)protected(受保护的):表明成员变量或方法对该类本身&与它在同一个包中的其它类&在其它包中的该类的子类都可见;

3)default(默认的,不加任何访问修饰符):表明成员变量或方法只有自己&其位于同一个包内的类可见;

4)private(私有的):表明该成员变量或方法是私有的,只有当前类对其具有访问权限。

由大到小:public(接口访问权限)、protected(继承访问权限)、包访问权限(没有使用任何访问权限修饰词)、private(私有无法访问)。

protected表示就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,却是可以访问的(protected也提供了包内访问权限)。

(2)访问权限注意点:

1)类的访问权限,只能是包访问权限(默认无访问修饰符即可)或者public。若把一个类中的构造器指定为private,则不能访问该类,若要创建该类的对象,则需要在该类的static成员内部创建,如单例模式。

2)如果没能为类访问权限指定一个访问修饰符,默认得到包访问权限,则该类的对象可以由包内任何其他类创建,但是包外不可以。

3)访问权限的控制,也称为具体实现的隐藏。制定规则(如使用访问权限,设定成员所遵守的界限),是防止客户端程序员对类随心所欲而为。

(3)控制对成员的访问权限的两个原因:

使用户不要碰触那些不该碰触的部分,对类内部的操作是必要的,不属于客户端程序员所需接口的一部分;

让类库设计者可以更改类的内部工作方式,而不会对客户端程序员产生重大影响;访问权限控制可以确保不会有任何客户端程序员依赖于类的底层实现的任何部分。

(4)对某成员的访问权的唯一途径:

2)通过不加访问权限修饰词并将其他类放置在同一个包内的方式给成员赋予包访问权;

3)继承技术,访问protected成员;

4)提供访问器和变异器(get/set方法),以读取和改变数值。

(1)抽象类不能被实例化,实例化的工作应该交由它的子类来完成,它只需要有一个引用即可。

(2)抽象方法必须由子类来进行重写。

(3)只要包含一个抽象方法的类,该类必须要定义成抽象类,不管是否还包含有其他方法。

(4)抽象类中可以包含具体的方法,当然也可以不包含抽象方法。

(5)子类中的抽象方法不能与父类的抽象方法同名。

(6)abstract不能与final并列修饰同一个类。(abstract需要子类去实现,而final表示不能被继承,矛盾。)

A、final修饰的类为终态类,不能被继承,而抽象类是必须被继承的才有其意义的,因此,final是不能用来修饰抽象类的。

B、final修饰的方法为终态方法,不能被重写。而继承抽象类,必须重写其方法。

C、抽象方法是仅声明,并不做实现的方法。

值传递:Java中原始数据类型都是值传递,传递的是值的副本,形参的改变不会影响实际参数的值;

引用传递:传递的是引用类型数据,包括String,数组,列表,map,类对象等类型,形参与实参指向的是同一内存地址,因此形参改变会影响实参的值。

定义:按照现有类的类型来创建新类 ,无需改变现有类的形式,采用现有类的形式并在其增加新代码,称为继承。通过关键字extends实现。

(1)当创建一个类时,总在继承。(除非明确指明继承类,否则都是隐式第继承根类Object);

(2)为了继承,一般将所有的数据成员都指定为private,将所有的方法指定为public;

(3)可以将继承视作是对类的复用;

(4)is-a关系用继承;

(5)继承允许对象视为自身的类型或其基类型加以处理;

(6)如果向上转型,不能调用那些新的方法(如Animal an = new Cat(),an是不能调用Cat中有的而Animal中没有的方法,会返回一条编译时出错消息),所以向上转型会丢失具体的类型信息。

(1)构造方法不能被继承;方法和属性可以被继承;

(2)子类的构造方法隐式地调用父类的不带参数的构造方法;

(3)当父类没有不带参数的构造方法时,子类需要使用super来显示调用父类的构造方法,super指的是对父类的引用;

(4)super关键字必须是构造方法中的第一行语句。特例如下:

整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。

比较,如果数据一致就把内存中的值改为update。这样使用CAS就保证了原子操作。其余几个方法的原理跟这个相同。

操作是 CPU 原语,所以性能比较好。

下面结合实例来分析一下incrementAndGet()方法如何在不加锁的情况下通过CAS实现线程安全,我们不妨考虑一下方法的执行:

(2)线程1运行到第四行获取到当前的value为3,线程切换。

(3)线程2开始运行,获取到value为3,利用CAS对比内存中的值也为3,比较成功,修改内存,此时内存中的value改变,加1后值为4,线程切换。

(4)线程1恢复运行,利用CAS比较发现自己的value为3,内存中的value为4,得到一个重要的结论-->此时value正在被另外一个线程修改,所以我不能去修改它。

(5)线程1的compareAndSet失败,循环判断,因为value是volatile修饰的,所以它具备可见性的特性,线程2对于value的改变能被线程1看到,只要线程1发现当前获取的value是4,内存中的value也是4,说明线程2对于value的修改已经完毕并且线程1可以尝试去修改它。

(6)最后说一点,比如说此时线程3也准备修改value了,没关系,因为比较-交换是一个原子操作不可被打断,线程3修改了value,线程1进行compareAndSet的时候必然返回的false,这样线程1会继续循环去获取最新的value并进行compareAndSet,直至获取的value和内存中的value一致为止。

整个过程中,利用CAS机制保证了对于value的修改的线程安全性。


新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。

(4)线程A调用compareAndSet发现预期值(current=0)与内存中对应的值(valueOffset=1,被线程B修改)不相等,即在本线程执行期间有被修改过,则放弃此次修改,返回false。

多个线程对AtomicInteger类型的变量进行自增操作,运算结果无误,也就是说AtomicInteger可以实现原子操作,即在多线程环境中,执行的操作不会被其他线程打断。若用普通的int变量,i++多线程操作可能导致结果有误。

现在再来思考这个问题:AtomicInteger是如何实现线程安全呢?请大家自己先考虑一下这个问题,其实我们在语言层面是没有做任何同步的操作的,大家也可以看到源码没有任何锁加在上面,可它为什么是线程安全的呢?这就是Atomic包下这些类的奥秘:语言层面不做处理,我们将其交给硬件—CPU和内存,利用CPU的多处理能力,实现硬件层面的阻塞,再加上volatile变量的特性即可实现基于原子操作的线程安全。所以说,CAS并不是无阻塞,只是阻塞并非在语言、线程方面,而是在硬件层面,所以无疑这样的操作会更快更高效!

总结一下,AtomicInteger 中主要实现了整型的原子操作,防止并发情况下出现异常结果,其内部主要依靠JDK 中的unsafe 类操作内存中的数据来实现的。volatile 修饰符保证了value在内存中其他线程可以看到其值得改变。CAS操作保证了AtomicInteger 可以安全的修改value 的值。

    CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。(这段描述引自《Java并发编程实践》)
简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

乐观锁:其实现机制是基于CAS的,每次不加锁,假设没有冲突完成操作,如果有冲突,重试直到成功为止。

即一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。

如上面源代码所示,可以看出最后调用的是Atomic:comxchg这个方法,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

intel的手册对lock前缀的说明如下:

(1)确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他 处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。

(2)禁止该指令与之前和之后的读和写指令重排序。

(3)把写缓冲区中的所有数据刷新到内存中。

关于处理器如何实现原子操作有以下三种:

(1)处理器自动保证基本内存操作的原子性

首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

(2)使用总线锁保证原子性

    第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图:

    原因是有可能多个处理器同时从各自的缓存中读取变量i,分别进行加一操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。

    处理器使用总线锁就是来解决这个问题的所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

(3)使用缓存锁保证原子性

    第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁会把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

    但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

   以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

    从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。


 所谓ABA问题基本是这个样子:

(1)进程P1在共享变量中读到值为A

(2)P1被抢占了,进程P2执行

(3)P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。

(4)P1回来看到共享变量里的值没有被改变,于是继续执行。

    虽然P1以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free 的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了。(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)

这个例子你可能没有看懂,维基百科上给了一个活生生的例子——

你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子
调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。

(2)循环时间长开销大

    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory

(3)只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

CAS利用CPU调用底层指令实现,即CAS操作正是利用了处理器提供的CMPXCHG指令实现的。

单一处理器,进行简单的读写操作时,能保证自身读取的原子性,多处理器或复杂的内存操作时,CAS采用总线加锁或缓存加锁方式保证原子性。

如i=0初始化,多处理器多线程环境下进行i++操作下,处理器A和B同时读取i值到各自缓存,分别进行递增,回写值i=1相同。处理器提供LOCK#信号,进行总线加锁后,处理器A读取i值并递增,处理器B被阻塞不能读取i值。

总线加锁,在LOCK#信号下,其他线程无法操作内存,性能较差,缓存加锁能较好处理该问题。

缓存加锁,处理器A和B同时读取i值到缓存,处理器A提前完成递增,数据立即回写到主内存,并让处理器B缓存该数据失效,处理器B需重新读取i值。

    虽然基于CAS的线程安全机制很好很高效,但要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小,型如计数器这样的需求用起来才有效,否则也不会有锁的存在了。

    AQS,它维护了一个volatile int state(代表共享资源)状态变量和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。

    AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。

    AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

    CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

然后下面这3个方法是CountDownLatch类中最重要的方法:

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
 

    CountDownLatch类是一个同步计数器,构造时传入int参数,该参数就是计数器的初始值,每调用一次countDown()方法,计数器减1,计数器大于0 时,await()方法会阻塞程序继续执行。CountDownLatch可以看作是一个倒计数的锁存器,当计数减至0时触发特定的事件。利用这种特性,可以让主线程等待子线程的结束。

 用给定的计数初始化 CountDownLatch。在调用countDown() 方法,使当前计数减一,且当前计数到达零之前,await 方法会一直受阻塞。当前计数到达零之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。

 CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。

(3)它不要求调用 countDown 方法的线程等到计数到达零时才继续,而在所有线程都能通过之前,它只是阻止任何线程继续通过一个await。即调用countDown 方法的线程并不会阻塞。CountDownLatch调用await方法将阻塞当前线程,直到其他线程调用countDown 方法,使其计数到达零时才继续。 

CountDownLatch是通过“共享锁”实现的。在创建CountDownLatch中时,会传递一个int类型参数count,该参数是“锁计数器”的初始状态,表示该“共享锁”最多能被count个线程同时获取。当某线程调用该CountDownLatch对象的await()方法时,该线程会等待“共享锁”可用时,才能获取“共享锁”进而继续运行。而“共享锁”可用的条件,就是“锁计数器”的值为0!而“锁计数器”的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将“锁计数器”-1;通过这种方式,必须有count个线程调用countDown()之后,“锁计数器”才为0,而前面提到的等待线程才能继续运行!

下面通过CountDownLatch实现:"主线程"等待"5个子线程"全部都完成"指定的工作(休眠1000ms)"之后,再继续运行。

// "主线程"等待5个任务的完成
等待2个子线程执行完毕... 2个子线程已经执行完毕

    字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:

第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。

下面举几个例子就明白了:

假若有若干个线程都要进行写数据操作,并且只有所有线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了:

线程Thread-3写入数据完毕,等待其他线程写入完毕 线程Thread-2写入数据完毕,等待其他线程写入完毕 线程Thread-0写入数据完毕,等待其他线程写入完毕 线程Thread-1写入数据完毕,等待其他线程写入完毕 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务...

从上面输出结果可以看出,每个写入线程执行完写数据操作之后,就在等待其他线程写入操作完毕。当所有线程线程写入操作完毕之后,所有线程就继续进行后续的操作了。

如果说想在所有线程写入操作完之后,进行额外的其他操作可以为CyclicBarrier提供Runnable参数:

线程Thread-0写入数据完毕,等待其他线程写入完毕 线程Thread-1写入数据完毕,等待其他线程写入完毕 线程Thread-2写入数据完毕,等待其他线程写入完毕 线程Thread-3写入数据完毕,等待其他线程写入完毕 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务...

从结果可以看出,当四个线程都到达barrier状态后,会从四个线程中选择一个线程去执行Runnable。

另外CyclicBarrier是可以重用的,看下面这个例子:

线程Thread-2写入数据完毕,等待其他线程写入完毕 线程Thread-1写入数据完毕,等待其他线程写入完毕 线程Thread-0写入数据完毕,等待其他线程写入完毕 线程Thread-3写入数据完毕,等待其他线程写入完毕 Thread-3所有线程写入完毕,继续处理其他任务... Thread-1所有线程写入完毕,继续处理其他任务... Thread-0所有线程写入完毕,继续处理其他任务... Thread-2所有线程写入完毕,继续处理其他任务... 线程Thread-7写入数据完毕,等待其他线程写入完毕 线程Thread-6写入数据完毕,等待其他线程写入完毕 线程Thread-5写入数据完毕,等待其他线程写入完毕 线程Thread-4写入数据完毕,等待其他线程写入完毕 Thread-4所有线程写入完毕,继续处理其他任务... Thread-7所有线程写入完毕,继续处理其他任务... Thread-6所有线程写入完毕,继续处理其他任务... Thread-5所有线程写入完毕,继续处理其他任务...

从执行结果可以看出,在初次的4个线程越过barrier状态后,又可以用来进行新一轮的使用。而CountDownLatch无法进行重复使用。

(1) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。

Semaphore翻译成字面意思为信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。

acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。release()用来释放许可。注意,在释放许可之前,必须先获获得许可。这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

//尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
//尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
//尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
//尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
 

另外还可以通过availablePermits()方法得到可用的许可数目。

下面通过一个例子来看一下Semaphore的具体使用。假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

工人1占用一个机器在生产...
工人0占用一个机器在生产...
工人4占用一个机器在生产...
工人3占用一个机器在生产...
工人2占用一个机器在生产...
工人5占用一个机器在生产...
工人7占用一个机器在生产...
工人6占用一个机器在生产...
 

(1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

(2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

MyBatis的初始化的过程其实就是解析配置文件和初始化Configuration的过程,MyBatis的初始化过程可用以下几行代码来表述:

// 加载mybatis的配置文件(它也加载关联的映射文件)

上图的初始化过程经过以下的几步:

1.开启一个数据库访问会话---创建SqlSession对象:MyBatis使用SQLSession对象来封装一次数据库的会话访问。通过该对象实现对事务的控制和数据查询。

MyBatis封装了对数据库的访问,把对数据库的会话和事务控制放到了SqlSession对象中。

 

(2)、为查询创建缓存,以提高性能;

3、Mybatis的主要构件及其相互关系

 从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:

它们的关系如下图所示:

4、Mybatis和数据库的交互方式

Id 和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。由于面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种使用MyBatis支持接口(Interface)调用方式。

    MyBatis 引用Mapper 接口这种调用方式,纯粹是为了满足面向接口编程的需要。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置SQL语句,这样就可以脱离XML配置文件,实现“0配置”)。

#{}是sql的参数占位符,Mybatis会将sql中的#{}替换为?号,在sql执行前会使用PreparedStatement的参数设置方法,按序给sql的?号占位符设置参数值,比如

这样做的好处是:更安全,更迅速,通常也是首选做法。#{item.name}的取值方式为使用反射从参数对象中获取item对象的name属性值,相当于param.getItem().getName()。


(1)#相当于对数据加上双引号,$相当于直接显示数据。

(2) #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111",如果传入的值是id,则解析成的sql为order by "id"。

(4)#方式能够很大程度防止sql注入,$方式无法防止Sql注入

(5)$方式一般用于传入数据库对象,例如传入表名。

(6)一般能用#的就别用$。

(7)MyBatis排序时使用order by 动态参数时需要注意,用$而不是#。

默认情况下,使用#{}格式的语法会导致MyBatis创建预处理语句属性并以它为背景设置安全的值(比如?)。这样做很安全,很迅速也是首选做法,有时你只是想直接在SQL语句中插入一个不改变的字符串。比如,像ORDER BY,你可以这样来使用:ORDER BY ${columnName} 这里MyBatis不会修改或转义字符串。


#{}:占位符号,好处防止sql注入

动态 SQL 是 mybatis 的强大特性之一,也是它优于其他 ORM 框架的一个重要原因。mybatis 在对 sql 语句进行预编译之前,会对 sql 进行动态解析,解析为一个 BoundSql 对象,也是在此处对动态 SQL 进行处理的。在动态 SQL 解析阶段, #{ } 和 ${ } 会有不同的表现。

一个 #{ } 被解析为一个参数占位符 ? ,而${ } 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换

当我们传递的参数为 "Jack" 时,上述 sql 的解析为:

预编译之前的 SQL 语句已经不包含变量了,完全已经是常量数据了。 综上所得, ${ } 变量的替换阶段是在动态 SQL 解析阶段,而 #{ }变量的替换是在 DBMS 中

首先这是为了性能考虑的,相同的预编译 sql 可以重复利用。其次,${ } 在预编译之前已经被变量替换了,这会存在 sql 注入问题。例如,如下的 sql:

-- 之后的语句将作为注释,不起作用,因此本来的一条查询语句偷偷的包含了一个删除表数据的 SQL。

2、表名作为变量时,必须使用 ${ }

这是因为,表名是字符串,使用 sql 占位符替换字符串时会带上单引号 '',这会导致 sql 语法错误,例如:

预编译之后的sql 变为:

上述 sql 语句是存在语法错误的,表名不能加单引号 ''(注意,反引号 ``是可以的)。

sql 预编译指的是数据库驱动在发送 sql 语句和参数给 DBMS 之前对 sql 语句进行编译,这样 DBMS 执行 sql 时,就不需要重新编译。

 2、为什么需要预编译

JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 sql 的执行。预编译之后的 sql 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的sql,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。预编译语句对象可以重复利用。把一个 sql 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个sql,可以直接使用这个缓存的 PreparedState 对象。mybatis 默认情况下,将对所有的 sql 进行预编译。

6、最佳实践中,通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

   Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。

   Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

7、使用Mybatis的mapper接口调用时有哪些要求?

8、Mybatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

9、简述Mybatis的插件运行原理,以及如何编写一个插件。

Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。

实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

10、Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不?

Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。总体说来mybatis 动态SQL 语句主要有以下几类:

下面分别介绍这几种处理方式

 

    如果你提供了title参数,那么就要满足title=#{title},同样如果你提供了Content和Owner的时候,它们也需要满足相应的条件,之后就是返回满足这些条件的所有Blog,这是非常有用的一个功能。

    以往我们使用其他类型框架或者直接使用JDBC的时候, 如果我们要达到同样的选择效果的时候,我们就需要拼SQL语句,这是极其麻烦的,比起来,上述的动态SQL就要简单多了。

 

when元素表示当when中的条件满足的时候就输出其中的内容,跟JAVA中的switch效果差不多的是按照条件的顺序,当when中有条件满足的时候,就会跳出choose,即所有的when和otherwise条件中,只有一个会输出,当所有的我很条件都不满足的时候就输出otherwise中的内容。所以上述语句的意思非常简单,当title!=null的时候就输出and

 

trim元素的主要功能是可以在自己包含的内容前加上某些前缀也可以在其后加上某些后缀,与之对应的属性是prefix和suffix;可以把包含内容的首部某些内容覆盖,即忽略,也可以把尾部的某些内容覆盖,对应的属性是prefixOverrides和suffixOverrides;正因为trim有这样的功能,所以我们也可以非常简单的利用trim来代替where元素的功能。

trim标记是一个格式化的标记,可以完成set或者是where标记的功能,如下代码:

在红色标记的地方是不存在第一个and的,上面两个属性的意思如下:

prefix:前缀      

在红色标记的地方不存在逗号,而且自动加了一个set前缀和where后缀,上面三个属性的意义如下,其中prefix意义如上:

suffixoverride:去掉最后一个逗号(也可以是其他的标记,就像是上面前缀中的and一样)

 

where元素的作用是会在写入where元素的地方输出一个where,另外一个好处是你不需要考虑where元素里面的条件输出是什么样子的,MyBatis会智能的帮你处理,如果所有的条件都不满足那么MyBatis就会查出所有的记录,如果输出后是and

    set元素主要是用在更新操作的时候,它的主要功能和where元素其实是差不多的,主要是在包含的语句前输出一个set,然后如果包含的语句是以逗号结束的话将会把该逗号忽略,如果set包含的内容为空的话则会出错。有了set元素我们就可以动态的更新那些修改了的字段。

(1)item表示集合中每一个元素进行迭代时的别名。

(2)index指定一个名字,用于表示在迭代过程中,每次迭代到位置。

(3)open表示该语句以什么开始。

(4)separator表示在每次进行迭代之间以什么符号作为分隔符。

(5)close表示以什么结束。

在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况:

(1)如果传入的是单参数参数类型是一个List的时候,collection属性值为list

(2)如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array

(3)如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key。

1、单参数List的类型

mapper 应该是这样的接口:

mybatis的动态sql的执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

   在MyBatis进行查询映射时,其实查询出来的每一个属性都是放在一个对应的Map里面的,其中键是列名,值则是其对应的值。

   1.当提供的返回类型属性是resultType时,MyBatis会将Map里面的键值对取出赋给resultType所指定的对象对应的属性。所以其实MyBatis的每一个查询映射的返回类型都是ResultMap,只是当提供的返回类型属性是resultType的时候,MyBatis会自动把对应的值赋给resultType所指定对象的属性。

   2.当提供的返回类型是resultMap时,因为Map不能很好表示领域模型,就需要自己再进一步的把它转化为对应的对象,这常常在复杂查询中很有作用。


(1)resultType可以映射结果集为基本类型的,而resultMap不能映射结果集为基本类型。

(2)使用resultType进行输出映射,只有查询出来的列名和pojo中的属性名一致,该列才可以映射成功。如果查询出来的列名和pojo中的属性名全部不一致,没有创建pojo对象。只要查询出来的列名和pojo中的属性有一个一致,就会创建pojo对象。对于列名和属性名不一致的情况,就需要通过resultMap来解决。即用resultType进行输出映射,只有查询出来的列名和pojo中的属性名一致,该列才可以映射成功。如果查询出来的列名和pojo的属性名不一致,通过定义一个resultMap对列名和pojo属性名之间作一个映射关系。

(3)resultType 通常用于接收基本类型,包装类型的结果集映射(包装类型的时候就有要求了,必须包装类型中的属性值跟查询结果的字段对应的上,否则的话对应不上的属性是接收不到查询结果的)。而resultMap用于解决复杂查询时的映射问题。比如:列名和对象属性名不一致时可以使用resultMap来配置;还有查询的对象中包含其他的对象等。

12、Mybatis中的一对一、一对多查询

a.resultType:使用resultType实现较为简单,如果pojo中没有包括查询出来的列名,需要增加列名对应的属性,即可完成映射。

b.如果没有查询结果的特殊要求建议使用resultType。

c.resultMap:需要单独定义resultMap,实现有点麻烦,如果对查询结果有特殊的要求,使用resultMap可以完成将关联查询映射pojo的属性中。

在一对一结果映射时,使用resultType更加简单方便,如果有特殊要求(对象嵌套对象)时,需要使用resultMap进行映射,比如:查询订单列表,然后在点击列表中的查看订单明细按钮,这个时候就需要使用resultMap进行结果映射。而resultType更适用于查询明细信息,比如,查询订单明细列表。

一对多查询(例如查询订单及订单明细):

而使用resultType实现:将订单明细映射到orders中的orderdetails中,需要自己处理,使用双重循环遍历,去掉重复记录,将订单明细放在orderdetails中。

作用:将查询结果按照sql列名pojo属性名一致性映射到pojo中。

场合:常见一些明细记录的展示,比如用户购买商品明细,将关联查询信息全部展示在页面时,此时可直接使用resultType将每一条记录映射到pojo中,在前端页面遍历list(list中是pojo)即可。

使用association和collection完成一对一和一对多高级映射(对结果有特殊的映射要求)。

作用:将关联查询信息映射到一个pojo对象中。

场合:为了方便查询关联信息可以使用association将关联订单信息映射为用户对象的pojo属性中,比如:查询订单及关联用户信息。使用resultType无法将查询结果映射到pojo对象的pojo属性中,根据对结果集查询遍历的需要选择使用resultType还是resultMap。

作用:将关联查询信息映射到一个list集合中。

场合:为了方便查询遍历关联信息可以使用collection将关联信息映射到list集合中,比如:查询用户权限范围模块及模块下的菜单,可使用collection将模块映射到模块list中,将菜单列表映射到模块对象的菜单list属性中,这样的作的目的也是方便对查询结果集进行遍历查询。如果使用resultType无法将查询结果映射到list集合中。

MyBatis 提供了查询缓存来缓存数据,以提高查询的性能。MyBatis 的缓存分为一级缓存二级缓存

1、一级缓存是sqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap),用于存储缓存数据。不同的sqlSession之间的缓存区域(HashMap)是互不影响的。

一级缓存是 SqlSession 级别的缓存,是基于 HashMap 的本地缓存。不同的 SqlSession 之间的缓存数据区域互不影响。

一级缓存的作用域是 SqlSession 范围,当同一个 SqlSession 执行两次相同的 sql 语句时,第一次执行完后会将数据库中查询的数据写到缓存,第二次查询时直接从缓存获取不用去数据库查询。当 SqlSession 执行 insert、update、delete 操做并提交到数据库时,会清空缓存,保证缓存中的信息是最新的。

MyBatis默认开启一级缓存。

1.如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前SqlSession缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读。

2.当一个SqlSession结束后那么他里面的一级缓存也就不存在了,mybatis默认是开启一级缓存,不需要配置。

二级缓存是 mapper 级别的缓存,同样是基于 HashMap 进行存储,多个 SqlSession 可以共用二级缓存,其作用域是 mapper 的同一个 namespace。不同的 SqlSession 两次执行相同的 namespace 下的 sql 语句,会执行相同的 sql,第二次查询只会查询第一次查询时读取数据库后写到缓存的数据,不会再去数据库查询。

MyBatis 默认没有开启二级缓存,开启只需在配置文件中写入如下代码:

1.如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前mapper缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读

2、二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。

3、对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。

14、Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。

15、为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?

Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。

B+Tree是B树的变种,有着比B树更高的查询性能,来看下m阶B+Tree特征:

(1)有m个子树的节点包含有m个元素(B-Tree中是m-1)。

(2)根节点和分支节点中不保存数据,只用于索引,所有数据都保存在叶子节点中。

(3)所有分支节点和根节点都同时存在于子节点中,在子节点元素中是最大或者最小的元素。

(4)叶子节点会包含所有的关键字,以及指向数据记录的指针,并且叶子节点本身是根据关键字的大小从小到大顺序链接。

(1)非叶子节点只存储键值信息。

(2)所有叶子节点之间都有一个链指针。

(3)数据记录都存放在叶子节点中。

作为B树的加强版,B+树与B树的差异在于

(1)有n棵子树的节点含有n个关键字(也有认为是n-1个关键字)

(2)所有的叶子节点包含了全部的关键字,及指向含这些关键字记录的指针,且叶子节点本身根据关键字自小而大顺序连接

(3)非叶子节点可以看成索引部分,节点中仅含有其子树(根节点)中的最大(或最小)关键字

数据库为什么要用B+树结构?

为什么使用B+树?言简意赅,就是因为:

(1)索引文件很大,不可能全部存储在内存中,故要存储到磁盘上

(2)索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数(为什么使用B-/+Tree,还跟磁盘存取原理有关。)

(3)局部性原理与磁盘预读,预读的长度一般为页(page)的整倍数,(在许多操作系统中,页得大小通常为4k)。

(4)数据库系统巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,(由于节点中有两个数组,所以地址连续)。而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性。

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

对于B-Tree而言,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。B树的每个节点可以存储多个关键字,它将节点大小设置为磁盘页的大小,充分利用了磁盘预读的功能。每次读取磁盘页时就会读取一整个节点。也正因每个节点存储着非常多个关键字,树的深度就会非常的小。进而要执行的磁盘读取操作次数就会非常少,更多的是在内存中对读取进来的数据进行查找。

为什么红黑树不适合做索引?

红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。也就是说,使用红黑树(平衡二叉树)结构的话,每次磁盘预读中的很多数据是用不上的数据。因此,它没能利用好磁盘预读的提供的数据。然后又由于深度大(较B树而言),所以进行的磁盘IO操作更多。

    B树的查询,主要发生在内存中,而平衡二叉树的查询,则是发生在磁盘读取中。因此,虽然B树查询查询的次数不比平衡二叉树的次数少,但是相比起磁盘IO速度,内存中比较的耗时就可以忽略不计了。因此,B树更适合作为索引。

比B树更适合作为索引的结构——B+树

比B树更适合作为索引的结构是B+树。MySQL中也是使用B+树作为索引。它是B树的变种,因此是基于B树来改进的。为什么B+树会比B树更加优秀呢?

B树:有序数组+平衡多叉树; 
B+树:有序数组链表+平衡多叉树;

从B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。

    B+树的关键字全部存放在叶子节点中,非叶子节点用来做索引,而叶子节点中有一个指针指向一下个叶子节点。做这个优化的目的是为了提高区间访问的性能。而正是这个特性决定了B+树更适合用来存储外部数据。

    数据库索引采用B+树的主要原因是B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。

(1)单节点可以存储更多的元素,使得查询磁盘IO次数更少。首先B+树的中间节点不存储数据,所以同样大小的磁盘页可以容纳更多的节点元素,如此一来,相同数量的数据下,B+树就相对来说要更加矮胖些,磁盘IO的次数更少。

(2)所有查询都要查找到叶子节点,查询性能稳定。由于只有叶子节点才保存数据,B+树每次查询都要到叶子节点;而B树每次查询则不一样,最好的情况是根节点,最坏的情况是叶子节点,没有B+树稳定。

(3)所有叶子节点形成有序链表,便于范围查询。

Servlet的生命周期包含了下面4个阶段:

单例模式,它的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。

单例模式具备典型的3个特点:1、只有一个实例。 2、自我实例化。 3、提供全局访问点。

因此当系统中只需要一个实例对象或者系统中只允许一个公共访问点,除了这个公共访问点外,不能通过其他访问点访问该实例时,可以使用单例模式。

单例模式的主要优点就是节约系统资源、提高了系统效率,同时也能够严格控制客户对它的访问。也许就是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,所以扩展起来有一定的困难。

JSP和Servlet有哪些相同点和不同点,他们之间的联系是什么?

里分离开来。而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。JSP侧重于视图,Servlet主要用于控制逻辑。

      同样用于鉴定2个对象是否相等的,java集合中有 list 和 set 两类,其中 set不允许元素重复出现,那么这个不允许重复出现的方法,如果用 equals 去比较的话,如果存在1000个元素,你 new 一个新的元素出来,需要去调用1000次 equals 去逐个和他们比较是否是同一个对象,这样会大大降低效率。hashcode实际上是返回对象的存储地址,如果这个位置上没有元素,就把元素直接存储在上面,如果这个位置上已经存在元素,这个时候才去调用equals方法与新元素进行比较,相同的话就不存了,散列到其他地址上。

1、输出9*9表。程序分析:分行与列考虑,共9行9列,i控制行,j控制列。

2、猴子吃桃问题:猴子第一天摘下若干个桃子,当即吃了一半,还不瘾,又多吃了一个, 第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了,求第一天共摘了多少。
程序分析:采取逆向思维的方法,从后往前推断,第10天剩下一个,那第9天就是(1+1)*2=4,第8天(4+1)*2=10个,以此类推。

3、两个乒乓球队进行比赛,各出三人,甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单,有人向队员打听比赛的名单,a说他不和x比,c说他不和x,z比,请编程序给出两两对决的名单。
  程序分析:假设a,b,c的对手分别是i,j,k,每个人的对手不可能是同一个人,并且满足以上条件


第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了,求第一天共摘了多少。程序分析:采取逆向思维的方法,从后往前推断,第10天剩下一个,那第9天就是(1+1)*2=4,第8天(4+1)*2=10个,以此类推。 * @author tong * */public class EatPear { public

package tong.yue.day2;/** * 题目描述:两个乒乓球队进行比赛,各出三人,甲队为a,b,c三人,乙队为x,y,z三人。 * 已抽签决定比赛名单,有人向队员打听比赛的名单,a说他不和x比,c说他不和x,z比,请编程序给出两两对决的名单。 * 分析:假设a,b,c的对手分别是i,j,k,每个人的对手不可能是同一个人,并且满足以上条件 * @author tong

*****最给力、最全面实用的Java参考资料-《Java程序员面试宝典 - 基础知识、编程 2016最新》*****
-----内容详实、贴切,详细介绍了java基本知识、java的基本API,常用用法、以及50道java常见编程题,是程序员朋友不可多得的参考资料;
-----无论做java编程开发,还是找工作面试,此文档都是不可多得的好资料
-----应用内容涵盖了:Java基础知识总结、java常用API大全、java编程50题。

  • 注意:新概念英语笔记已不再支持,若之前已下载用户请不要更新。

练习题有些答案不严谨,这个数据答案对上了,但是改成别的有的就不行了

练习题有些答案不严谨,这个数据答案对上了,但是改成别的有的就不行了

内容太少,建议多补充一些

内容太少,建议多补充一些

人民的公务员为人民服务

人民的公务员为人民服务

  • 启用“家人共享”后,此 App 最多可供 6 名家庭成员使用。

更多来自此开发人员的 App

我要回帖

更多关于 java基础编程题 的文章

 

随机推荐