java基础知识

JAVA基础

1.Java跨平台原理

1、是么是平台

CPU处理器与操作系统的整体叫平台

CPU大家都知道,如果计算机是人,那CPU就是人的大脑,它既负责思维运算,又负责身体各部件的命令控制。CPU的种类很多,除去我们熟知的Intel与AMD外,还有比如上面说到的SUN的Sparc,比如IBM的PowerPC等等,这些各个公司生产的CPU使用或相同或不同的指令集。指令集就是cpu中用来计算和控制计算机系统的一套指令的集合。指令集又分为精简指令集(RISC)与复杂指令集(CISC),每种cpu都有其特定的指令集。开发程序,首先要知道该程序在什么CPU上运行,也就是要知道CPU所使用的指令集。

下面说操作系统,操作系统是充当用户和计算机之间交互的界面软件,不同的操作系统支持不同的CPU,严格意义上说是不同的操作系统支持不同CPU的指令集。例如windows和liunx都支持Intel和AMD的复杂指令集,但并不支持PowerPC所使用的精简指令集,而早期的MAC电脑使用的是PowerPC处理器,所以也就无法在MAC下直接安装windows,直到05年MAC改用了Intel的CPU,才使在MAC下安装windows成为可能。但问题来了,原来的MAC操作系统也只支持PowerPC,在Intel上也不能安装,怎么办?所以苹果公司也得重写自己的MAC操作系统以支持这种变化。最后总结下,我们要知道,不同的操作系统支持不同的CPU指令集,现在的windows,liunx,mac,solaris都支持Intel与AMD的CPU指令集。

可以说“平台=CPU+OS”。又因为现在主流的操作系统都支持主流的CPU,所以有时也把操作系统称为平台。

知道什么是平台,我们看Java跨平台原理。

2、Java跨平台原理

使用特定编译器编译的程序只能在对应的平台运行,这里也可以说编译器是与平台相关的,编译后的文件也是与平台相关的。我们说的语言跨平台是编译后的文件跨平台,而不是源程序跨平台.

第一,C语言是编译执行的编译器与平台相关编译生成的可执行文件与平台相关;第二,Java是解释执行的,编译为中间码的编译器与平台无关,编译生成的中间码也与平台无关(一次编译,到处运行),中间码再由解释器解释执行,解释器是与平台相关的,也就是不同的平台需要不同的解释器.

这里再说下语言根据执行方式的不同分类:第一是编译执行,如上文中说到的C,它把源程序由特定平台的编译器一次性编译为平台相关的机器码,它的优点是执行速度快,缺点是无法跨平台;第二是解释执行,如HTML,JavaScript,它使用特定的解释器,把代码一行行解释为机器码,类似于同声翻译,它的优点是可以跨平台,缺点是执行速度慢,暴露源程序;第三种是从Java开始引入的“中间码+虚拟机”的方式,它既整合了编译语言与解释语言的优点,同时如虚拟机又可以解决如垃圾回收,安全性检查等这些传统语言头疼的问题,所以其后微软的.NET平台也使用的这种方式。

Java先编译后解释

同一个.class文件在不同的虚拟机会得到不同的机器指令(WindowsLinux的机器指令不同),但是最终执行的结果却是相同的

 

 

2.九种基本数据类型的大小,以及他们的封装类

首先,八种基本数据类型分别是:int、short、float、double、long、boolean、byte、char;它们的封装类分别是:Integer、Short、Float、Double、Long、Boolean、Byte、Character。

因为对基本数据类型封装之后,封装类有可以有方法和属性,然后就可以利用这些方法和属性来处理数据,比如Ingeter对象中有parseInt(Strings),可以把字符串转换为int类型等。我们都知道有些类型的数据会有默认值,基本数据类型跟封装类型的默认值是不一样的,比如inti,如果不赋值i默认为0;但是Integerj,如果不赋值,则j为null;因为封装类产生的是对象,而对象默认值为null。

tip:String类型不是基本数据类型,它实际上是final修饰,所以也不可以继承。

3.Switch能否用string做参数?

在Java7之前,switch只能支持byte、short、char、int或者其对应的封装类以及Enum类型。在Java7中,String支持被加上了。在Java语言中Swith可以使用参数类型有:Onlyconvertibleintvalues,stringsorenumvariablesarepermitted可以自动转换为整型的(byte,short,int),String类型,枚举类型。

Java中不能做为Switch参数的有boolean,float,double,long(不能直接转换为int啊)。

4.equals与==的区别。

基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean,他们之间的比较,应用双等号(==),比较的是他们的值。

复合数据类型(类)当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。

 

对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。

5.Object有哪些公用方法?

Object是所有类的父类,任何类都默认继承Object。Object类到底实现了哪些方法?

1.clone方法

保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。

2.getClass方法

final方法,获得运行时类型。

3.toString方法

该方法用得比较多,一般子类都有覆盖。

4.finalize方法

该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。

5.equals方法

该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。

6.hashCode方法

该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。

一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash-Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。

7.wait方法

wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(longtimeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。

调用该方法后当前线程进入睡眠状态,直到以下事件发生。

(1)其他线程调用了该对象的notify方法。

(2)其他线程调用了该对象的notifyAll方法。

(3)其他线程调用了interrupt中断该线程。

(4)时间间隔到了。

此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。

8.notify方法

该方法唤醒在该对象上等待的某个线程。

9.notifyAll方法

该方法唤醒在该对象上等待的所有线程。

6.Java的四种引用,强弱软虚,用到的场景

强引用:

强引用不会被GC回收,并且在java.lang.ref里也没有实际的对应类型,平时工作接触的最多的就是强引用。Objectobj=newObject();这里的obj引用便是一个强引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用:

如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用:

引用(weakreference)在强度上弱于软引用,通过类WeakReference来表示。它的作用是引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收。弱引用的作用在于解决强引用所带来的对象之间在存活时间上的耦合关系。弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。

对于这种情况的解决办法就是使用弱引用来引用这些对象,这样哈希表中的键和值对象都能被垃圾回收。Java中提供了WeakHashMap来满足这一常见需求。

虚引用:

在介绍幽灵引用之前,要先介绍Java提供的对象终止化机制(finalization)。在Object类里面有个finalize方法,其设计的初衷是在一个对象被真正回收之前,可以用来执行一些清理的工作。因为Java并没有提供类似C++的析构函数一样的机制,就通过finalize方法来实现。但是问题在于垃圾回收器的运行时间是不固定的,所以这些清理工作的实际运行时间也是不能预知的。幽灵引用(phantomreference)可以解决这个问题。在创建幽灵引用PhantomReference的时候必须要指定一个引用队列。当一个对象的finalize方法已经被调用了之后,这个对象的幽灵引用会被加入到队列中。通过检查该队列里面的内容就知道一个对象是不是已经准备要被回收了。

幽灵引用及其队列的使用情况并不多见,主要用来实现比较精细的内存使用控制,这对于移动设备来说是很有意义的。程序可以在确定一个对象要被回收之后,再申请内存创建新的对象。通过这种方式可以使得程序所消耗的内存维持在一个相对较低的数量。

7.Hashcode的作用。

总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。

你知道它们的区别吗?前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。

那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?

这就是Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。

也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。

于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。

哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。

初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。

这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。

如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,

就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。

所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。所以,Java对于eqauls方法和hashCode方法是这样规定的:

1、如果两个对象相同,那么它们的hashCode值一定要相同;

2、如果两个对象的hashCode相同,它们并不一定相同,上面说的对象相同指的是用eqauls方法比较。

 

8.ArrayList、LinkedList、Vector的区别。

LinkedList类

LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。

注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:

Listlist=Collections.synchronizedList(newLinkedList(...));

 

ArrayList类

ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。

size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。

每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。

和LinkedList一样,ArrayList也是非同步的(unsynchronized)。

 

Vector类

Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。

 

9.String、StringBuffer与StringBuilder的区别。

最近学习到StringBuffer,心中有好些疑问,搜索了一些关于String,StringBuffer,StringBuilder的东西,现在整理一下。

关于这三个类在字符串处理中的位置不言而喻,那么他们到底有什么优缺点,到底什么时候该用谁呢?下面我们从以下几点说明一下

1.三者在执行速度方面的比较:StringBuilder>StringBuffer>String

 

2.String<(StringBuffer,StringBuilder)的原因

String:字符串常量

StringBuffer:字符创变量

StringBuilder:字符创变量

从上面的名字可以看到,String是“字符创常量”,也就是不可改变的对象。对于这句话的理解你可能会产生这样一个疑问,比如这段代码:

1Strings="abcd";
2s=s+1;
3System.out.print(s);//result:abcd1

 

我们明明就是改变了String型的变量s的,为什么说是没有改变呢?其实这是一种欺骗,JVM是这样解析这段代码的:首先创建对象s,赋予一个abcd,然后再创建一个新的对象s用来执行第二行代码,也就是说我们之前对象s并没有变化,所以我们说String类型是不可改变的对象了,由于这种机制,每当用String操作字符串时,实际上是在不断的创建新的对象,而原来的对象就会变为垃圾被GC回收掉,可想而知这样执行效率会有多底。

而StringBuffer与StringBuilder就不一样了,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,这样就不会像String一样创建一些而外的对象进行操作了,当然速度就快了。

3.一个特殊的例子:

1Stringstr=“Thisisonlya”+“simple”+“test”;
3StringBufferbuilder=newStringBuilder(“Thisisonlya”).append(“simple”).append(“test”);

你会很惊讶的发现,生成str对象的速度简直太快了,而这个时候StringBuffer居然速度上根本一点都不占优势。其实这是JVM的一个把戏,实际上:

Stringstr=“Thisisonlya”+“simple”+“test”;

其实就是:

Stringstr=“Thisisonlyasimpletest”;

所以不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的String对象的话,速度就没那么快了,譬如:

Stringstr2=“Thisisonlya”;

Stringstr3=“simple”;

Stringstr4=“test”;

Stringstr1=str2+str3+str4;

这时候JVM会规规矩矩的按照原来的方式去做。

4.StringBuilder与StringBuffer

StringBuilder:线程非安全的

StringBuffer:线程安全的

当我们在字符串缓冲去被多个线程使用是,JVM不能保证StringBuilder的操作是安全的,虽然他的速度最快,但是可以保证StringBuffer是可以正确操作的。当然大多数情况下就是我们是在单线程下进行的操作,所以大多数情况下是建议用StringBuilder而不用StringBuffer的,就是速度的原因。

 

对于三者使用的总结:1.如果要操作少量的数据用=String

2.单线程操作字符串缓冲区下操作大量数据=StringBuilder

3.多线程操作字符串缓冲区下操作大量数据=StringBuffer

 

10.Map、Set、List、Queue、Stack的特点与用法

Map

Map是键值对,键Key是唯一不能重复的,一个键对应一个值,值可以重复。

TreeMap可以保证顺序,HashMap不保证顺序,即为无序的。

Map中可以将Key和Value单独抽取出来,其中KeySet()方法可以将所有的keys抽取正一个Set。而Values()方法可以将map中所有的values抽取成一个集合。

 

Set

不包含重复元素的集合,set中最多包含一个null元素

只能用Lterator实现单项遍历,Set中没有同步方法。

 

List

有序的可重复集合。

可以在任意位置增加删除元素。

用Iterator实现单向遍历,也可用ListIterator实现双向遍历

 

Queue

Queue遵从先进先出原则。

使用时尽量避免add()和remove()方法,而是使用offer()来添加元素,使用poll()来移除元素,它的优点是可以通过返回值来判断是否成功。

LinkedList实现了Queue接口。

Queue通常不允许插入null元素。

 

Stack

Stack遵从后进先出原则。

Stack继承自Vector。

它通过五个操作对类Vector进行扩展,允许将向量视为堆栈,它提供了通常的push和pop操作,以及取堆栈顶点的peek()方法、测试堆栈是否为空的empty方法等

 

用法

如果涉及堆栈,队列等操作,建议使用List

对于快速插入和删除元素的,建议使用LinkedList

如果需要快速随机访问元素的,建议使用ArrayList

 

11.HashMap和Hashtable的区别

1HashMap不是线程安全的

hastmap是一个接口是map接口的子接口,是将键映射到值的对象,其中键和值都是对象,并且不能包含重复键,但可以包含重复值。HashMap允许nullkey和nullvalue,而hashtable不允许。

2HashTable是线程安全的一个Collection。

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Mapinterface的一个实现。最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap就必须为之提供外同步。Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差

publicstaticvoidmain(Stringargs[]){HashTableh=newHashTable();h.put("用户1",newInteger(90));h.put("用户2",newInteger(50));h.put("用户3",newInteger(60));h.put("用户4",newInteger(70));h.put("用户5",newInteger(80));Enumeratione=h.elements();while(e.hasMoreElements()){System.out.println(e.nextElement());}

总结:

hashmap 线程不安全 允许有null的键和值 效率高一点、 方法不是Synchronize的要提供外同步 有containsvalue和containsKey方法 HashMap是Java1.2引进的Mapinterface的一个实现 HashMap是Hashtable的轻量级实现
hashtable 线程安全 不允许有null的键和值 效率稍低、 方法是是Synchronize的 有contains方法方法 、Hashtable继承于Dictionary类 Hashtable比HashMap要旧

 

 

12.HashMap与ConcurrentHashMap的区别

ConcurrentHashMap融合了hashtable和hashmap二者的优势。

 

hashtable是做了同步的,hashmap未考虑同步。所以hashmap在单线程情况下效率较高。hashtable在的多线程情况下,同步操作能保证程序执行的正确性。

 

但是hashtable每次同步执行的时候都要锁住整个结构。看下图:

 

 

图左侧清晰的标注出来,lock每次都要锁住整个结构。

 

ConcurrentHashMap正是为了解决这个问题而诞生的。

 

ConcurrentHashMap锁的方式是稍微细粒度的。ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。

 

试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。

 

更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。

 

而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

 

 

13.TreeMap、HashMap、LindedHashMap的区别。

Java为数据结构中的映射定义了一个接口java.util.Map,它有四个实现类,分别是HashMap、HashTable、LinkedHashMap和TreeMap

 

Map用于存储键值对,根据键得到值,因此不允许键重复,值可以重复。

 

(1)HashMap是一个最常用的Map,它根据键的hashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为null,不允许多条记录的值为null。HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要同步,可以用Collections.synchronizedMap(HashMapmap)方法使HashMap具有同步的能力。

(2)Hashtable与HashMap类似,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,然而,这也导致了Hashtable在写入时会比较慢。

(3)LinkedHashMap保存了记录的插入顺序,在用Iteraor遍历LinkedHashMap时,先得到的记录肯定是先插入的。在遍历的时候会比HashMap慢。有HashMap的全部特性。

(4)TreeMap能够把它保存的记录根据键排序,默认是按升序排序,也可以指定排序的比较器。当用Iteraor遍历TreeMap时,得到的记录是排过序的。TreeMap的键和值都不能为空。

 

14.Java中Collection和Collections的区别

1、java.util.Collection是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。

Collection

├List

│├LinkedList

│├ArrayList

│└Vector

│ └Stack

└Set

 

2、java.util.Collections是一个包装类(工具类/帮助类)。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,用于对集合中元素进行排序、搜索以及线程安全等各种操作,服务于Java的Collection框架。

 

15.Excption与Error包结构

Java异常架构图

1.Throwable
Throwable是Java语言中所有错误或异常的超类。
Throwable包含两个子类:ErrorException。它们通常用于指示发生了异常情况。
Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。

2.Exception
Exception及其子类是Throwable的一种形式,它指出了合理的应用程序想要捕获的条件。

3.RuntimeException
RuntimeException是那些可能在Java虚拟机正常运行期间抛出的异常的超类。
编译器不会检查RuntimeException异常。例如,除数为零时,抛出ArithmeticException异常。RuntimeException是ArithmeticException的超类。当代码发生除数为零的情况时,倘若既"没有通过throws声明抛出ArithmeticException异常",也"没有通过try...catch...处理该异常",也能通过编译。这就是我们所说的"编译器不会检查RuntimeException异常"!
如果代码会产生RuntimeException异常,则需要通过修改代码进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!

4.Error
和Exception一样,Error也是Throwable的子类。它用于指示合理的应用程序不应该试图捕获的严重问题,大多数这样的错误都是异常条件。
和RuntimeException一样,编译器也不会检查Error。

Java将可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常(RuntimeException)和错误(Error)

(01)运行时异常
定义:RuntimeException及其子类都被称为运行时异常。
特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。
如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!

(02)被检查的异常
定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。
特点Java编译器会检查它。此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。
被检查异常通常都是可以恢复的。

(03)错误
定义:Error类及其子类。
特点:和运行时异常一样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。
按照Java惯例,我们是不应该是实现任何新的Error子类的!

对于上面的3种结构,我们在抛出异常或错误时,到底该哪一种?《EffectiveJava》中给出的建议是:对于可以恢复的条件使用被检查异常,对于程序错误使用运行时异常。

OOM:

OutOfMemoryError异常

除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,

JavaHeap溢出

一般的异常信息:java.lang.OutOfMemoryError:Javaheapspacess

java堆用于存储对象实例,我们只要不断的创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

出现这种异常,一般手段是先通过内存映像分析工具(如EclipseMemoryAnalyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(MemoryLeak)还是内存溢出(MemoryOverflow)。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象时通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收。

如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。

2,虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

这里需要注意当栈的大小越大可分配的线程数就越少。

3,运行时常量池溢出

异常信息:java.lang.OutOfMemoryError:PermGenspace

如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

4,方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

异常信息:java.lang.OutOfMemoryError:PermGenspace

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

16.Java面向对象的三个特征与含义

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装是面向对象的特征之一,是对象和类概念的主要特性。 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

 

继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。

 

多态就是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

 

多态是面向对象三大特征里相对难理解和表述的一个特征。

 

多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

 

多态存在的三个必要条件

一、要有继承;

二、要有重写;

三、父类引用指向子类对象。

 

 

17.Override和Overload的含义与区别

方法的重写(Overriding)和重载(Overloading)是Java多态性的不同表现。

重写(Overriding)是父类与子类之间多态性的一种表现,而重载(Overloading)是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding) 。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被&quot;屏蔽&quot;了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型或有不同的参数次序,则称为方法的重载(Overloading)。不能通过访问权限、返回类型、抛出的异常进行重载。

 

1.Override 特点

1、覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;

2、覆盖的方法的返回值必须和被覆盖的方法的返回一致;

3、覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;

4、方法被定义为final不能被重写。

5、对于继承来说,如果某一方法在父类中是访问权限是private,那么就不能在子类对其进行重写覆盖,如果定义的话,也只是定义了一个新方法,而不会达到重写覆盖的效果。(通常存在于父类和子类之间。)

 

2.Overload 特点

1、在使用重载时只能通过不同的参数样式。例如,不同的参数类型,不同的参数个数,不同的参数顺序(当然,同一方法内的几个参数类型必须不一样,例如可以是fun(int, float), 但是不能为fun(int, int));

2、不能通过访问权限、返回类型、抛出的异常进行重载;

3、方法的异常类型和数目不会对重载造成影响;

4、重载事件通常发生在同一个类中,不同方法之间的现象。

5、存在于同一类中,但是只有虚方法和抽象方法才能被覆写。

 

3.具体实现机制:

overload是重载,重载是一种参数多态机制,即代码通过参数的类型或个数不同而实现的多态机制。 是一种静态的绑定机制(在编译时已经知道具体执行的是哪个代码段)。

 

override是覆盖。覆盖是一种动态绑定的多态机制。即在父类和子类中同名元素(如成员函数)有不同 的实现代码。执行的是哪个代码是根据运行时实际情况而定的。

 

 

18.Interface与abstract类的区别

1.abstract class 在Java中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。

2.在abstract class 中可以有自己的数据成员,也可以有非abstarct的方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在 interface中一般不定义数据成员),所有的方法都是public abstract的。

3.抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值。接口中定义的变量默认是public static final 型,且必须给其赋初值,所以实现类中不能重新定义,也不能改变其值。

4.abstract class和interface所反映出的设计理念不同。其实abstract class表示的是"is-a"关系,interface表示的是"like-a"关系。

5.实现抽象类和接口的类必须实现其中的所有方法。抽象类中可以有非抽象方法。接口中则不能有实现方法。

abstract class 和 interface 是 Java语言中的两种定义抽象类的方式,它们之间有很大的相似性。但是对于它们的选择却又往往反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理,因为它们表现了概念间的不同的关系。

 

19.Static Class 与 non Static Class

静态内部类和非静态内部类之间到底有什么不同呢?下面是两者间主要的不同。

(1)内部静态类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。

(2)非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。

(3)一个非静态内部类不能脱离外部类实体被创建,一个非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面。

 

内部静态类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。一个非静态内部类不能脱离外部类实体被创建,一个非静态内部类可以访问外部类的数据和方法,因为他就在外部类里面

 

简单理解就是:如果把类比喻成鸡蛋,内部类为蛋黄,外部类是蛋壳。那么静态类相当于熟鸡蛋,就算蛋壳破碎(外部类没有实例化),蛋黄依然完好(内部类可以实例化);而非静态类相当于生鸡蛋,蛋壳破碎(无实例化),蛋黄也会跟着xx(不能实例化)。

 

20.java多态的实现原理。

深入理解JVM一书 8.3.2 分派

 

重载----静态分派

依赖静态类型来定位方法执行版本的分派动作成为静态分派,典型应用是方法重载,发生在编译阶段。

 

重写----动态分派

在运行期根据实际类型确定方法执行版本额分派过程成为动态分派

 

21.实现多线程的两种方法:Thread与Runable

不论是那种方式,最后都需要通过Thread类的实例调用start()方法来开始线程的执行,start()方法通过java虚拟机调用线程中定义的run方法来执行该线程。通过查看java源程序中的start()方法的定义可以看到,它是通过调用操作系统的start0方法来实现多线程的操作的。

但是一般在系统的开发中遇到多线程的情况的时候,以实现Runnable接口的方式为主要方式。这是因为实现接口的方式有很多的优点:

1、就是通过继承Thread类的方式时,线程类就无法继承其他的类来实现其他一些功能,实现接口的方式就没有这中限制;

2.也是最重要的一点就是,通过实现Runnable接口的方式可以达到资源共享的效果。

但是其实两种方式是有密切联系的:

我们通过实现接口Runnable建立的MyThread类,而Thread类也是实现了Runnable接口的子类。如果我们想启动线程,需要通过Thread类中的start()方法,Thread类中的start()方法来调用MyThread类中run方法来执行该线程。这个实现是典型的代理模式。

 

 

22.线程同步的方法:sychronized、lock、reentrantLock

 

在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。

 

1、ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候

线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,

如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断

如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

 

ReentrantLock获取锁定与三种方式:

  1. a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
  2. b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;

c)tryLock(long timeout,TimeUnit unit),   如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;

  1. d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

 

2、synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中

 

3、在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

 

5.0的多线程任务包对于同步的性能方面有了很大的改进,在原有synchronized关键字的基础上,又增加了ReentrantLock,以及各种Atomic类。了解其性能的优劣程度,有助与我们在特定的情形下做出正确的选择。

 

总体的结论先摆出来:

 

synchronized:

在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。

 

ReentrantLock:

ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

 

Atomic:

和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。

 

 

所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。

 

 

23.方法锁、对象锁、类锁

首先介绍一下对象锁(也叫方法锁)与类锁有那些不同。下文中使用对象锁称呼代替方法锁。

 

对于对象锁,是针对一个对象的,它只在该对象的某个内存位置声明一个标志位标识该对象是否拥有锁,所以它只会锁住当前的对象。一般一个对象锁是对一个非静态成员变量进行syncronized修饰,或者对一个非静态方法进行syncronized修饰。对于对象锁,不同对象访问同一个被syncronized修饰的方法的时候不会阻塞住。

 

类锁是锁住整个类的,当有多个线程来声明这个类的对象的时候将会被阻塞,直到拥有这个类锁的对象被销毁或者主动释放了类锁。这个时候在被阻塞住的线程被挑选出一个占有该类锁,声明该类的对象。其他线程继续被阻塞住。

 

无论是类锁还是对象锁,父类和子类之间是否阻塞没有直接关系。当对一个父类加了类锁,子类是不会受到影响的,相反也是如此。因为synchronized关键字并不是方法签名的一部分,它是对方法进行修饰的。当子类覆写父类中的同步方法或是接口中声明的同步方法的时候,synchronized修饰符是不会被自动继承的,所以相应的阻塞问题不会出现。

 

注意:这里的阻塞问题是指的按照正常情况下应该阻塞,而因为synchronized是父类与子类之间不可传递导致不会阻塞。那正常情况下阻塞是什么那,下面会详细介绍。但是,当一个子类没有覆盖父类的方法的时候,这时候通过子类访问方法则会产生阻塞。

 

 

24.生产者消费者模式

生产者消费者问题是一个典型的线程同步问题。生产者生产商品放到容器中,容器有一定的容量(只能顺序放,先放后拿),消费者消费商品,当容器满了后,生产者等待,当容器为空时,消费者等待。当生产者将商品放入容器后,通知消费者;当消费者拿走商品后,通知生产者。

 

 

public class ProducerConsumerTest {

 

public static void main(String []args){

Container con = new Container();

Producer p = new Producer(con);

Consumer c = new Consumer(con);

new Thread(p).start();

new Thread(c).start();

}

}

 

class Goods{

int id;

public Goods(int id){

this.id=id;

}

 

public String toString(){

return "商品"+this.id;

}

}

class Container{//容器采用栈,先进后出

private int index = 0;

Goods[] goods = new Goods[6];

 

public synchronized void push(Goods good){

while(index==goods.length){//当容器满了,生产者等待

try {

wait();

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

goods[index]=good;

index++;

notifyAll();//当生产者放入商品后通知消费者

}

 

public synchronized Goods pop(){

while(index==0){//当容器内没有商品是等待

try {

wait();

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

index--;

notifyAll();//当消费者消费了商品后通知生产者

return goods[index];

}

}

class Producer implements Runnable{

 

Container con = new Container();

public Producer(Container con){

this.con=con;

}

 

public void run(){

for(int i=0; i<20; i++){

Goods good = new Goods(i);

con.push(good);

System.out.println("生产了:"+good);

try {

Thread.sleep(100);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

 

}

class Consumer implements Runnable{

 

Container con = new Container();

public Consumer(Container con){

this.con=con;

}

 

public void run(){

for(int i=0; i<20; i++){

Goods good=con.pop();

System.out.println("消费了:"+good);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

 

25.ThreadLocal的设计理念与作用

Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。

 

首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。

另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。

如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

下面来看一个hibernate中典型的ThreadLocal的应用:

Java代码  收藏代码

private static final ThreadLocal threadSession = new ThreadLocal();

 

public static Session getSession() throws InfrastructureException {

Session s = (Session) threadSession.get();

try {

if (s == null) {

s = getSessionFactory().openSession();

threadSession.set(s);

}

} catch (HibernateException ex) {

throw new InfrastructureException(ex);

}

return s;

}

可以看到在getSession()方法中,首先判断当前线程中有没有放进去session,如果还没有,那么通过sessionFactory().openSession()来创建一个session,再将session set到线程中,实际是放到当前线程的ThreadLocalMap这个map中,这时,对于这个session的唯一引用就是当前线程中的那个ThreadLocalMap(下面会讲到),而threadSession作为这个值的key,要取得这个session可以通过threadSession.get()来得到,里面执行的操作实际是先取得当前线程中的ThreadLocalMap,然后将threadSession作为key将对应的值取出。这个session相当于线程的私有变量,而不是public的。

显然,其他线程中是取不到这个session的,他们也只能取到自己的ThreadLocalMap中的东西。要是session是多个线程共享使用的,那还不乱套了。

试想如果不用ThreadLocal怎么来实现呢?可能就要在action中创建session,然后把session一个个传到service和dao中,这可够麻烦的。或者可以自己定义一个静态的map,将当前thread作为key,创建的session作为值,put到map中,应该也行,这也是一般人的想法,但事实上,ThreadLocal的实现刚好相反,它是在每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了,不知道除了这些还有什么其他的好处。

 

总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:

1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。

2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

ThreadLocal的应用场合,我觉得最适合的是多线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

当然如果要把本来线程共享的对象通过ThreadLocal.set()放到线程中也可以,可以实现避免参数传递的访问方式,但是要注意get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决。但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中。

 

 

26.ThreadPool用法与优势

1. 优势

合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。

2. 线程池的使用

线程池的创建

我们可以通过ThreadPoolExecutor来创建一个线程池。

new  ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

创建一个线程池需要输入几个参数:

  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
  • runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是5提供的四种策略。
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

向线程池提交任务

我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。

threadsPool.execute(new Runnable() {            @Override            public void run() {                // TODO Auto-generated method stub            }        });

我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

Future<Object> future = executor.submit(harReturnValuetask);try {     Object s = future.get();} catch (InterruptedException e) {    // 处理中断异常} catch (ExecutionException e) {    // 处理无法执行任务异常} finally {    // 关闭线程池    executor.shutdown();}

线程池的关闭

我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。

3. 线程池的分析

流程分析:线程池的主要工作流程如下图:

从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:

  1. 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
  2. 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
  3. 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

源码分析。上面的流程分析让我们很直观的了解了线程池的工作原理,让我们再通过源代码来看看是如何实现的。线程池执行任务的方法如下:

public void execute(Runnable command) {    if (command == null)       throw new NullPointerException();    //如果线程数小于基本线程数,则创建线程并执行当前任务     if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {    //如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。        if (runState == RUNNING && workQueue.offer(command)) {            if (runState != RUNNING || poolSize == 0)                      ensureQueuedTaskHandled(command);        }    //如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,则创建一个线程执行任务。        else if (!addIfUnderMaximumPoolSize(command))        //抛出RejectedExecutionException异常            reject(command); // is shutdown or saturated    }}

工作线程。线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会无限循环获取工作队列里的任务来执行。我们可以从Worker的run方法里看到这点:

public void run() {     try {           Runnable task = firstTask;           firstTask = null;            while (task != null || (task = getTask()) != null) {                    runTask(task);                    task = null;            }      } finally {             workerDone(this);      }}

4. 合理的配置线程池

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

5. 线程池的监控

通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
  • largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不+ getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如:

protected void beforeExecute(Thread t, Runnable r) { }

27. ArrayBlockingQueue和LinkedBlockingQueue的使用

BlockingQueue接口定义了一种阻塞的FIFO queue,每一个BlockingQueue都有一个容量,让容量满时往BlockingQueue中添加数据时会造成阻塞,当容量为空时取元素操作会阻塞。

 

ArrayBlockingQueue是一个由数组支持的有界阻塞队列。在读写操作上都需要锁住整个容器,因此吞吐量与一般的实现是相似的,适合于实现“生产者消费者”模式。

 

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

 

ArrayBlockingQueue和LinkedBlockingQueue的区别:

  1. 队列中锁的实现不同

ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;

LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock

  1. 在生产或消费时操作不同

ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;

LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node<E>进行插入或移除,会影响性能

  1. 队列大小初始化方式不同

ArrayBlockingQueue实现的队列中必须指定队列的大小;

LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE

 

Java代码  

  1. publicclass BlockingQueueTest {
  2. private static ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(5, true); //最大容量为5的数组堵塞队列
  3. //private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>(5);
  4. private static CountDownLatch producerLatch; //倒计时计数器
  5. private static CountDownLatch consumerLatch;
  6. public static void main(String[] args) {
  7. BlockingQueueTest queueTest = new BlockingQueueTest();
  8. test();
  9. }
  10. privatevoid test(){
  11. producerLatch = newCountDownLatch(10); //state值为10
  12. consumerLatch = newCountDownLatch(10); //state值为10
  13. Thread t1 = newThread(new ProducerTask());
  14. Thread t2 = newThread(new ConsumerTask());
  15. //启动线程
  16. start();
  17. start();
  18. try{
  19. out.println("producer waiting...");
  20. await(); //进入等待状态,直到state值为0,再继续往下执行
  21. out.println("producer end");
  22. catch(InterruptedException e) {
  23. printStackTrace();
  24. }
  25. try{
  26. out.println("consumer waiting...");
  27. await(); //进入等待状态,直到state值为0,再继续往下执行
  28. out.println("consumer end");
  29. catch(InterruptedException e) {
  30. printStackTrace();
  31. }
  32. //结束线程
  33. interrupt();
  34. interrupt();
  35. out.println("end");
  36. }
  37. //生产者
  38. classProducerTask implements Runnable{
  39. privateRandom rnd = new Random();
  40. @Override
  41. publicvoid run() {
  42. try{
  43. while(true){
  44. put(rnd.nextInt(100)); //如果queue容量已满,则当前线程会堵塞,直到有空间再继续
  45. //offer方法为非堵塞的
  46. //queue.offer(rnd.nextInt(100), 1, TimeUnit.SECONDS); //等待1秒后还不能加入队列则返回失败,放弃加入
  47. //queue.offer(rnd.nextInt(100));
  48. countDown(); //state值减1
  49. //TimeUnit.SECONDS.sleep(2); //线程休眠2秒
  50. }
  51. catch(InterruptedException e) {
  52. //e.printStackTrace();
  53. }  catch(Exception ex){
  54. printStackTrace();
  55. }
  56. }
  57. }
  58. //消费者
  59. classConsumerTask implements Runnable{
  60. @Override
  61. publicvoid run() {
  62. try{
  63. while(true){
  64. Integer value = queue.take(); //如果queue为空,则当前线程会堵塞,直到有新数据加入
  65. //poll方法为非堵塞的
  66. //Integer value = queue.poll(1, TimeUnit.SECONDS); //等待1秒后还没有数据可取则返回失败,放弃获取
  67. //Integer value = queue.poll();
  68. out.println("value = "+ value);
  69. countDown(); //state值减1
  70. SECONDS.sleep(2); //线程休眠2秒
  71. }
  72. catch(InterruptedException e) {
  73. //e.printStackTrace();
  74. catch(Exception ex){
  75. printStackTrace();
  76. }
  77. }
  78. }
  79. }

 

28.wait()和sleep()的区别

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

 

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

 

在调用sleep()方法的过程中,线程不会释放对象锁。

 

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

 

 

29. foreach与正常for循环效率对比

该条内容并不确定,也没有查到可靠信息,请大家自行判断

<!--for在进行ArrayList循环的情况下优于foreach循环

foreach在进行LinkList循环的情况下优于for循环-->

 

30.Java IO与NIO

Java.nio提出了新的流(stream)通讯概念并且加入了新的缓冲、文件流以及socket(套接字)特性。

java.io 概览

这个包通过数据流和序列化机制来实现系统输入和输出。并且支持多种类型的数据流,包括简单的字节、原生数据类型、地区字符以及对象。流是一个数据的序列:一个程序使用输入流从一个源头读取数据;

另一个程序使用输出流写入并发送数据到目的地。

这些程序都使用字节流来执行字节的输入和输出。所有涉及字节流的类都是继承自InputStream和OutputStream。

关于 InputStream 和 OutputStream

执行InputStream和OutputStream的操作一般都意味着不断循环的将字节逐一从输入流读出或写入到输出流。你可以使用缓冲I/O流来降低I/O成本(凡是I/O请求都经常触发磁盘访问、网络动作或其他一些成本昂贵的操作)。缓冲输入流则是从缓冲的内存区域读取数据,只有缓冲读完才会调用native input API(不同操作系统提供的本地输入流API——译者注)。同样的,缓冲输出流也将数据写入缓冲,只有缓冲写满才会调用native output API。这些带缓冲的API很好的封装了未缓冲的流操作: BufferedInputStream 和 BufferedOutputStream.

File I/O

上面一节主要是针对数据流,它提供一种数据读取和写入的简单模型。真实的数据流其实是涉及种类繁多的数据源和目的地,包括磁盘文件。但是,数据流并不支持所有磁盘文件操作。下面的链接介绍了非数据流的文件I/O:

  • File类可以编写平台无关的检查和处理文件、目录的代码。
  • Random access files支持非序列化的磁盘文件数据访问。

java.net socket

两个在网络上运行的程序之间会建立双向通讯的链接,socket就是其中一个端点。Socket相关的类代表着客户端程序和服务端程序之间的连接。java.net包提供了两个类:Socket和ServerSocket。它们分别实现了连接的客户端和服务端。

客户端知道服务端运行机器的域名,以及服务器监听的端口,它尝试连接到服务器,如果一切正常,服务器接受并建立连接。当接受连接时,服务器在监听端口上绑定一个新的socket,并且通知远程端点设置客户端的地址和端口。之所以要建立一个新的socket是为了处理已连接客户端请求的同时还能继续监听原始socket上的连接请求。

服务器使用阻塞模式等待客户端连接:serverSocket.accept()是一个阻塞指令,当服务器等待接受连接时主线程不能做任何其他操作。由于这个原因,服务器想要达到多任务处理就只能通过实现一个多线程服务器:每当新建一个socket时就必须为它创建一个新线程。

NIO API

I/O性能经常是一个现代应用的痛处。操作系统持续优化改进I/O性能,JVM也提供了一套运行环境帮助Java程序员规避了绝大多数操作系统I/O之间的差异。这些都让I/O相关编码更加高效和简单,但是却隐藏了操作系统的功能特性。想要增强I/O性能,其实你可以通过一些特殊的编码直接访问操作系统的底层功能,但是这并不是最佳解决方案——你的代码将必须依赖某个操作系统。 Java.nio包应运而生来解决这个难题,它提供了高性能的I/O特性,并支持当今大多数常用的商用操作系统。

JDK1.4的NIO包介绍了一系列新的I/O操作的抽象概念。

java.nio 概览

Java.nio这个新增的包实现了Java平台新的 I/O API。NIO API 包含如下特性:

  • 原生类型数据缓冲
  • 字符集的编码器和解码器
  • 基于Perl风格正则表达式的模式匹配
  • 通道(Channel),一种新的原生I/O抽象概念
  • 支持锁和内存映射的文件接口
  • 通过多路复用、非阻塞的I/O能力实现可伸缩的服务器架构

在SUN(现Oracle)的站点上可以找到java.nio的详细技术文档。这里我将解释一些nio的概念,并且和老的java.io库做下比较。建议不要把java.nio当作java.io的替代品,即使它是java.io的“扩展”。Nio的诞生导致了整个I/O类和接口的重新修订(详情请看)。
NIO中一个最重要的概念是在非阻塞模式下运行,与传统Java I/O类库完全不同。什么是非阻塞模式?

Non blocking mode 非阻塞模式

一个I/O流的字节必须序列化的访问。各种设备,打印机端口、网络连接等都是常见的例子。
数据流通常比阻塞式设备慢,而且经常断断续续。大多数操作系统允许将数据流设置为非阻塞模式,允许进程检查是否流上是否有可用数据,即使没有也不会导致进程阻塞。这种机制能让进程在输入流空闲等待时执行其他逻辑,但是数据到达时又能及时处理。操作系统能够观察一堆的数据流并且指示其中哪些处于可用状态。通过观察这些可用状态,一个进程可以采用常用的编码和单个线程就能同时处理多个活动的数据流。这在网络服务器处理大量网络连接时被广泛使用。

Buffers 缓冲

从简单的入手,首先的改进是一系列java.io包中新建的缓冲类。这些缓冲提供了一个可以在内存容器中存储一堆原生类型数据的机制。一个缓冲对象是一个固定容量的容器,容器中的数据可以被读写。

所有的缓冲都是可读的,但并非都是可写的。每个缓冲类都实现了isReadOnly()方法来表示缓冲内容是否允许被修改。

Channels

缓冲需要配合Channel来使用。Channel是I/O传输的入口,缓冲则是数据传输的源头或目的地。在写入时,你想发送的数据首先被放入缓冲,接着被传递至一个Channel;在读取时,Channel负责接收数据,然后寄存至你提供的缓冲中。(比如在网络传输时的过程:用户数据——>发送端缓冲——>发送端Channel——>网络传输——>接收端Channel——>接收端缓冲——>用户数据,译者注)

Channel就像一个管道一样,将数据在管道两端的字节缓冲之间进行高效率的传输。它就像是一个网关,通过它可以用最小的成本来访问操作系统本地的I/O服务,而缓冲则是在两端内部的端点,Channel使用它来发送和接收数据。

Channel能在非阻塞模式下运行。一个非阻塞模式下的Channel不会让调用线程睡眠。请求的操作要么立刻完成、要么返回一个结果告知什么都没做。只有面向数据流的Channel,比如socket,能够在非阻塞模式下运行。在java.nio中的Channel包括FileChannel、ServerSocketChannel以及SocketChannel;这些都是为文件和socket管理所提供的特定的Channel。

FileChannel

FileChannel是可读可写的Channel,它必须阻塞,不能用在非阻塞模式中。面向数据流I/O的非阻塞风格并不适合面向文件的操作,因为文件I/O有本质上的区别。

FileChannel对象不能被直接创建。一个FileChannel实例只能通过在打开的文件对象(RandomAccessFile、FileInputStream、或FileOutputStream)上调用getChannel()得到。GetChannel()方法返回一个连接到相同文件的FileChannel对象,与文件对象拥有相同的访问权限。FileChannel对象是线程安全的。多线程能够并发的调用同一个实例上的方法而不会导致任何问题,但是并非所有操作是支持多线程的。影响Channel位置或者文件大小的操作必须是单线程的。

使用FileChannel,可以让拷贝等操作变成Channel到Channel的传输(transferTo()和transferFrom()方法),而且读写操作更易于使用缓冲。

SocketChannel

SocketChannel与FileChannel不同:新的Socket Channel能在非阻塞模式下运行并且是可选择的。不再需要为每个socket连接指派线程了。使用新的NIO类,一个或多个线程能管理成百上千个活动的socket连接,只用到很小甚至0的性能损失。使用Selector对象可以选择可用的Socket Channel。

有3个Socket Channel类型:SocketChannel, ServerSocketChannel, 以及 DatagramChannel; SocketChannel和DatagramChannel是可读可写的,ServerSocketChannel 监听到来的连接,并且创建新的SocketChannel 对象。所有这些SocketChannel初始化时会创建一个同等的socket对象(java.net sockets)。这个同等的socket能从Channel对象上调用socket()方法获取。每个Socket Channel (in java.nio.channels)对象拥有一个相关的java.net.socket对象,反之并非所有的socket对象拥有相关的Channel对象。如果你使用传统方式创建一个socket对象,直接初始化它,它将不会拥有一个相关的Channel对象,它的getChannel()方法会返回null。

Socket Channel能在非阻塞模式下运行。传统Java Socket阻塞的本性曾经是影响Java应用可伸缩性的罪魁祸首之一。而基于非阻塞I/O则构建了很多复杂巧妙的、高性能的应用。设置或重置Channel的阻塞模式很简单,只要调用configureBlocking()即可。

非阻塞socket常在服务器端使用因为它能让同时管理多个socket变得简单。

Selector 选择器

选择器(Selector)提供了挑选可用状态Channel的能力,从而实现多路复用的I/O。我下面的例子将很好的解释选择器的优势:

设想你在一个火车站(non-selector,非选择器场景),有3个平台 (channels), 每个平台都有火车到达(buffer,缓冲)。每个平台上都有个管理员管理着到达的列车(worker thread,工作线程)。这是非选择器的场景。现在设想下选择器场景。有3个平台(channels),每个平台会有火车到达(缓冲),每个平台都有个指示器(比如一个响铃)指示着“火车到达”(selection key)。在这个场景下只有一个管理员就可以管理所有的3个平台。他只需查看指示器(selector.select())来判断是否有火车到达然后去处理一下到达的列车。

理解selector场景的优势很简单:一个单线程就可以实现多重任务处理的应用。除此之外,使用非阻塞选择器还能得到更多好处!设想火车管理员看着指示器时:他可以等待新的列车到达而不做其他事(使用selector.select()的阻塞模式);也可以在等待新的列车到达时(非阻塞模式使用selector.selectNow())去卖车票,这样selector就会返回null让线程执行其他代码逻辑。

IO vs. NIO

NIO使得I/O比传统I/O更加高效。在一个I/O操作占很高比例的程序中,想想看会有什么不同。比如,如果一个应用需要使用socket拷贝文件或者传输字节,使用NIO可能会得到更快的性能,因为它比I/O的API更接近操作系统。随着数据量的增大,性能提升将更可观。除了io API里,NIO还为数据流操作提供了其他的功能特性。但是,不可能用NIO取代IO,因为NIO的API是基于java.io之上扩展的功能。NIO通过扩展本地的IO API,为广大开发者使用更加强大的方式操作数据流带来了新的可能性。

31.JAVA反射机制

那么什么是Java的反射呢?

大家都知道,要让Java程序能够运行,那么就得让Java类要被Java虚拟机加载。Java类如果不被Java虚拟机加载,是不能正常运行的。现在我们运行的所有的程序都是在编译期的时候就已经知道了你所需要的那个类的已经被加载了。

Java的反射机制是在编译并不确定是哪个类被加载了,而是在程序运行的时候才加载、探知、自审。使用在编译期并不知道的类。这样的特点就是反射。

 

那么Java反射有什么作用呢?

假如我们有两个程序员,一个程序员在写程序的时候,需要使用第二个程序员所写的类,但第二个程序员并没完成他所写的类。那么第一个程序员的代码能否通过编译呢?这是不能通过编译的。利用Java反射的机制,就可以让第一个程序员在没有得到第二个程序员所写的类的时候,来完成自身代码的编译。

 

Java的反射机制它知道类的基本结构,这种对Java类结构探知的能力,我们称为Java类的“自审”。大家都用过Jcreator和eclipse。当我们构建出一个对象的时候,去调用该对象的方法和属性的时候。一按点,编译工具就会自动的把该对象能够使用的所有的方法和属性全部都列出来,供用户进行选择。这就是利用了Java反射的原理,是对我们创建对象的探知、自审。

 

 

Class

       要正确使用Java反射机制就得使用java.lang.Class这个类。它是Java反射机制的起源。当一个类被加载以后,Java虚拟机就会自动产生一个Class对象。通过这个Class对象我们就能获得加载到虚拟机当中这个Class对象对应的方法、成员以及构造方法的声明和定义等信息。

 

 

反射API

 

       u反射API用于反应在当前Java虚拟机中的类、接口或者对象信息

u功能
获取一个对象的类信息.

       —获取一个类的访问修饰符、成员、方法、构造方法以及超类的信息.

       —检获属于一个接口的常量和方法声明.

       —创建一个直到程序运行期间才知道名字的类的实例.

       —获取并设置一个对象的成员,甚至这个成员的名字是
在程序运行期间才知道.

       —检测一个在运行期间才知道名字的对象的方法

 

利用Java反射机制我们可以很灵活的对已经加载到Java虚拟机当中的类信息进行检测。当然这种检测在对运行的性能上会有些减弱,所以什么时候使用反射,就要靠业务的需求、大小,以及经验的积累来决定。

 

使用反射机制的步骤:

u导入java.lang.relfect 

u遵循三个步骤
第一步是获得你想操作的类的 java.lang.Class 对象
第二步是调用诸如 getDeclaredMethods 的方法
第三步使用 反射API 来操作这些信息

 

32.JAVA泛型

JAVA泛型用类型擦除来实现的,仅仅存在于程序源码当中,在编译后的字节码文件中,就已经替换为原来的原生类型,并且在相应的地方插入了强制转型代码。基于这种方法实现的泛型成为伪泛型。

 

33.解析XML的几种方式的原理与特点

1.DOM生成和解析XML文档

为 XML 文档的已解析版本定义了一组接口。解析器读入整个文档,然后构建一个驻留内存的树结构,然后代码就可以使用 DOM 接口来操作这个树结构。

优点:整个文档树在内存中,便于操作;支持删除、修改、重新排列等多种功能;

缺点:将整个文档调入内存(包括无用的节点),浪费时间和空间;使用场合:一旦解析了文档还需多次访问这些数据;硬件资源充足(内存、CPU)。

2.SAX生成和解析XML文档

为解决DOM的问题,出现了SAX。SAX ,事件驱动。当解析器发现元素开始、元素结束、文本、文档的开始或结束等时,发送事件,程序员编写响应这些事件的代码,保存数据。

优点:不用事先调入整个文档,占用资源少;SAX解析器代码比DOM解析器代码小,适于Applet,下载。

缺点:不是持久的;事件过后,若没保存数据,那么数据就丢了;无状态性;从事件中只能得到文本,但不知该文本属于哪个元素;使用场合:Applet;只需XML文档的少量内容,很少回头访问;机器内存少;

3.DOM4J生成和解析XML文档

DOM4J 是一个非常非常优秀的Java XML API,具有性能优异、功能强大和极端易用使用的特点,同时它也是一个开放源代码的软件。如今你可以看到越来越多的 Java 软件都在使用 DOM4J 来读写 XML,特别值得一提的是连 Sun 的 JAXM 也在用 DOM4J。

4.JDOM生成和解析XML

为减少DOM、SAX的编码量,出现了JDOM;优点:20-80原则,极大减少了代码量。使用场合:要实现的功能简单,如解析、创建等,但在底层,JDOM还是使用SAX(最常用)、DOM、Xanan文档。

34.C++和JAVA对比

1) 概述

Java其实也是由C++发展而来,保留了C++的大部分内容,其编程方式类似于C++,但是摒弃了C++的诸多不合理之处,从根本上解决了C++的固有缺陷。使得Java句法更清晰,规模更小,更易学,同时更趋于健壮性,安全性和平台无关性。

a.全局变量:C++将函数和变量定义为全局的,而不加封装,增加了程序的负担 ,并且往往会由于使用不当而造成系统的崩溃。

b.Java是完全面向对象的语言,类将方法和数据封装在其内,不能在所 用的类之外定义程序的全局变量,只能通过在一个类中定义public static的变量来实现一个全局变量,使得其它类可以访问和修改该变量。这种完善的包装保证了系统的安全性。

2) goto语句

a.goto语句一般用于无条件转移子程序和多结构分支技术,是C++中的合法语句,造成了程序结构的混乱,不易理解。

b.Java不提供goto语句,使得程序更简洁易读,增强了程序的健壮性。

3) 指针

a.指针是C++语言中最灵活也最容易出错的数据类型,易出现由于指针误操作而导致的系统崩溃,同时指针操作内存时也经常出错。

b.Java没有指针的概念,更有利于程序的安全。

4) 内存管理

a.C++语言中必须通过程序释放内存资源,增加了程序设计者的负担,再次释放已释放的内存块或释放未分配的内存块会造成系统崩溃,忘记释放不再使用的内存块也会逐渐耗尽系统资源。

b.Java自动进行内存回收操作,当一个对象不再被用到时,无须使用内存回收器,只需要给它加上标签以示删除。无用内存的回收器在后台运行,利用空闲时间工作,保证了系统资源的完整性,避免了内存管理不周而引起的系统崩溃。

5) 数据类型的一致性

a.在C++语言中,不同的平台上,编译器对简单数据类型分别分配不同的字节数,导致了代码数据的不可移植性。

b.在Java中,采用基于IEEE标准的数据类型,无论任何硬件平台上对数据类型的位数分配总是固定的。

6) 类型转换

a.在C++中,会出现数据类型的隐含转换,涉及到自动强制类型转换,使得不安全因素大大增加。

b.Java中系统要对对象的处理进行严格的相容性检查,防止不安全的转换。如果需要,必须由程序显式进行强制类型转换。

7) 头文件

a.在C++语言中使用头文件声明类的原型和全局变量及库函数等,使得在大系统中对头文件的维护非常困难。

b.Java不支持头文件,类成员的类型和访问权限都封装在类中,运行时 系统对访问进行控制,防止非法访问。

8) 结构和联合

a.C++中用结构和联合来表示一定的数据结构,其成员的公有性带来了安全隐患。

b.Java不支持结构和联合,通过类把数据结构及对该数据的操作封装在类中。

9) 预处理

a.C++在编译过程中都有一个预编译阶段,即预处理器,为开发人员提供了方便,但也增加了编译的复杂性。

b.Java允许预处理,但不支持预处理器功能,提供import语句实现类似 的功能。

10) 多重继承

a.C++支持多重继承,允许许多父类派生一个子类,虽然功能强大,但使用复杂,而且会引起许多麻烦,编译程序实现也很不易。
b.Java不支持多重继承,但允许一个类实现多个接口,即实现了C++的多重继承功能,又避免了C++的缺陷。

11) 操作符重载

a.操作符重载被认为是C++的突出特征。

b.为了保持Java语言尽可能的简单,Java不支持操作符重载。

12) 函数

a.在C中,代码组织在函数中,函数可以访问程序的全局变量; C++增加了类,提供了类方法,但由于C++仍然支持C,所以C++程序中仍然可以使用C的函数,结果导致函数和方法混合使用,使得程序比较混乱 。

b.Java没有函数。作为一种比C++更纯的面向对象的语言,强迫开发人员把所有例行程序包括在类中,可以更好的组织编码。

13) 字符串

a.C++不支持字符串变量,使用“Null”终止符代表字符串的结束。

b.Java字符串类作为Java语言的一部分定义,而不是作为外加的延伸部分,在整个系统中建立字符串和访问字符串元素的方法是一致的。

 

35.Java1.7新特性

Java 7 的架构图:

新特性一览表:

Swing

网络

集合

RIA/发布

XML

java.lang

Java 虚拟机

Java I/O

java.nio.file 包以及相关的包 java.nio.file.attribute 提供对文件 I/O 以及访问文件系统的全面支持,请看 File I/O (featuring NIO.2).

  • 目录<Java home>/sample/nio/chatserver/ 包含使用nio.file 包的演示程序
  • 目录<Java home>/demo/nio/zipfs/ 包含2 NFS 文件系统的演示程序

安全性

并发

  • fork/join 框架,基于ForkJoinPool 类,是 Executor 接口的实现,设计它用来进行高效的运行大量任务;使用 work-stealing 技术用来保证大量的 worker 线程工作,特别适合多处理器环境,详情请看 Fork/Join
    • 目录<Java home>/sample/forkjoin/包含了 fork/join 框架的演示程序
  • ThreadLocalRandom类class 消除了使用伪随机码线程的竞争,请看 Concurrent Random Numbers.
  • Phaser类是一个新的同步的屏障,与 CyclicBarrier 类似.

Java 2D

国际化

  • 支持Unicode 6.0.0
    • 目录<Java home>/demo/jfc/Font2DTest/ 包含 Unicode 6.0 的演示程序
    • Java SE 7 可容纳在 ISO 4217 中新的货币,详情请看Currency 类.

Java 编程语言特性

JDBC 4.1

  • 支持使用try-with-resources 语句进行自动的资源释放,包括连接、语句和结果集
  • 支持 RowSet 1.1

 

36.Java1.8新特性

http://www.open-open.com/lib/view/open1403232177575.html

 

37 常用设计模式总结

一 : 单例模式(Singleton)

单例模式:Singleton的作用是保证在应用程序中,一个类Class只有一个实例存在。并提供全局访问。

结构:

账本类:1 单一实例 2 给多个对象共享 3 自己创建

网页计数器
public class LazySingleton

{
private static LazySingleton newInstance = null;

private LazySingleton ()

{

}

public static synchronized  LazySingleton getInstance ()

{
if (newInstance == null)

{
newInstance = new LazySingleton ();
}
return newInstance;
}

}
singleton限制了实例个数,有利于gc的回收。

二:策略模式(Strategy)

策略模式:策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模式把行为和环境分开。环境类负责维持和查询行为类,各种算法在具体的策略类中提供。由于算法和环境独立开来,算法的增减,修改都不会影响到环境和客户端。

结构:

使用QQMM时使用外挂  客户端 ME 抽象类: 外挂 具体:策略(图片,笑话,名人名言)

图书销售算法(不同书本折扣的算法)

三:原型模式(Prototype)

原型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法

结构:

复印技术: 1 不是同一个对象 2 属同类

短消息(转发) 1-nMM

因为Java中的提供clone()方法来实现对象的克隆,所以Prototype模式实现一下子变得很简单.

四:外观模式(Façade)

门面模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用,减少复杂性。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。

1 门面角色 2 子系统角色

结构:

Facade典型应用就是数据库JDBC的应用和Session的应用

ME---àMM---à(father,mum,sister,brother)

五:备忘录模式(Memento)

Memento模式:Memento对象是一个保存另外一个对象内部状态拷贝的对象,这样以后就可以将该对象恢复到原先保存的状态。模式的用意是在不破坏封装的条件下,将一个对象的状态捕捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。模式所涉及的角色有三个,备忘录角色、发起人角色和负责人角色。

备忘录角色的作用:

(1) 将发起人对象的内部状态存储起来,备忘录可以根据发起人对象的判断来决定存储多少发起人对象的内部状态。

(2) 备忘录可以保护其内容不被发起人对象之外的任何对象所读取。

发起人角色的作用:

(1) 创建一个含有当前内部状态的备忘录对象。

(2) 使用备忘录对象存储其内部状态。

负责人角色的作用:

(1) 负责保存备忘录对象。

(2) 不检查备忘录对象的内容

结构:

备份系统时使用

GHOST

六 : 命令模式(Command)

命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。

结构:

 

MM(客户端)--àME(请求者)--à命令角色--à(具体命令)代理处(接收者)--àMM

上网 IE 输入 http地址 发送命令

七: 解释器(Interpreter)

解释器模式:给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后,使用模式设计解释这些语句。在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构,也就是一系列的组合规则。每一个命令对象都有一个解释方法,代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。

结构:

编译原理之编译器

文言文注释:一段文言文,将它翻译成白话文

八:中介者模式(Mediator)

调停者模式:包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使他们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化,把对象在小尺度的行为上与其他对象的相互作用分开处理。

结构:

法院和原告,被告的关系

九:责任链模式(CHAIN OF RESPONSIBLEITY)

责任链模式:执行者的不确定性 在责任链模式中,很多对象由每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。

结构:

典型的对象结构:

喝酒时通过成语接龙决定谁喝酒(马到成功-功不可没-没完没了)

十:工厂模式(Factory)

工厂模式定义一个用于创建对象的接口,让接口子类通过工厂方法决定实例化哪一个类。

结构:

水果园〉(葡萄园,苹果园)--〉(葡萄,苹果)(各自生产)

十一:抽象工厂模式(Abstract Factory)

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

结构:

女娲造人---〉(阴,阳)--〉(人,兽)----〉(男人,女人,公兽,母兽)(人和兽属于不同的产品类)

十二:建造模式(Builder)

建造模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示.Builder模式是一步一步创建一个复杂的对象,它允许用户可以只通过指定复杂对象的类型和内容就可以构建它们.用户不知道内部的具体构建细节.Builder模式是非常类似抽象工厂模式,细微的区别大概只有在反复使用中才能体会到。

将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。

结构:

交互图:

 

 

 

汽车制造

十三:合成模式(Composite)

合成模式将对象以树形结构组织起来,以达成“部分-整体” 的层次结构,使得客户端对单个对象和组合对象的使用具有一致性. 合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。

结构:

windows的目录树(文件系统)

十四:装饰模式(DECORATOR)

装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。

使用Decorator的理由是:这些功能需要由用户动态决定加入的方式和时机.Decorator提供了"即插即用"的方法,在运行期间决定何时增加何种功能.

结构:

visio中文件可以使用背景进行装饰

变废为宝

十五:设计模式之Adapter(适配器)

适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端
将两个不兼容的类纠合在一起使用,属于结构型模式,需要Adaptee(被适配者)和Adaptor(适配器)两个身份.

为何使用?
我们经常碰到要将两个没有关系的类组合在一起使用,第一解决方案是:修改各自类的接口,但是如果我们没有源代码,或者,我们不愿意为了一个应用而修改各自的接口。 怎么办? 使用Adapter,在这两种接口之间创建一个混合接口(混血儿).

如何使用?
实现Adapter方式,其实"think in Java"的"类再生"一节中已经提到,有两种方式:组合(composition)和继承(inheritance).

结构:

对象结构:

充电器(手机和220V电压)

jdbc-odbc

十六:桥梁模式(Bridge)

桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化。也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化。

结构:

jdbc驱动程序

十七:代理模式(Proxy)

代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。

结构:

运行时的代理结构:

用代理服务器连接出网

销售代理(厂商)律师代理(客户)

foxmail

枪手

十八:享元模式(Flyweight)

享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。

结构:

共享方法:

字体的26个字母和各自的斜体等

十九:状态模式(State)

状态模式:状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。

结构:

人心情不同时表现不同有不同的行为

编钟

登录login logout

二十:观察者模式(Observer)

观察者模式:观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。发布订阅。

结构:

公司邮件系统everyone@sina.com的应用。当公司员工向这个邮箱发邮件时会发给公司的每一个员工。如果设置了Outlook则会及时收到通知。

接收到短消息

二十一:模板方法模式(Template)

模板方法模式:模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。

结构:

使用网页设计时使用的模板架构网页(骨架) 算法的各个逻辑系统

二十二:访问者模式(Visitor)

访问者模式:访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易,就是增加一个新的访问者类。访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。当使用访问者模式时,要将尽可能多的对象浏览逻辑放在访问者类中,而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。

结构:

电脑销售系统: 访问者(自己)---〉电脑配置系统(主板,CPU,内存。。。。。。)

二十三:迭代子模式(Iterator)

迭代子模式:迭代子模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。迭代子模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。

结构:

查询数据库,返回结果集(map list set

二十四:MVC模式

MVC模式:它强制性的使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心部件:模型、视图、控制器。它们各自处理自己的任务。相互通信。

MVC还使用了的设计模式,如:用来指定视图缺省控制器的Factory Method和用来增加视图滚动的Decorator。但是MVC的主要关系还是由Observer、Composite和Strategy三个设计模式给出的。

 

struts图解:其中不同颜色代表MVC的不同部分:红色(控制器)、紫色(模型)和绿色(视图)

struts应用 spring 应用

 

38.JNI的使用

http://www.cnblogs.com/mandroid/archive/2011/06/15/2081093.html

JVM

1. 内存模型以及分区

JVM内存模型如下图所示:

此处我们集中注意中间绿色的部分,该部分为JVM的运行时内存,该部分包含了:

线程私有的(灰色):

  1. 程序计数器:记录执行到第几条指令
  2. 虚拟机方法栈:执行Java方法所用,每执行一个方法便加入一个栈帧,里面含有局部变量表、操作栈、动态链接和方法出口等
  3. 本地方法栈:与虚拟机方法栈相似,用于执行native方法

线程共享的(蓝色):

  1. 堆:对象实例存放地,,分为年轻代和老年代。年轻代又细分为伊甸园区和两个相对的Survival区
  2. 方法区:也叫永久代,存放了类的信息、常量、类静态变量等元素,含有一个运行时常量池

 

 

 

 

 

 

 

 

 

我们现在来逐个的看下每个到底是做什么的!

1、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

2、Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时

 

创建一个栈帧(Stack Frame )用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

经常有人把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java 内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。其中64 位长度的long 和double 类型的数据会占用2 个局部变量空间(Slot),其余的数据类型只占用1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

 

在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。

3、本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

4、Java 堆

对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage

Collected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread LocalAllocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java 堆中的上述各个区域的分配和回收等细节将会是下一章的主题。根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

5、方法区

方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。

 

对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。

5、运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。

既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常

6、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

 

2. 堆里面的分区:Eden,survival from to,老年代,各自的特点

 

1.Eden区

Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。

 

2.Survival from to

Survival区与Eden区相同都在Java堆的年轻代。Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survival from区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代。

 

3.年老代

年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。

 

3. 对象创建方法,对象的内存分配,对象的访问定位

对象的创建

Java对象的创建大致上有以下几个步骤:

  1. 类加载检查:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程
  2. 为对象分配内存:对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。由于堆被线程共享,因此此过程需要进行同步处理(分配在TLAB上不需要同步)
  3. 内存空间初始化:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 对象设置:JVM对对象头进行必要的设置,保存一些对象的信息(指明是哪个类的实例,哈希码,GC年龄等)
  5. init:执行完上面的4个步骤后,对JVM来说对象已经创建完毕了,但对于Java程序来说,我们还需要对对象进行一些必要的初始化。

对象的内存分配

Java对象的内存分配有两种情况,由Java堆是否规整来决定(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定):

  1. 指针碰撞(Bump the pointer):如果Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离
  2. 空闲列表(Free List):如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录

对象的访问定位

对象的访问形式取决于虚拟机的实现,目前主流的访问方式有使用句柄和直接指针两种:

使用句柄:

如果使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息:

优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针:

如果使用直接指针访问对象,那么对象的实例数据中就包含一个指向对象类型数据的指针,引用中存的直接就是对象的地址:

优势:速度更快,节省了一次指针定位的时间开销,积少成多的效应非常可观。

 

4. GC的两种判定方法:引用计数与引用链

基于引用计数与基于引用链这两大类别的自动内存管理方式最大的不同之处在于:前者只需要局部信息,而后者需要全局信息

 

引用计数

引用计数顾名思义,就是记录下一个对象被引用指向的次数。引用计数方式最基本的形态就是让每个被管理的对象与一个引用计数器关联在一起,该计数器记录着该对象当前被引用的次数,每当创建一个新的引用指向该对象时其计数器就加1,每当指向该对象的引用失效时计数器就减1。当该计数器的值降到0就认为对象死亡。每个计数器只记录了其对应对象的局部信息——被引用的次数,而没有(也不需要)一份全局的对象图的生死信息。由于只维护局部信息,所以不需要扫描全局对象图就可以识别并释放死对象;但也因为缺乏全局对象图信息,所以无法处理循环引用的状况。

 

引用链

引用链需要内存的全局信息,当使用引用链进行GC时,从对象图的“根”(GC Root,必然是活的引用,包括栈中的引用,类静态属性的引用,常量的引用,JNI的引用等)出发扫描出去,基于引用的可到达性算法来判断对象的生死。这使得对象的生死状态能批量的被识别出来,然后批量释放死对象。引用链不需要显式维护对象的引用计数,只在GC使用可达性算法遍历全局信息的时候判断对象是否被引用,是否存活。

 

5. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方

标记清除

标记清除算法分两步执行:

  1. 暂停用户线程,通过GC Root使用可达性算法标记存活对象
  2. 清除未被标记的垃圾对象

标记清除算法缺点如下:

  1. 效率较低,需要暂停用户线程
  2. 清除垃圾对象后内存空间不连续,存在较多内存碎片

标记算法如今使用的较少了

 

复制算法

复制算法也分两步执行,在复制算法中一般会有至少两片的内存空间(一片是活动空间,里面含有各种对象,另一片是空闲空间,里面是空的):

  1. 暂停用户线程,标记活动空间的存活对象
  2. 把活动空间的存活对象复制到空闲空间去,清除活动空间

复制算法相比标记清除算法,优势在于其垃圾回收后的内存是连续的。

但是复制算法的缺点也很明显:

  1. 需要浪费一定的内存作为空闲空间
  2. 如果对象的存活率很高,则需要复制大量存活对象,导致效率低下

复制算法一般用于年轻代的Minor GC,主要是因为年轻代的大部分对象存活率都较低

 

标记整理

标记整理算法是标记清除算法的改进,分为标记、整理两步:

  1. 暂停用户线程,标记所有存活对象
  2. 移动所有存活对象,按内存地址次序一次排列,回收末端对象以后的内存空间

标记整理算法与标记清除算法相比,整理出的内存是连续的;而与复制算法相比,不需要多片内存空间。

然而标记整理算法的第二步整理过程较为麻烦,需要整理存活对象的引用地址,理论上来说效率要低于复制算法。

因此标记整理算法一般引用于老年代的Major GC

 

6. GC收集器有哪些?它们的特点是?

常见的GC收集器如下图所示,连线代表可搭配使用:

1.Serial收集器(串行收集器)

用于新生代的单线程收集器,收集时需要暂停所有工作线程(Stop the world)。优点在于:简单高效,单个CPU时没有线程交互的开销,堆较小时停顿时间不长。常与Serial Old 收集器一起使用,示意图如下所示:

2.ParNew收集器(parallel new 收集器,新生代并行收集器)

Serial收集器多线程版本,除了使用多线程外和Serial收集器一模一样。常与Serial Old 收集器一起使用,示意图如下:

 

3.Parallel Scavenge收集器

与ParNew收集器一样是一款多线程收集器,其特点在于关注点与别的GC收集器不同:一般的GC收集器关注于缩短工作线程暂停的时间,而该收集器关注于吞吐量,因此也被称为吞吐量优先收集器。(吞吐量 = 用户运行代码时间 /  (用户运行代码时间 + 垃圾回收时间))高吞吐量与停顿时间短相比主要强调任务快完成,因此常和Parallel Old 收集器一起使用(没有Parallel Old之前与Serial Old一起使用),示意图如下:

 

4.Serial Old收集器

Serial收集器的年老代版本,不在赘述。

 

5.Parallel Old收集器

年老代的并行收集器,在JDK1.6开始使用。

 

6.CMS收集器(Concurrent Mark Sweep,并发标记清除收集器)

CMS收集器是一个年老代的收集器,是以最短回收停顿时间为目标的收集器,其示意图如下所示:

CMS收集器基于标记清除算法实现,主要分为4个步骤:

  1. 初始标记,需要stop the world,标记GC Root能关联到的对象,速度快
  2. 并发标记,对GC Root执行可达性算法
  3. 重新标记,需要stop the world,修复并发标记时因用户线程运行而产生的标记变化,所需时间比初始标记长,但远比并发标记短
  4. 并发清理

CMS收集器的缺点在于:

  1. 其对于CPU资源很敏感。在并发阶段,虽然CMS收集器不会暂停用户线程,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量降低。其默认启动的回收线程数是(cpu数量+3)/4,当cpu数较少的时候,会分掉大部分的cpu去执行收集器线程
  2. 无法处理浮动垃圾,浮动垃圾即在并发清除阶段因为是并发执行,还会产生垃圾,这一部分垃圾即为浮动垃圾,要等下次收集
  3. CMS收集器使用的是标记清除算法,GC后会产生碎片

7.G1收集器(Garbage First收集器)

相比CMS收集器,G1收集器主要有两处改进:

  1. 使用标记整理算法,确保GC后不会产生内存碎片
  2. 可以精确控制停顿,允许指定消耗在垃圾回收上的时间

G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

 

 

7. Minor GC与Full GC分别在什么时候发生?

Minor GC也叫Young GC,当年轻代内存满的时候会触发,会对年轻代进行GC

Full GC也叫Major GC,当年老代满的时候会触发,当我们调用System.gc时也可能会触发,会对年轻代和年老代进行GC

 

8. 类加载的五个过程:加载、验证、准备、解析、初始化

JVM把class文件加载的内存,并对数据进行校验、转换解析和初始化,最终形成JVM可以直接使用的Java类型的过程就是加载机制。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。

1.加载

在加载阶段,虚拟机需要完成以下事情:

  1. 通过一个类的权限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法去这些数据的访问入口

2.验证

在验证阶段,虚拟机主要完成:

  1. 文件格式验证:验证class文件格式规范
  2. 元数据验证:这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求
  3. 字节码验证:进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
  4. 符号引用验证:符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问

3.准备

准备阶段是正式为类变量(被static修饰的变量)分配内存并设置变量初始值(0值)的阶段,这些内存都将在方法区中进行分配

 

4.解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程

常见的解析有四种:

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

 

5.初始化

初始化阶段才真正开始执行类中定义的java程序代码,初始化阶段是执行类构造器<clinit>()方法的过程

 

9. 双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader

JVM的类加载是通过类加载器实现的, Java中的类加载器体系结构如下:

  • 启动类加载器(Bootstrap ClassLoader):是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
  • 标准扩展类加载器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器
  • 应用程序类加载器:由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器

值得注意的是:上述三个JDK提供的类加载器虽然是父子类加载器关系,但是没有使用继承,而是使用了组合关系。
从JDK1.2开始,JVM规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:

  1. 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成
  2. 每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都传递给顶层的启动类加载器
  3. 如果顶层的启动类加载器无法完成加载请求,则子类加载器会尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载

采用双亲委派模型的好处在于:使得java类随着它的类加载器一起具备了一种带有优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。

 

10. 分派:静态分派与动态分派

要理解分派,我们先来理解Java中的两种类型:

  • 静态类型:变量声明时的类型
  • 实际类型:变量实例化时采用的类型

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是方法重载(重载是通过参数的静态类型而不是实际类型来选择重载的版本的)。

 

变量的实际类型是在运行期确定的,重写方法的调用也是根据实际类型来调用的。我们把这种在运行期根据实际类型来确定方法执行版本的分派动作,称为动态分派。

 

操作系统

1. 进程和线程的区别。

  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
  • 线程是进程的一个实体, 是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
  • 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序 健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

2. 死锁的必要条件,怎么处理死锁。

1、死锁的四个必要条件

互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。

请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。

非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。

循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。

2、处理死锁的策略

1.忽略该问题。例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。

2.检测死锁并且恢复。

3.仔细地对资源进行动态分配,以避免死锁。

4.通过破除死锁四个必要条件之一,来防止死锁产生,最著名的算法首推Dijkstra[1965]提出的银行家(banker)算法。

3. Window内存管理方式:段存储,页存储,段页存储。

1、段存储

段式管理的基本思想是把程序按照内容或过程函数关系分段,每段都有自己的名字。一个用户作业或进程所包括的段对应一个二维线形虚拟空间,也就是一个二维虚拟存储器。段式管理程序以段为单位分配内存,然后通过地址映射机构把段式虚拟地址转换为实际内存物理地址。其优点是可以分别编写和编译,可以针对不同类型的段采用不同的保护,可以按段为单位来进行共享,包括通过动态链接进行代码共享。缺点是会产生碎片。

2、页存储

页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页;页式管理把内存空间按照页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应的页表;并用相应的硬件地址变换机构来解决离散地址变换问题。页式管理采用请求调页或预调页技术来实现内外存存储器的统一管理。其优点是没有外碎片,每个内碎片不超过页的大小。缺点是,程序全部装入内存,要求有相应的硬件支持。例如地址变换机构缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本,增加了系统开销。

3、段页式管理:

为了实现段页式管理,系统必须为每个作业或进程建立一张段表以管理内存分配与释放、缺段处理等。另外由于一个段又被划分成了若干个页。每个段必须建立一张页表以把段中的虚页变换成内存中的实际页面。显然与页式管理时相同,页表中也要有相应的实现缺页中断处理和页面保护等功能的表项。段页式管理的段式管理与页式管理方案结合而成的所以具有他们两者的优点。但反过来说,由于管理软件的增加,复杂性和开销也就随之增加了。另外需要的硬件以及占用的内存也有所增加。使得速度降下来。

 

4. 进程的几种状态。

如上图所示,进程包括三种状态:就绪态、运行态和阻塞态。详细说明如下:

注意:阻塞和就绪的区别:阻塞是等待除CPU以外的资源,而就绪等待的是CPU资源。

1)就绪——执行:对就绪状态的进程,当进程调度程序按一种选定的策略从中选中一个就绪进程,为之分配了处理机后,该进程便由就绪状态变为执行状态;
2)执行——阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如进程提出输入/输出请求而变成等待外部设备传输信息的状态,进程申请资源(主存空间或外部设备)得不到满足时变成等待资源状态,进程运行中出现了故障(程序出错或主存储器读写错等)变成等待干预状态等等;
3)阻塞——就绪:处于阻塞状态的进程,在其等待的事件已经发生,如输入/输出完成,资源得到满足或错误处理完毕时,处于等待状态的进程并不马上转入执行状态,而是先转入就绪状态,然后再由系统进程调度程序在适当的时候将该进程转为执行状态;
4)执行——就绪:正在执行的进程,因时间片用完而被暂停执行,或在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行而被迫让出处理机时,该进程便由执行状态转变为就绪状态。

5. IPC几种通信方式。

1、为什么要进行进程间的通讯IPC

数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

2、常用的进程间的通讯方式

(1)、管道(pipe):管道可用于具有亲缘关系的进程间的通信,是一种半双工的方式,数据只能单向流动,允许一个进程和另一个与它有共同祖先的进程之间进行通信。

(2)、命名管道(named pipe):命名管道克服了管道没有名字的限制,同时除了具有管道的功能外(也是半双工),它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。

(3)、信号(signal):信号是比较复杂的通信方式,用于通知接收进程有某种事件发生了,除了进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。

(4)、消息队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺

(5)、共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

(6)、内存映射:内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。

(7)、信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

(8)、套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

6. 什么是虚拟内存。

在现在操作系统中,都使用了MMU的存储管理技术,而MMU管理的地址是虚拟地址,虚拟地址=逻辑地址=[段选择子]:[线形地址],利用段选择子找到描述符。虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。

7. 虚拟地址、逻辑地址、线性地址、物理地址的区别。

虚拟地址即逻辑地址,是指由程序产生的与段相关的偏移地址部分。

逻辑地址(Logical Address 是指由程式产生的和段相关的偏移地址部分。例如,你在进行C语言指针编程中,能读取指针变量本身值(&操作),实际上这个值就是逻辑地址,他是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程式执行代码段限长内的偏移地址(假定代码段、数据段如果完全相同)。应用程式员仅需和逻辑地址打交道,而分段和分页机制对你来说是完全透明的,仅由系统编程人员涉及。应用程式员虽然自己能直接操作内存,那也只能在操作系统给你分配的内存段操作。

线性地址(Linear Address 是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。

物理地址(Physical Address 是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

数据结构与算法

1. 链表与数组。

数组和链表的区别

  • 数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。
  • 链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表数据结构了。

2. 队列和栈,出栈与入栈。

  • 相同点:从"数据结构"的角度看,它们都是线性结构,即数据元素之间的关系相同。不同点:栈(Stack)是限定只能在表的一端进行插入和删除操作的线性表。 队列(Queue)是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。它们是完全不同的数据类型。除了它们各自的基本操作集不同外,主要区别是对插入和删除操作的"限定"。

    栈必须按"后进先出"的规则进行操作:比如说,小学老师批改学生的作业,如果不打乱作业本的顺序的话,那么老师批改的第一份作业一定是最后那名同学交的那份作业,如果把所有作业本看作是一个栈中的元素,那么最后一个同学交的作业本就是栈顶元素,而第一个同学交的,也就是最低端的作业本,就是栈底元素,这就是对栈的读取规则。

    而队列必须按"先进先出"的规则进行操作:打个比方,一些人去银行办理业务,一定是先去排队的最先得到服务,当然他也是第一个走出银行的(假设这些人都在一个窗口排队)。如果把所有这些等候服务的人看作是队的元素,第一个人就是对头元素,相应的,最后一个人就是队尾元素。这是队的读取规则。

3. 链表的删除、插入、反向。

http://www.cnblogs.com/scorpioying/archive/2013/02/19/2917282.html 链表的各类操作

4. 字符串操作。

http://blog.csdn.net/yangtrees/article/details/8155041

5. Hash表的hash函数,冲突解决方法有哪些。

1.哈希表的构造方法

1.1直接定址法

取关键字或者关键字的某个线性函数值作为哈希地址,即

H(Key)=Key或者H(Key)=a*Key+b(a,b为整数)

这种散列函数也叫做自身函数.如果H(Key)的哈希地址上已经有值了,那么就往下一个位置找,直到找到H(Key)的位置没有值了就把元素放进去.
此法仅适合于:地址集合的大小 等于 关键字集合的大小

1.2 数字分析法

分析一组数据,比如一组员工的出生年月,这时我们发现出生年月的前几位数字一般都相同,因此,出现冲突的概率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果利用后面的几位数字来构造散列地址,则冲突的几率则会明显降低.
因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址.
此法适于:能预先估计出全体关键字的每一位上各种数字出现的频度。

1.3 平方取中法

以关键字的平方值的中间几位作为存储地址(哈希地址)。求“关键字的平方值” 的目的是“扩大差别” ,同时平方值的中间各位又能受到整个关键字中各位的影响。

此法适于:关键字中的每一位都有某些数字重复出现频度很高的现象。

1.4 折叠法

将关键字分割成若干部分,然后取它们的叠加和为哈希地址。两种叠加处理的方法:移位叠加:将分 割后的几部分低位对齐相加;间界叠加:从一端沿分割界来回折叠,然后对齐相加。

此法适于:关键字的数字位数特别多

1.5随机数法

设定哈希函数为:H(key) = Random(key)其中,Random 为伪随机函数
此法适于:对长度不等的关键字构造哈希函数。

1.6除留余数法

取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址.即
哈希函数为:H(key) = key MOD p ( p≤m ),其中, m为表长,p 为不大于 m 的素数。

2.哈希表冲突解决方法

哈希表处理冲突主要有开放寻址法再散列法链地址法(拉链法)和建立一个公共溢出区四种方法。
通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,因此解决冲突是哈希法的另一个关键问题。
“处理冲突” 的实际含义是:为产生冲突的关键字寻找下一个哈希地址。

2.1开放定址法

一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

2.1.1线性探测

冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

公式:

fi(key) = (f(key)+di) MOD m (di=1,2,3,......,m-1)

2.1.2二次探测法

冲突发生时,在表的左右进行跳跃式探测,双向寻找到可能的空位置。

公式:

fi(key) = (f(key)+di) MOD m (di = 12, -12, 22, -22,……, q2, -q2, q <= m/2

2.1.3随机探测法

在冲突时,对于位移量 di 采用随机函数计算得到,我们称之为随机探测法。

公式:

fi(key) = (f(key)+di) MOD m (di是一个随机数列)

线性探测再散列容易产生“二次聚集”,即在处理同义词的冲突时又导致非同义词的冲突。
线性探测再散列的优点是:只要哈希表不满,就一定能找到一个不冲突的哈希地址,而二次探测再散列和伪随机探测再散列则不一定。

2.2链地址法

将所有哈希地址相同的记录都链接在同一链表中。各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况。
处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

2.3再哈希法

这种方法是同时构造多个不同的哈希函数:

Hi=RH1(key),i=1,2,3,…,n.

当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

2.4建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表.(注意:在这个方法里面是把元素分开两个表来存储)

6. 各种排序:冒泡、选择、插入、希尔、归并、快排、堆排、桶排、基数的原理、平均时间复杂度、最坏时间复杂度、空间复杂度、是否稳定。

http://blog.csdn.net/hguisu/article/details/7776068#t9 c++版本

http://www.codeceo.com/article/8-java-sort.html  java实现

7. 快排的partition函数与归并的Merge函数。

3、划分算法Partition
(1) 简单的划分方法
具体做法
第一步:(初始化)设置两个指针i和j,它们的初值分别为区间的下界和上界,即i=low,i=high;选取无序区的第一个记录R[i](即R[low])作为基准记录,并将它保存在变量pivot中;
第二步:令j自high起向左扫描,直到找到第1个关键字小于pivot.key的记录R[j],将R[j])移至i所指的位置上,这相当于R[j]和基准R[i](即pivot)进行了交换,使关键字小于基准关键字pivot.key的记录移到了基准的左边,交换后R[j]中相当于是pivot;然后,令i指针自i+1位置开始向右扫描,直至找到第1个关键字大于pivot.key的记录R[i],将R[i]移到i所指的位置上,这相当于交换了R[i]和基准R[j],使关键字大于基准关键字的记录移到了基准的右边,交换后R[i]中又相当于存放了pivot;接着令指针j自位置j-1开始向左扫描,如此交替改变扫描方向,从两端各自往中间靠拢,直至i=j时,i便是基准pivot最终的位置,将pivot放在此位置上就完成了一次划分。

一次划分过程
一次划分过程中,具体变化情况【参见动画演示

划分算法:
int Partition(SeqList R,int i,int j)
{//调用Partition(R,low,high)时,对R[low..high]做划分,
//并返回基准记录的位置
ReceType pivot=R[i]; //用区间的第1个记录作为基准 '
while(i<j){ //从区间两端交替向中间扫描,直至i=j为止
while(i<j&&R[j].key>=pivot.key) //pivot相当于在位置i上
j--; //从右向左扫描,查找第1个关键字小于pivot.key的记录R[j]
if(i<j) //表示找到的R[j]的关键字<pivot.key
R[i++]=R[j]; //相当于交换R[i]和R[j],交换后i指针加1
while(i<j&&R[i].key<=pivot.key) //pivot相当于在位置j上
i++; //从左向右扫描,查找第1个关键字大于pivot.key的记录R[i]
if(i<j) //表示找到了R[i],使R[i].key>pivot.key
R[j--]=R[i]; //相当于交换R[i]和R[j],交换后j指针减1
} //endwhile
R[i]=pivot; //基准记录已被最后定位
return i;
} //partition
两路归并算法

1
、算法基本思路
设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:R[low..m],R[m+1..high],先将它们合并到一个局部的暂存向量R1(相当于输出堆)中,待合并完成后将R1复制回R[low..high]中。

(1)合并过程
合并过程中,设置i,j和p三个指针,其初值分别指向这三个记录区的起始位置。合并时依次比较R[i]和R[j]的关键字,取关键字较小的记录复制到R1[p]中,然后将被复制记录的指针i或j加1,以及指向复制位置的指针p加1。
重复这一过程直至两个输入的子文件有一个已全部复制完毕(不妨称其为空),此时将另一非空的子文件中剩余记录依次复制到R1中即可。

(2)动态申请R1
实现时,R1是动态申请的,因为申请的空间可能很大,故须加入申请空间是否成功的处理。

2、归并算法
void Merge(SeqList R,int low,int m,int high)
{//将两个有序的子文件R[low..m)和R[m+1..high]归并成一个有序的
//子文件R[low..high]
int i=low,j=m+1,p=0; //置初始值
RecType *R1; //R1是局部向量,若p定义为此类型指针速度更快
R1=(ReeType *)malloc((high-low+1)*sizeof(RecType));
if(! R1) //申请空间失败
Error("Insufficient memory available!");
while(i<=m&&j<=high) //两子文件非空时取其小者输出到R1[p]上
R1[p++]=(R[i].key<=R[j].key)?R[i++]:R[j++];
while(i<=m) //若第1个子文件非空,则复制剩余记录到R1中
R1[p++]=R[i++];
while(j<=high) //若第2个子文件非空,则复制剩余记录到R1中
R1[p++]=R[j++];
for(p=0,i=low;i<=high;p++,i++)
R[i]=R1[p];//归并完成后将结果复制回R[low..high]
} //Merge

8. 对冒泡与快排的改进。

1、冒泡:

算法复杂度

  1. 时间复杂度:O(n^2)

冒泡排序耗时的操作有:比较 + 交换(每次交换两次赋值)。时间复杂度如下:

1)        最好情况:序列是升序排列,在这种情况下,需要进行的比较操作为(n-1)次。交换操作为0次。即O(n)

2)        最坏情况:序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。交换操作数和比较操作数一样。即O(n^2)

3)        渐进时间复杂度(平均时间复杂度):O(n^2)

  1. 空间复杂度:O(1)

从实现原理可知,冒泡排序是在原输入数组上进行比较交换的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)

 

(三)稳定性

冒泡排序是稳定的,不会改变相同元素的相对顺序。

 

(四)优化改进

  1. 有序优化:在进行相邻元素比较时,可以记录下循环中没有发生交换的多个连续索引对(起始索引和结束索引),在下次轮询时直接对有序区间的最大值进行比较。
  2. 双向冒泡:参考资料过程中看到了双向冒泡,不同之处在于“从左至右与从右至左两种冒泡方式交替执行”,个人认为不能提高算法效率并且增加代码复杂度。

2、快排

(二)算法复杂度

  1. 时间复杂度:O(nlog2n)

快速排序耗时的操作有:比较 + 交换(每次交换两次赋值)。时间复杂度如下:

1)        最好情况:选择的基准值刚好是中间值,分区后两分区包含元素个数接近相等。因为,总共经过x次分区,根据2^x<=n得出x=log2n,每次分区需用n-1个元素与基准比较。所以O(nlog2n)

2)        最坏情况:每次分区后,只有一个分区包含除基准元素之外的元素。这样就和冒泡排序一样慢,需n(n-1)/2次比较。即O(n^2)

3)        渐进时间复杂度(平均时间复杂度):O(nlog2n)

  1. 空间复杂度:O(1)

从实现原理可知,快速排序是在原输入数组上进行比较分区的(称“就地排序”),所需开辟的辅助空间跟输入数组规模无关,所以空间复杂度为:O(1)

 

(三)稳定性

快速是不稳定的,会改变相同元素的相对顺序。如示例,以第一个基准89排序时,首先将最后一个元素-7移到了第一个分区的第一个位置上。改变了与第二个-7的相对顺序。

 

(四)优化改进

当每次分区后,两个分区的元素个数相近时,效率最高。所以找一个比较有代表性的基准值就是关键。通常会采取如下方式:

  1. 选取分区的第一个元素做为基准值。这种方式在分区基本有序情况下会分区不均。
  2. 随机快排:每次分区的基准值是该分区的随机元素,这样就避免了有序导致的分布不均的问题
  3. 平衡快排:取开头、结尾、中间3个数据,通过比较选出其中的中值。

 

9. 二分查找,与变种二分查找。

http://blog.csdn.net/u011116672/article/details/50196139

 

二分查找代码:

public int binarySearch(int[] a,int key) {

int low = 0;

int high = a.length - 1;

int mid = 0;

while(low <= high){

mid = (low + high) / 2;

if(a[mid] == key) return mid;

if(a[mid] > key) high = mid - 1;

if(a[mid] < key) low = mid + 1;

}

return -1;

}

10. 二叉树、B+树、AVL树、红黑树、哈夫曼树。

BST

即二叉搜索树:

1.所有非叶子结点至多拥有两个儿子(Left和Right);

2.所有结点存储一个关键字;

3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树;

如:

 

BST树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;

否则,如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入

右儿子;如果左儿子或右儿子的指针为空,则报告找不到相应的关键字;

如果BST树的所有非叶子结点的左右子树的结点数目均保持差不多(平衡),那么B树

的搜索性能逼近二分查找;但它比连续内存空间的二分查找的优点是,改变BST树结构

(插入与删除结点)不需要移动大段的内存数据,甚至通常是常数开销;

如:

 

但BST树在经过多次插入与删除后,有可能导致不同的结构:

右边也是一个BST树,但它的搜索性能已经是线性的了;同样的关键字集合有可能导致不同的

树结构索引;所以,使用BST树还要考虑尽可能让BST树保持左图的结构,和避免右图的结构,也就

是所谓的“平衡”问题;

 AVL平衡二叉搜索树

定义:平衡二叉树或为空树,或为如下性质的二叉排序树:
(1)左右子树深度之差的绝对值不超过1;
(2)左右子树仍然为平衡二叉树.
平衡因子BF=左子树深度-右子树深度.
平衡二叉树每个结点的平衡因子只能是1,0,-1。若其绝对值超过1,则该二叉排序树就是不平衡的。
如图所示为平衡树和非平衡树示意图:

 

 

RBT 红黑树

AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多;
红黑是弱平衡的,用非严格的平衡来换取增删节点时候旋转次数的降低;
所以简单说,搜索的次数远远大于插入和删除,那么选择AVL树,如果搜索,插入删除次数几乎差不多,应该选择RB树。

红黑树上每个结点内含五个域,color,key,left,right,p。如果相应的指针域没有,则设为NIL。
一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点,即空结点(NIL)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。
下图所示,即是一颗红黑树:

 

 

 

 

B-

是一种平衡多路搜索树(并不是二叉的):

1.定义任意非叶子结点最多只有M个儿子;且M>2;

2.根结点的儿子数为[2, M];

3.除根结点以外的非叶子结点的儿子数为[M/2, M];

4.每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)

5.非叶子结点的关键字个数=指向儿子的指针个数-1;

6.非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];

7.非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的

子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;

8.所有叶子结点位于同一层;

如:(M=3

B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果

命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为

空,或已经是叶子结点;

B-树的特性:

1.关键字集合分布在整颗树中;

2.任何一个关键字出现且只出现在一个结点中;

3.搜索有可能在非叶子结点结束;

4.其搜索性能等价于在关键字全集内做一次二分查找;

5.自动层次控制;

由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少

利用率,其最底搜索性能为:

 

其中,M为设定的非叶子结点最多子树个数,N为关键字总数;

所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题;

由于M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占

M/2的结点;删除结点时,需将两个不足M/2的兄弟结点合并;

 

 

B+

B+树是B-树的变体,也是一种多路搜索树:

1.其定义基本与B-树同,除了:

2.非叶子结点的子树指针与关键字个数相同;

3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树

(B-树是开区间);

5.为所有叶子结点增加一个链指针;

6.所有关键字都在叶子结点出现;

如:(M=3)

B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在

非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;

B+的特性:

1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好

是有序的;

2.不可能在非叶子结点命中;

3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储

(关键字)数据的数据层;

4.更适合文件索引系统;比如对已经建立索引的数据库记录,查找10<=id<=20,那么只要通过根节点搜索到id=10的叶节点,之后只要根据叶节点的链表找到第一个大于20的就行了,比B-树在查找10到20内的每一个时每次都从根节点出发查找提高了不少效率。

 

B*

是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;

B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3

(代替B+树的1/2);

B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据

复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父

结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;

B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分

数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字

(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之

间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;

所以,B*树分配新结点的概率比B+树要低,空间使用率更高;

 

小结

B树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于

走右结点;

B-树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键

字范围的子结点;

所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;

B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点

中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;

B*树:在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率

从1/2提高到2/3;

 

B+/B*Tree应用

数据库索引--索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。

 

 

数据库索引--表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键。

 

 

倒排索引--也可以由B树及其变种实现但不一定非要B树及其变种实现,如lucene没有使用B树结构,因此lucene可以用二分搜索算法快速定位关键词。实现时,lucene将下面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)、位置文件 (positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。

[java] view plain copy

  1. 关键词文章号[出现频率]              出现位置
  2. guangzhou           1[2]                      3,6
  3. he                  2[1]                      1
  4. i                   1[1]                      4
  5. live                1[2]                      2,5,
  6. 2[1]                      2
  7. shanghai            2[1]                      3
  8. tom                 1[1]                      1

 

http://blog.csdn.net/sup_heaven/article/details/39313731

11. 二叉树的前中后续遍历:递归与非递归写法,层序遍历算法。

二叉树求深度 递归非递归

  1. importutil.LinkedList;
  2. publicclass Deep
  3. {
  4. //递归实现1
  5. public int findDeep(BiTree root)
  6. {
  7. int deep = 0;
  8. if(root != null)
  9. {
  10. intlchilddeep = findDeep(root.left);
  11. intrchilddeep = findDeep(root.right);
  12. deep = lchilddeep > rchilddeep ? lchilddeep + 1: rchilddeep + 1;
  13. }
  14. returndeep;
  15. }
  16. //递归实现2
  17. publicint findDeep1(BiTree root)
  18. {
  19. if(root == null)
  20. {
  21. return0;
  22. }
  23. else
  24. {
  25. intlchilddeep = findDeep1(root.left);//求左子树的深度
  26. intrchilddeep = findDeep1(root.left);//求右子树的深度
  27. returnlchilddeep > rchilddeep ? lchilddeep + 1 : rchilddeep + 1;//左子树和右子树深度较大的那个加一等于整个树的深度
  28. }
  29. }
  30. //非递归实现
  31. publicint findDeep2(BiTree root)
  32. {
  33. if(root == null)
  34. return0;
  35. BiTree current = null;
  36. LinkedList<BiTree> queue = newLinkedList<BiTree>();
  37. offer(root);
  38. intcur,last;
  39. intlevel = 0;
  40. while(!queue.isEmpty())
  41. {
  42. cur = 0;//记录本层已经遍历的节点个数
  43. last = queue.size();//当遍历完当前层以后,队列里元素全是下一层的元素,队列的长度是这一层的节点的个数
  44. while(cur < last)//当还没有遍历到本层最后一个节点时循环
  45. {
  46. current = queue.poll();//出队一个元素
  47. cur++;
  48. //把当前节点的左右节点入队(如果存在的话)
  49. if(current.left != null)
  50. {
  51. offer(current.left);
  52. }
  53. if(current.right != null)
  54. {
  55. offer(current.right);
  56. }
  57. }
  58. level++;//每遍历完一层level+1
  59. }
  60. returnlevel;
  61. }
  62. publicstatic void main(String[] args)
  63. {
  64. BiTree root = BiTree.buildTree();
  65. Deep deep = newDeep();
  66. out.println(deep.findDeep(root));
  67. out.println(deep.findDeep1(root));
  68. out.println(deep.findDeep2(root));
  69. }
  70. }
  71. packagetree;

递归非递归遍历先中后序

  1. importutil.Stack;
  2. publicclassBinaryTree {
  3. protectedNode root;
  4. publicBinaryTree(Node root) {
  5. this.root = root;
  6. }
  7. publicNode getRoot() {
  8. returnroot;
  9. }
  10. /** 构造树*/
  11. publicstatic Node init() {
  12. Node a = newNode('A');
  13. Node b = newNode('B', null, a);
  14. Node c = newNode('C');
  15. Node d = newNode('D', b, c);
  16. Node e = newNode('E');
  17. Node f = newNode('F', e, null);
  18. Node g = newNode('G', null, f);
  19. Node h = new Node('H', d, g);
  20. return h;// root
  21. }
  22. /** 访问节点 */
  23. public static void visit(Node p) {
  24. out.print(p.getKey() + " ");
  25. }
  26. /** 递归实现前序遍历 */
  27. protected static void preorder(Node p) {
  28. if (p != null) {
  29. visit(p);
  30. preorder(p.getLeft());
  31. preorder(p.getRight());
  32. }
  33. }
  34. /** 递归实现中序遍历 */
  35. protected static void inorder(Node p) {
  36. if (p != null) {
  37. inorder(p.getLeft());
  38. visit(p);
  39. inorder(p.getRight());
  40. }
  41. }
  42. /** 递归实现后序遍历 */
  43. protected static void postorder(Node p) {
  44. if (p != null) {
  45. postorder(p.getLeft());
  46. postorder(p.getRight());
  47. visit(p);
  48. }
  49. }
  50. /**********************************************************************************************/
  51. /** 非递归实现前序遍历 */
  52. protected static void iterativePreorder(Node p) {
  53. Stack<Node> stack = new Stack<Node>();
  54. if (p != null) {
  55. push(p);
  56. while (!stack.empty()) {
  57. p = stack.pop();
  58. visit(p);
  59. if (p.getRight() != null)
  60. push(p.getRight());
  61. if (p.getLeft() != null)  //为什么getLeft() 在后,getRight()在前应为while 循环第一句就是pop visit所以要把left放上,先访问。之中方法是即压即访问法。
  62. push(p.getLeft());
  63. }
  64. }
  65. }
  66. /** 非递归实现中序遍历 */  //思路与上面iterativePreorder 一致。
  67. protected static void iterativeInorder(Node p) {
  68. Stack<Node> stack = new Stack<Node>();
  69. while (p != null) {
  70. while (p != null) {
  71. if (p.getRight() != null)
  72. push(p.getRight());// 当前节点右子入栈
  73. push(p);// 当前节点入栈
  74. p = p.getLeft();
  75. }
  76. p = stack.pop();
  77. while (!stack.empty() && p.getRight() == null) {
  78. visit(p);
  79. p = stack.pop();
  80. }
  81. visit(p);
  82. if (!stack.empty())
  83. p = stack.pop();
  84. else
  85. p = null;
  86. }
  87. }
  88. /*******************************************************************************************/
  89. /*******************************************************************************************/
  90. /** 非递归实现前序遍历2 */
  91. protected static void iterativePreorder2(Node p) {
  92. Stack<Node> stack = new Stack<Node>();
  93. Node node = p;
  94. while (node != null || stack.size() > 0) {
  95. while (node != null) {//压入所有的左节点,压入前访问它。左节点压入完后pop访问右节点。像这样算法时思考规律性的东西在哪。不管哪个节点都要压所节点判断右节点。
  96. visit(node);
  97. push(node);
  98. node = node.getLeft();
  99. }
  100. if (stack.size() > 0) {//
  101. node = stack.pop();
  102. node = node.getRight();
  103. }
  104. }
  105. }
  106. /** 非递归实现中序遍历2 */
  107. protected static void iterativeInorder2(Node p) {
  108. Stack<Node> stack = new Stack<Node>();
  109. Node node = p;
  110. while (node != null || stack.size() > 0) {
  111. while (node != null) {
  112. push(node);
  113. node = node.getLeft();
  114. }
  115. if (stack.size() > 0) {
  116. node = stack.pop();
  117. visit(node);   //与iterativePreorder2比较只有这句话的位置不一样,弹出时再访问。
  118. node = node.getRight();
  119. }
  120. }
  121. }
  122. /*******************************************************************************************/
  123. /** 非递归实现后序遍历 */
  124. protected static void iterativePostorder(Node p) {
  125. Node q = p;
  126. Stack<Node> stack = new Stack<Node>();
  127. while (p != null) {
  128. // 左子树入栈
  129. for (; p.getLeft() != null; p = p.getLeft())
  130. push(p);
  131. // 当前节点无右子或右子已经输出
  132. while (p != null && (p.getRight() == null || p.getRight() == q)) {
  133. visit(p);
  134. q = p;// 记录上一个已输出节点
  135. if (stack.empty())
  136. return;
  137. p = stack.pop();
  138. }
  139. // 处理右子
  140. push(p);
  141. p = p.getRight();
  142. }
  143. }
  144. /** 非递归实现后序遍历 双栈法 */
  145. protected static void iterativePostorder2(Node p) {//理解左子树   右子树 根递归性质,把它运用到循环当中去。
  146. Stack<Node> lstack = new Stack<Node>();//左子树栈
  147. Stack<Node> rstack = new Stack<Node>();//右子树栈
  148. Node node = p, right;
  149. do {
  150. while (node != null) {
  151. right = node.getRight();
  152. push(node);
  153. push(right);
  154. node = node.getLeft();
  155. }
  156. node = lstack.pop();
  157. right = rstack.pop();
  158. if (right == null) {
  159. visit(node);
  160. else {
  161. push(node);
  162. push(null);
  163. }
  164. node = right;
  165. while (lstack.size() > 0 || rstack.size() > 0);
  166. }
  167. /** 非递归实现后序遍历 单栈法*/
  168. protected static void iterativePostorder3(Node p) {
  169. Stack<Node> stack = new Stack<Node>();
  170. Node node = p, prev = p;
  171. while (node != null || stack.size() > 0) {
  172. while (node != null) {
  173. push(node);
  174. node = node.getLeft();
  175. }
  176. if (stack.size() > 0) {
  177. Node temp = stack.peek().getRight();
  178. if (temp == null || temp == prev) {
  179. node = stack.pop();
  180. visit(node);
  181. prev = node;
  182. node = null;
  183. else {
  184. node = temp;
  185. }
  186. }
  187. }
  188. }
  189. /** 非递归实现后序遍历4 双栈法*/
  190. protected static void iterativePostorder4(Node p) {
  191. Stack<Node> stack = new Stack<Node>();
  192. Stack<Node> temp = new Stack<Node>();
  193. Node node = p;
  194. while (node != null || stack.size() > 0) {
  195. while (node != null) {
  196. push(node);
  197. push(node);
  198. node = node.getRight();
  199. }
  200. if (stack.size() > 0) {
  201. node = stack.pop();
  202. node = node.getLeft();
  203. }
  204. }
  205. while (temp.size() > 0) {//把插入序列都插入到了temp。
  206. node = temp.pop();
  207. visit(node);
  208. }
  209. }
  210. /**
  211. * @param args
  212. */
  213. public static void main(String[] args) {
  214. BinaryTree tree = new BinaryTree(init());
  215. out.print(" 递归遍历\n");
  216. out.print(" Pre-Order:");
  217. preorder(tree.getRoot());
  218. out.print(" \n In-Order:");
  219. inorder(tree.getRoot());
  220. out.print("\n Post-Order:");
  221. postorder(tree.getRoot());
  222. out.print(" \n非递归遍历");
  223. out.print(" \n Pre-Order:");
  224. iterativePreorder(tree.getRoot());
  225. out.print("\n Pre-Order2:");
  226. iterativePreorder2(tree.getRoot());
  227. out.print(" \n In-Order:");
  228. iterativeInorder(tree.getRoot());
  229. out.print("\n In-Order2:");
  230. iterativeInorder2(tree.getRoot());
  231. out.print("\n Post-Order:");
  232. iterativePostorder(tree.getRoot());
  233. out.print("\n Post-Order2:");
  234. iterativePostorder2(tree.getRoot());
  235. out.print("\n Post-Order3:");
  236. iterativePostorder3(tree.getRoot());
  237. out.print("\n Post-Order4:");
  238. iterativePostorder4(tree.getRoot());
  239. }
  240. }
  241. classNode {
  242. private char key;
  243. private Node left, right;
  244. public Node(char key) {
  245. this(key, nullnull);
  246. }
  247. public Node(char key, Node left, Node right) {
  248. this.key = key;
  249. this.left = left;
  250. this.right = right;
  251. }
  252. public char getKey() {
  253. return key;
  254. }
  255. public void setKey(char key) {
  256. this.key = key;
  257. }
  258. public Node getLeft() {
  259. return left;
  260. }
  261. public void setLeft(Node left) {
  262. this.left = left;
  263. }
  264. public Node getRight() {
  265. return right;
  266. }
  267. public void setRight(Node right) {
  268. this.right = right;
  269. }
  270. }

 

 

http://blog.csdn.net/fansongy/article/details/6798278

12. 图的BFS与DFS算法,最小生成树prim算法与最短路径Dijkstra算法。

迪杰斯特拉

最小生成树

在prim算法中,通过加入最小邻接边的方法来建立最小生成树算法。首先构造一个零图,在选一个初始顶点加入到新集合中,然后分别在原先的顶点集合中抽取一个顶点,使得构成的边为权值最小,然后将该笔边加入到图中,并将抽出的顶点加入到新集合中,重复这个过程,知道新集合等于原先的集合。

 

在kruskal算法中,根据边的权值以递增的方式逐渐建立最小生成树。具体操作是:将赋权图每个顶点都看做森林,然后将图中每条邻接边的权值按照升序的方式进行排列,接着从排列好的邻接边表中抽取权值最小的边,写入该边的起始顶点和结束顶点,连接顶点将森林构成树,然后读取起始结束顶点的邻接边,优先抽取权值小的邻接边,继续连接顶点将森林构成树。添加邻接边的要求是加入到图中的邻接边不构成回路。如此反复进行,直到已经添加n-1条边为止。

http://blog.csdn.net/rongyongfeikai2/article/details/7468196

13. KMP算法。

http://blog.csdn.net/hackbuteer1/article/details/7319115

14. 排列组合问题。

http://res.tongyi.com/resources/old_article/student/487.html

15. 动态规划、贪心算法、分治算法。(一般不会问到)

http://www.tuicool.com/articles/jq6zAv

16. 大数据处理:类似10亿条数据找出最大的1000个数.........等等

http://kb.cnblogs.com/page/95701/

http://www.cnblogs.com/v-July-v/archive/2012/03/22/2413055.html

 

 

TCP/IP

(建议先将本块内容先理解一下,内容较多,好多只能粘贴网址了)

1 OSI与TCP/IP各层的结构与功能,都有哪些协议

OSI中的层 功能 TCP/IP协议族
应用层 文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
表示层 翻译、加密、压缩 没有协议
会话层 对话控制、建立同步点(续传) 没有协议
传输层 端口寻址、分段重组、流量、差错控制 TCP,UDP
网络层 逻辑寻址、路由选择 IP,ICMP,OSPF,EIGRP,IGMP
数据链路层 成帧、物理寻址、流量,差错,接入控制 SLIP,CSLIP,PPP,MTU
物理层 设置网络拓扑结构、比特传输、位同步 ISO2110,IEEE802,IEEE802.2

注意tcp本身不具有数据传输中噪音导致的错误检测功能,但是实现超时的错误重传功能;

网络层协议包括:

IP(Internet Protocol)协议、ICMP(Internet Control Message Protocol)
控制报文协议、ARP(Address Resolution Protocol)地址转换协议、RARP(Reverse ARP)反向地址转换协议。
IP是网络层的核心,通过路由选择将下一条IP封装后交给接口层。IP数据报是无连接服务。
ICMP是网络层的补充,可以回送报文。用来检测网络是否通畅。
Ping命令就是发送ICMP的echo包,通过回送的echo relay进行网络测试。
ARP是正向地址解析协议,通过已知的IP,寻找对应主机的MAC地址。
RARP是反向地址解析协议,通过MAC地址确定IP地址。比如无盘工作站还有DHCP服务。

传输层协议:

传输控制协议TCP(Transmission Control Protocol):TCP是面向连接的通信协议,通过三次握手建立连接,通讯完成时要拆除连接,由于TCP是面向连接的所以只能用于点对点的通讯。
TCP提供的是一种可靠的数据流服务,采用“带重传的肯定确认”技术来实现传输的可靠性。

用户数据报协议UDP(User Datagram protocol):UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。
UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出丢包现象,实际应用中要求程序员编程验证。UDP与TCP位于同一层,但它不管数据包的顺序、错误或重发。因此,UDP不被应用于那些使用虚电路的面向连接的服务,UDP主要用于那些面向查询---应答的服务,例如NFS。相对于FTP或Telnet,这些服务需要交换的信息量较小。使用UDP的服务包括NTP(网络时间协议)和DNS(DNS也使用TCP)。

应用层协议。
FTP(File Transfer Protocol)是文件传输协议,一般上传下载用FTP服务,数据端口是20H,控制端口是21H。
Telnet服务是用户远程登录服务,使用23H端口,使用明码传送,保密性差、简单方便。
DNS(Domain Name Service)是域名解析服务,提供域名到IP地址之间的转换,使用端口53。
SMTP(Simple Mail Transfer Protocol)是简单邮件传输协议,用来控制信件的发送、中转,使用端口25。
NFS(Network File System)是网络文件系统,用于网络中不同主机间的文件共享。
HTTP(Hypertext Transfer Protocol)是超文本传输协议,用于实现互联网中的WWW服务,使用端口80。

 

http://blog.csdn.net/gs_008/article/details/50976379

2.TCP与UDP的区别。

TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。

UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。

3.TCP报文结构。

http://blog.csdn.net/a19881029/article/details/29557837

4. TCP的三次握手与四次挥手过程,各个状态名称与含义,TIMEWAIT的作用。

http://www.cnblogs.com/yuxingfirst/archive/2013/06/30/3163400.html

5. TCP拥塞控制

http://blog.csdn.net/sicofield/article/details/9708383

6.TCP滑动窗口与回退N针协议

http://www.cnblogs.com/hupp/p/4857093.html

7. Http的报文结构。

http://blog.csdn.net/zhll3377/article/details/7748086

8. Http的状态码含义。

http://www.cnblogs.com/lovening/archive/2011/04/28/2031764.html

9. Http request的几种类型。

http://blog.chinaunix.net/uid-20628302-id-3134741.html

10. Http1.1和Http1.0的区别

1,HTTP/1.0协议使用非持久连接,即在非持久连接下,一个tcp连接只传输一个Web对象,;
2,HTTP/1.1默认使用持久连接(然而,HTTP/1.1协议的客户机和服务器可以配置成使用非持久连接)。

在持久连接下,不必为每个Web对象的传送建立一个新的连接,一个连接中可以传输多个对象!

11. Http怎么处理长连接。

但是HTTP1.1开始默认建立的是长连接,即一旦浏览器发起HTTP请求,建立的连接不会请求应答之后立刻断掉。

 

1、 一个复杂的具备很多HTTP资源的网页会建立多少TCP连接,如何使用这些连接?

2、 已经建立的TCP连接是否会自动断开,时间是多久?

 

对于第一个问题。现在浏览器都有最大并发连接数限制,应该说如果需要,就会尽量在允许范围内建立更多的TCP持久连接来处理HTTP请求,同样滴,一个TCP持久连接可以不断传输多个HTTP请求,但是如果上一个请求的响应还未收到,则不能处理下一个请求(Pipeling管道技术可以解决这个问题从而进一步提升性能),所以说很多浏览器其实都可以修改允许最大并发连接数以提升浏览网页的速度。

 

对于第二个问题。问题在于服务器端对于长连接的实现,特别是在对长连接的维护上。FTP协议及SMTP协议中有NOOP消息,这个就可以认为是心跳报文,但HTTP协议没有类似的消息,这样服务器端只能使用超时断开的策略来维护连接。设想超时时间非常短,那么有效空闲时间就非常短,换句话讲:一旦链路上没有数据发送,服务器端很快就关闭连接。

也就是说其实HTTP的长连接很容易在空闲后自动断开,一般来说这个时间是300s左右。

12. Cookie与Session的作用于原理。

http://blog.csdn.net/jzhf2012/article/details/8496502

13. 电脑上访问一个网页,整个过程是怎么样的:DNS、HTTP、TCP、OSPF、IP、ARP。

假设你用一个全新的浏览器(第一次启动的那种),访问百度(http://www.baidu.com/),在你敲入网址并按下回车之后,将会发生以下神奇的事情:

浏览器先尝试从Host文件中获取http://www.baidu.com/对应的IP地址,如果能取到当然万事大吉大家都能嗨,如果不能,就使用DNS协议来获取IP咯。

在DNS协议中,PC会向你的本地DNS服务器求助(一般是路由器),希望从本地DNS服务器那里得到百度的IP,得到就好,得不到还得向更高层次的DNS服务器求助,最终总能得到百度的IP。
得到百度的IP,下一步是使用TCP协议,建立TCP连接。

在TCP协议中,建立TCP需要与百度服务器握手三次,你先告诉服务器你要给服务器发东西(SYN),服务器应答你并告诉你它也要给你发东西(SYN、ACK),然后你应答服务器(ACK),总共来回了3次,称为3次握手。
不过,建立TCP连接有个前提(或者说给服务器发消息有个前提):你必须能成功地把消息发到服务器上。虽然已经知道IP,但并无啥用(比如说,你在广东,你知道北京的地理坐标经纬度就能到北京了?你得知道有哪些路通往北京吧你得准备盘缠吧你得花时间吧)。

为了将消息从你的PC上传到服务器上,需要用到IP协议、ARP协议和OSPF协议。

我们都知道,你的PC和百度服务器之间一般会有许多路由器之类的东西,IP协议指定了出发地(你的PC)和目的地(服务器);你的数据会经过一个又一个路由器,OSPF决定了会经过那些路由器(用一种叫路由算法的玩意,找出最佳路径);从一个路由器怎么传给下一个路由器?这是ARP协议的JOB,ARP负责求下一个节点的地址(我们不止是要目的地,还要中间节点的地址)。
IP协议使用的是IP地址,整个发送过程中只涉及出发地和目的地2个IP地址,而ARP协议使用的是MAC地址,整个发送过程中涉及到每一个节点的MAP地址
现在,我们能和服务器通信,还建立了TCP连接,下一步干嘛,当然是用HTTP协议请求网页内容咯。

你发个HTTP请求报文给服务器,如果服务器禁止你访问它就给你回个"Forbidden",如果它暂时挂掉了就给你回个“内部服务错误”,如果它正常才给你回个“OK“并将你要的数据传给你;如果你还需要其它的东西再去跟它要(它一般还会给你的-_-)。
你收到了服务器的回复,是一坨HTML形式的文本。浏览器必须要能够理解文本的内容,并快速地渲染到屏幕上(浏览器一般用有限自动机来理解文本内容,渲染的话就各看本事了,之所以微软IE卡成狗而谷歌浏览器很6,就是它们的渲染速度不同...)

渲染出来后,你就看到百度的首页了

14. Ping的整个过程。

http://blog.chinaunix.net/uid-26758209-id-3146224.html

ICMP报文是什么

http://blog.csdn.net/tigerjibo/article/details/7356936

15. C/S模式下使用socket通信,几个关键函数。

http://blog.csdn.net/phunxm/article/details/5085825

16. IP地址分类。

http://tanghuimin0713.blog.51cto.com/4159848/791906

17. 路由器与交换机区别。

路由器和交换机,二者区别如下:
1,路由器工作于OSI模型的网络层,能够识别IP地址,并根据IP地址转发数据包,并维护着路由表,能够基于路由表进行最佳路线选择;
2,路由器上还能开启ACL访问控制列表、NAT地址转换等功能,扩展网络应用,;
3,传统交换机工作于OSI模型的数据链路层,能够识别MAC地址,根据MAC地址转发数据帧,并维护着一张桥表,根据桥表上MAC地址和端口的对应关系进行数据帧转发。
4,交换机能够隔离冲突域,并划分VLAN。

 

路由器多了一个路由的功能,就是作为AP、网桥或网关。
可以隔离内外网

一般的路由器只有一个WAN端口是路由功能,其余的Lan端口就是交换机。

 

 

 

 

 

 

Mysql数据库

引擎

MyISAM 和InnoDB 讲解

InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,视具体应用而定。基本的差别为:MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持。MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快,但是不提供事务支持,而InnoDB提供事务支持以及外部键等高级数据库功能。

以下是一些细节和具体实现的差别:

◆1.InnoDB不支持FULLTEXT类型的索引。

◆2.InnoDB 中不保存表的具体行数,也就是说,执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count(*)语句包含 where条件时,两种表的操作是一样的。

◆3.对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。

◆4.DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。

◆5.LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

另外,InnoDB表的行锁也不是绝对的,假如在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”

两种类型最主要的差别就是Innodb 支持事务处理与外键和行级锁。而MyISAM不支持.所以MyISAM往往就容易被人认为只适合在小项目中使用。

作为使用MySQL的用户角度出发,Innodb和MyISAM都是比较喜欢的,如果数据库平台要达到需求:99.9%的稳定性,方便的扩展性和高可用性来说的话,MyISAM绝对是首选。

原因如下:

1、平台上承载的大部分项目是读多写少的项目,而MyISAM的读性能是比Innodb强不少的。

2、MyISAM的索引和数据是分开的,并且索引是有压缩的,内存使用率就对应提高了不少。能加载更多索引,而Innodb是索引和数据是紧密捆绑的,没有使用压缩从而会造成Innodb比MyISAM体积庞大不小。

3、经常隔1,2个月就会发生应用开发人员不小心update一个表where写的范围不对,导致这个表没法正常用了,这个时候MyISAM的优越性就体现出来了,随便从当天拷贝的压缩包取出对应表的文件,随便放到一个数据库目录下,然后dump成sql再导回到主库,并把对应的binlog补上。如果是Innodb,恐怕不可能有这么快速度,别和我说让Innodb定期用导出xxx.sql机制备份,因为最小的一个数据库实例的数据量基本都是几十G大小。

4、从接触的应用逻辑来说,select count(*) 和order by 是最频繁的,大概能占了整个sql总语句的60%以上的操作,而这种操作Innodb其实也是会锁表的,很多人以为Innodb是行级锁,那个只是where对它主键是有效,非主键的都会锁全表的。

5、还有就是经常有很多应用部门需要我给他们定期某些表的数据,MyISAM的话很方便,只要发给他们对应那表的frm.MYD,MYI的文件,让他们自己在对应版本的数据库启动就行,而Innodb就需要导出xxx.sql了,因为光给别人文件,受字典数据文件的影响,对方是无法使用的。

6、如果和MyISAM比insert写操作的话,Innodb还达不到MyISAM的写性能,如果是针对基于索引的update操作,虽然MyISAM可能会逊色Innodb,但是那么高并发的写,从库能否追的上也是一个问题,还不如通过多实例分库分表架构来解决。

7、如果是用MyISAM的话,merge引擎可以大大加快应用部门的开发速度,他们只要对这个merge表做一些select count(*)操作,非常适合大项目总量约几亿的rows某一类型(如日志,调查统计)的业务表。

当然Innodb也不是绝对不用,用事务的项目就用Innodb的。另外,可能有人会说你MyISAM无法抗太多写操作,但是可以通过架构来弥补。

 

 

 

索引

 

MySQL索引的概念

索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。上述SQL语句,在没有索引的情况下,数据库会遍历全部200条数据后选择符合条件的;而有了相应的索引之后,数据库会直接在索引中查找符合条件的选项。如果我们把SQL语句换成“SELECT * FROM article WHERE id=2000000”,那么你是希望数据库按照顺序读取完200万行数据以后给你结果还是直接在索引中定位呢?上面的两个图片鲜明的用时对比已经给出了答案(注:一般数据库默认都会为主键生成索引)。

索引分为聚簇索引和非聚簇索引两种,聚簇索引是按照数据存放的物理位置为顺序的,而非聚簇索引就不一样了;聚簇索引能提高多行检索的速度,而非聚簇索引对于单行的检索很快。

MySQL索引的类型

  1. 普通索引

这是最基本的索引,它没有任何限制,比如上文中为title字段创建的索引就是一个普通索引,MyIASM中默认的BTREE类型的索引,也是我们大多数情况下用到的索引。

01 –直接创建索引
02 CREATE INDEX index_name ON table(column(length))

 

03 –修改表结构的方式添加索引
04 ALTER TABLE table_name ADD INDEX index_name ON (column(length))

 

05 –创建表的时候同时创建索引
06 CREATE TABLE table (

 

07 id int(11) NOT NULL AUTO_INCREMENT ,
08 title char(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,

 

09 content text CHARACTER SET utf8 COLLATE utf8_general_ci NULL ,
10 time int(10) NULL DEFAULT NULL ,

 

11 PRIMARY KEY (id),
12 INDEX index_name (title(length))

 

13 )
14 –删除索引

 

15 DROP INDEX index_name ON table
  1. 唯一索引

与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值(注意和主键不同)。如果是组合索引,则列值的组合必须唯一,创建方法和普通索引类似。

  1. 全文索引(FULLTEXT

MySQL从3.23.23版开始支持全文索引和全文检索,FULLTEXT索引仅可用于 MyISAM 表;他们可以从CHAR、VARCHAR或TEXT列中作为CREATE TABLE语句的一部分被创建,或是随后使用ALTER TABLE 或CREATE INDEX被添加。////对于较大的数据集,将你的资料输入一个没有FULLTEXT索引的表中,然后创建索引,其速度比把资料输入现有FULLTEXT索引的速度更为快。不过切记对于大容量的数据表,生成全文索引是一个非常消耗时间非常消耗硬盘空间的做法。

  1. 单列索引、多列索引

多个单列索引与单个多列索引的查询效果不同,因为执行查询时,MySQL只能使用一个索引,会从多个索引中选择一个限制最为严格的索引。

  1. 组合索引(最左前缀)

平时用的SQL查询语句一般都有比较多的限制条件,所以为了进一步榨取MySQL的效率,就要考虑建立组合索引。例如上表中针对title和time建立一个组合索引:ALTER TABLE article ADD INDEX index_titme_time (title(50),time(10))。建立这样的组合索引,其实是相当于分别建立了下面两组组合索引:

–title,time

–title

为什么没有time这样的组合索引呢?这是因为MySQL组合索引“最左前缀”的结果。简单的理解就是只从最左面的开始组合。并不是只要包含这两列的查询都会用到该组合索引,如下面的几个SQL所示:

1 –使用到上面的索引
2 SELECT * FROM article WHREE title='测试' AND time=1234567890;

 

3 SELECT * FROM article WHREE utitle='测试';
4 –不使用上面的索引

 

5 SELECT * FROM article WHREE time=1234567890;

MySQL索引的优化

上面都在说使用索引的好处,但过多的使用索引将会造成滥用。因此索引也会有它的缺点:虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快。索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句。下面是一些总结以及收藏的MySQL索引的注意事项和优化方法。

  1. 何时使用聚集索引或非聚集索引?
动作描述 使用聚集索引 使用非聚集索引
列经常被分组排序 使用 使用
返回某范围内的数据 使用 不使用
一个或极少不同值 不使用 不使用
小数目的不同值 使用 不使用
大数目的不同值 不使用 使用
频繁更新的列 不使用 使用
外键列 使用 使用
主键列 使用 使用
频繁修改索引列 不使用 使用

事实上,我们可以通过前面聚集索引和非聚集索引的定义的例子来理解上表。如:返回某范围内的数据一项。比如您的某个表有一个时间列,恰好您把聚合索引建立在了该列,这时您查询2004年1月1日至2004年10月1日之间的全部数据时,这个速度就将是很快的,因为您的这本字典正文是按日期进行排序的,聚类索引只需要找到要检索的所有数据中的开头和结尾数据即可;而不像非聚集索引,必须先查到目录中查到每一项数据对应的页码,然后再根据页码查到具体内容。其实这个具体用法我还不是很理解,只能等待后期的项目开发中慢慢学学了。

  1. 索引不会包含有NULL值的列

只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

  1. 使用短索引

对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

  1. 索引列排序

MySQL查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。

  1. like语句操作

一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。

  1. 不要在列上进行运算

例如:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′。关于这一点可以围观:一个单引号引发的MYSQL性能损失。

最后总结一下,MySQL只对一下操作符才使用索引:<,<=,=,>,>=,between,in,以及某些时候的like(不以通配符%或_开头的情形)。而理论上每张表里面最多可创建16个索引,不过除非是数据量真的很多,否则过多的使用索引也不是那么好玩的,比如我刚才针对text类型的字段创建索引的时候,系统差点就卡死了。

 

 

 

聚簇索引与非聚簇索引

 

聚簇索引的叶节点就是数据节点,而非聚簇索引的叶节点仍然是索引节点,并保留一个链接指向对应数据块。  聚簇索引和非聚簇索引的一个标志性区别就是聚簇索引的叶节点对应着数据页,从中间级的索引页的索引行直接对应着数据页。而非聚簇索引的索引B+树叶节点不是直接指向数据页面的。如果表有聚集索引或索引视图上有聚集索引,则行定位器是行的聚集索引键。如果聚集索引不是唯一的索引,SQL Server 将添加在内部生成的值(称为唯一值)以使所有重复键唯一。此四字节的值对于用户不可见。仅当需要使聚集键唯一以用于非聚集索引中时,才添加该值。SQL Server 通过使用存储在非聚集索引的叶行内的聚集索引键搜索聚集索引来检索数据行。

 

聚簇索引主键的插入速度要比非聚簇索引主键的插入速度慢很多。

相比之下,聚簇索引适合排序,非聚簇索引不适合用在排序的场合。因为聚簇索引本身已经是按照物理顺序放置的,排序很快。非聚簇索引则没有按序存放,需要额外消耗资源来排序。

当你需要取出一定范围内的数据时,用聚簇索引也比用非聚簇索引好。

 

非聚簇索引常被用在以下情况:

1、某列常用于集合函数(如Sum,....)。

2、某列常用于join,order by,group by。

3、查寻出的数据不超过表中数据量的20%。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注