秋招已接近尾声,无奈投了20多家,只面了4家,苏宁(等)、海康(等)、华为(等)和科大讯飞(offer已拒,未达到期望值),所以想在CSDN上整理下之前做的笔记,就当复习一遍,为10月份的笔试面试做些准备。本人主要投的是JAVA后台开发。大致整理内容分为JAVA部分,计算机操作系统、计算机网络、数据库和Linux常用指令部分,针对计算机算法和数据结构,主要在牛客网上针对题集复习的,因此没有做过详细的笔记。
目录
7、ArrayList、LinkedList和Vector的区别。
8、String、StirngBuilder和StringBuffer的区别。
9、Map、Set、List、Queue和Stack的特点与用法。
11、HashMap和Concurrent的区别,HashMap的底层实现。
12、TreeMap、HashMap和LinkedHashMap的区别。
13、Collection包结构,与Collections的区别。
14、try catch finally,try里有return,finally还执行吗?
15、Exception和Error包结构。OOM你遇到过哪些,SOF你遇到过哪些?
19、实现多线程的方法:Thread、Runnable和Callable。
1、Java部分
1.1 java基础
1、九种基本类型的大小,以及他们的封装类。
boolean byte char short int long float double
Boolean Byte Character Short Integer Long Float Double
byte (-128 - 127) 默认为0。
char (0 - 65535)
short (-2^15 – 2^15-1) 默认为0。
int (-2^31 – 2^31-1)默认为0。
long (-2^63 – 2^63-1)默认为0L。
float 默认为0.0f。
double 默认为0.0d;
boolean (true or false) 默认为false。
2、Switch能否用String做参数?
在jdk 7 之前,switch 只能支持 byte、short、char、int 这几个基本数据类型和其对应的封装类型。switch后面的括号里面只能放int类型的值,但由于byte,short,char类型,它们会自动转换为int类型(精精度小的向大的转化),所以它们也支持。注意,对于精度比int大的类型,比如long、float,doulble,不会自动转换为int,如果想使用,就必须强转为int,如(int)float;
jdk1.7后,整形,枚举类型,boolean,字符串都可以。处理方式为通过调用switch中String.hashCode,将string转换为int从而进行判断。
3、equals与==的区别。
equals 当对象的类型和值相同时返回true,否则返回false。
== 比较的是否指向同一个对象,若是返回true,不是返回flase。
注意:Object类中的equals方法和“==”是一样的,没有区别,而String类,Integer类等等一些类,是重写了equals方法,才使得equals和”==”不同,所以当创建类时,自动继承了object的equals方法,要想实现不同的等于比较,必须重写equals方法。
4、Object有哪些方法?
1)clone
保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常
2)equals
在Object中与==是一样的,子类一般需要重写该方法
3)hashCode
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到
4)getClass
final方法,获得运行时类型
5)wait
使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或 者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生:
1. 其他线程调用了该对象的notify方法
2. 其他线程调用了该对象的notifyAll方法
3. 其他线程调用了interrupt中断该线程
4. 时间间隔到了
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常
6)notify
唤醒在该对象上等待的某个线程
7)notifyAll
唤醒在该对象上等待的所有线程
8)toString
转换成字符串,一般子类都有重写,否则打印句柄
5、JAVA中的四中引用强虚软弱,应用的场景。
强引用(StrongReference):如String s = "abc",变量s就是字符串“abc”的强引用,只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
弱引用(WeakReference):用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
软引用(SoftReference):用来描述一些有用但并不是必需的对象,只有在内存不足的时候JVM才会回收该对象。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。
虚引用(PhantomReference):虚引用当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
“为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。”
6、hashCode的作用。
hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的;hashCode是用来在散列存储结构中确定对象的存储地址的。
7、ArrayList、LinkedList和Vector的区别。
ArrayList(貌似默认容量是10)和Vector(貌似默认容量是10)底层都是通过数组实现的,LinkedList底层是通过双链表(element,pre,next)实现的。
首先比较ArrayList和Vector:
- ArrayList是在内存不够的时默认是扩展50%+1,Vector是默认扩展一倍。
- Vector提供indexof(obj,start) 该矢量中位置索引处或以后的矢量中第一次出现对象参数的索引;如果找不到对象,则返回-1。ArrayList没有。
- Vector属于线程安全的,但大多数情况下不使用Vector,因为线程安全需要更大的系统消耗。
ArrayList和LinkedList的区别:
- 对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是 统一的,分配一个内部Entry对象。
- 在ArrayList的中插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList中插入或删除一个元素的开销是固定的。
- LinkedList不支持高效的随机元素访问。
- ArrayList的空间浪费主要体现在在List列表的结尾预留移动的容量空间,而在LinkedList的空间消费则体现在每一个元素都需要消耗相当的空间。
如果只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是对其它指定位置的插入、删除操作,最好选择LinkedList。
8、String、StirngBuilder和StringBuffer的区别。
String是字符串常量,StringBuffer和StringBuilder是字符串变量。
StringBuilder非线程安全(单线程使用),String和StringBuffer线程安全的(多线程使用)
如果不是多线程的,StringBuilder效率高于StringBuffer。
9、Map、Set、List、Queue和Stack的特点与用法。
Map:
Map是键值对,键Key是唯一不能重复的,一个键对应一个值,值可以重复。
TreeMap可以保证顺序,HashMap不保证顺序,即为无序的。
Map中可以将Key和Value单独抽取出来,其中KeySet()方法可以将所有的keys抽取出一个Set。而Values()方法可以将map中 所有的values抽取成一个集合Collecti-on。Set:
不包含重复元素的集合,set中最多包含一个null元素
只能用Iterator实现单向遍历,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有序链表查询的优化:(SkipList跳表)
使用跳表:
核心思想:
通过“空间来换取时间”的一个算法,通过在每个节点中增加了向前的指针,从而提升查找的效率。
原理:
跳跃列表是一种由多条链表组合而成的允许快速查询有序元素的数据结构。跳跃列表中的每条链表表示一个层,每一层存在 元素都是前一层(前一层是下方的层,从低至高生成跳跃列表)的存在元素的子集,在列表的头和尾有存在于每个层上的起 始点和结束点,作为每层链表的头结点和尾结点。
每层中出现的元素都是以常数概率随机存在的,如果当前层出现了该元素,根据“每一层存在的元素都是前一层的存在元素的 子集”的特点,该层的前几层都具有该元素,而且每个元素都会维护一个指向下一层该元素的指针。在整个跳跃列表的最高层 的头结点处有一个起始指针,当我们要查询某个值时,从起始指针出发,由最高层开始向前查找,如果判断下一个元素大于查 找元素则通过当前元素指向下一层的指针进入下一层再向前进行查找。如此往复直到查找到我们需要的元素为止。
跳表的满足条件:
- 由多条链表组成多个层组成,层数以log2N层为宜
- 第一层包含全部元素
- 如果元素x最高存在于y层,则y层以下所有层均包含x
- 每层元素都有一个指向下一层拥有相同值的元素的指针
- 在每层中都需要包含头结点和尾结点
- 起始结点指向最高层头结点
时间复杂度和空间复杂度:
跳表层数期望——O(logN),时间复杂度——O(logN),空间复杂度——O(N)。
相比二叉平衡树跳表的优势:
1、跳跃列表的固定概率p由你定义
抽象的来说,我们可以把二叉平衡树看作是p=1/2的跳表(只是简单的对比,实际上这两种数据结构有很大差异)。相对于跳跃列表而言,我们可以自己规定p的值,选择更加稀疏或是稠密的查找方式,在我们需要查找的数据量不同时,这更具有灵活性。
2、跳跃列表是有序的链表
有的时候堆排序和二叉树之所以不被使用不是因为效率的问题,而是因为这种数据结构很难利用计算机缓存特点,而且这类数据结构没有一个真正意义上的有序的表。我们知道大部分数据库的实现都是采用B+树,B+树优于B树的其中一个原因就是B+树在叶节点拥有串联起来的一份有序的链表。在很多情况下这也是跳表被更多使用的一个原因。
3、跳跃列表比平衡树更容易实现
大部分二叉平衡树的插入删除很难以实现,而且每次插入删除都要做大量的旋转平衡。相比较于二叉平衡树复杂的实现代码,跳跃列表则更加简单,因为概率本来是随机性的值,我们为某个新加入的值设置最高层的时候顾虑要小得多。
10、HashMap和HashTable的区别。
- 继承的父类不同
HashTable继承自Dictionary类,而HashMap继承自AbstractMap类。但两者都实现了Map接口。
- 线程安全性不同
HashTable时线程安全的(Synchronize),HashMap是线程不安全的。
- 是否提供contains方法
HashMap把HashTable的contains方法去掉了,改成了contatinsValue和containsKey,因为contains方法容易让人误解。HashTable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
- Key和value是否允许NULL值
Key和value都是对象,并且不能包含重复key,但可以包含重复的value。
HashTable中,key和value都不能出现null值。
HashMap中,null可以作为键,这样的键只能有一个;可以有一个或多个键对应的值为null。在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
- 两个遍历方式的内部实现上不同
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。
- Hash值不同
HashTable计算hash值,直接用key的hashCode(对长度取模),但是HashMap重新计算了key的hash值,通过hash&long的最大值(将hash值转换为正值),再对length取模。
- 内部实现使用的数组初始化和扩容方式不同
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16(降低Hash碰撞的几率),Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。
11、HashMap和Concurrent的区别,HashMap的底层实现。
区别:
- HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。
- ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。
- ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。
HashMap底层实现:
为什么加载因子默认为0.75?
使用随机哈希码,节点出现的频率在Hash桶总遵循泊松分布,当桶中的元素到达8个的时候概率已经变得非常小了,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的。
底层:数组+链表/红黑树
红黑树:
平衡二叉树实现复杂,插入删除效率低,不利于实践,因此提出红黑树。红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树(自平衡二叉搜索树)。满足二叉查找树的特征:任意一个节点所包含的键值,大于等于左孩子的键值,小于等于右孩子的键值。红黑树的每个节点上都有存储位表示节点的颜色,颜色是红(Red)或黑(Black)。
红黑树的特性:
1) 每个节点或者是黑色,或者是红;
2) 根节点是黑色;
3) 每个叶子节点是黑色; [注意:这里叶子节点,是指为空的叶子节点!]
4) 如果一个节点是红色的,则它的子节点必须是黑色的;
5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对接近平衡的二叉树)
Q1:为什么TreeMap采用红黑树而不是二叉查找树?红黑树是自平衡二叉搜索树。
其实这个问题就是在问红黑树相对于排序二叉树的优点。我们都知道排序二叉树虽然可以快速检索,但在最坏的情况下:如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点(如果插入节点集本身是大到小排列);或所有节点只有右节点(如果插入节点集本身是小到大排列)。在这种情况下,排序二叉树就变成了普通链表,其检索效率就会很差。
Q2:TreeMap、TreeSet 对比 HashMap、HashSet的优缺点?
缺点:
对于 TreeMap 而言,由于它底层采用一棵“红黑树”来保存集合中的 Entry,这意味这 TreeMap 添加元素、取出元素的性能都比 HashMap (O(1))低:
当 TreeMap 添加元素时,需要通过循环找到新增 Entry 的插入位置,因此比较耗性能(O(logN))
当从 TreeMap 中取出元素时,需要通过循环才能找到合适的 Entry,也比较耗性能(O(logN))
优点:
TreeMap 中的所有 Entry 总是按 key 根据指定排序规则保持有序状态,TreeSet 中所有元素总是根据指定排序规则保持有序状态。
12、TreeMap、HashMap和LinkedHashMap的区别。
- HashMap中k的值没有顺序,常用来做统计。
- LinkedHashMap它内部有一个链表,保持Key插入的顺序。迭代的时候,也是按照插入顺序迭代,而且迭代比HashMap快。
- TreeMap的顺序是Key的自然顺序(如整数从小到大),也可以指定比较函数。但不是插入的顺序。
13、Collection包结构,与Collections的区别。
14、try catch finally,try里有return,finally还执行吗?
在JAVA语言的异常处理中,finally里面代码块是为了保证无论出现了什么样的情况,finally里的代码一定会被执行。但是return 的意思就是结束当前函数的调用并跳出这个函数,因此finally块里面的代码也是在return前执行,如果try finally里都有return,那么 finally中的return将覆盖其他地方的return.
注意:
如果try中有return,但是finally中对return的值进行修改。那么最终返回的是修改前的数值还是修改后的数值?
运行时,在try中,要返回的结果已经准备好了,就在这个时候,程序跳到了finally,这个时候结果已经放到了x的局部变量中,执行完finally后,再取出结果,finally对x进行了改变,但不会影响返回的结果。
15、Exception和Error包结构。OOM你遇到过哪些,SOF你遇到过哪些?
Java将可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常(RuntimeException),错误(Error)。
- 运行时异常
特点:java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。
常见运行时异常:
ArithmeticException(除数为零异常)
IndexOutOfBoundsException(数组越界异常)
NullPointerException(空指针异常)
ClassCastException(类转换异常)
ArrayStoreException(数据存储异常,操作数组时类型不一致)
虽然Java编译器不会检查运行时异常,但是我们也可以通过throws进行声明抛出,也可以通过try-catch对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。例如,若会发生除数为零的情况,则需要通过代码避免该情况的发生!
- 被检查异常
特点:java编译器会检查它,此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。
CloneNotSupport:当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。
ClassNotFoundException:无法找到指定的类异常。
IOException:
FileNotFoundException:未找到指定文件引起异常。
EOFException:未完成输入操作即遇文件结束引起异常。
被检查异常通常都是可以恢复的。
- 错误
特点:和运行时异常一样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。
VirtualMachineError:OutOfMemoryError、StackOverFlowError
OOM:
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
- Java Heap溢出(Java heap spacess)
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。
如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。
- 虚拟机和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这里需要注意当栈的大小越大可分配的线程数就越少。
- 运行时常量池溢出(PermGenspace)
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
- 方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。
异常信息:java.lang.OutOfMemoryError:PermGenspace
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。
SOF:
当应用程序递归太深而发生堆栈溢出时,抛出该错误。
因为栈一般默认为1-2mb,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1mb而导致溢出。
栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。
16、JAVA面向对象的三个特征及含义。
继承:
继承是子对象可以继承父对象的属性和行为,并进行扩展。(特殊到一般)
封装:
1、将类中的属性和方法定义为私有;
2、对外提供一个操作这些属性,方法的接口;
3、面向对象系统的封装单位是对象,类概念本身具有封装的意义,因为对象的特性是由它所属的类说明来描述的。
作用:
1、其他对象不能直接修改和读取本对象所拥有的属性和方法,从而有效的避免了外部错误对它的“交叉感染”;
2、当对象的内部做了某些修改时,由于它只通过少量的接口对外提供服务,因此大大的减少了内部的修改对外部的影响。
多态:
多态分为编译时多态和运行时多态:(重载和重写)
编译时多态:编译器编译期间,根据根据同一对象或类中相同方法名的不同参数列表来决定调用哪个方法。
运行时多态:程序运行期间,同一方法会根据调用对象的不同,分别去执行其实际的相应方法。
动态绑定:运行期间判断所引用对象的实际类型,根据其实际类型调用其相应的方法。
重载:
1、参数列表不一致;
2、可以有不同的返回类型和访问修饰符;
3、可以抛出不同的异常;
4、存在于同类中。
重写:
1、方法名、参数列表和返回值相同;
2、子类方法不能缩小父类方法的访问权限;
3、不能抛出新的异常且抛出的异常可以更加宽泛;
4、存在于子类和父类之间;
5、方法被定义为final不能重写。
17、接口和抽象类的区别。
1、一个类只能继承一个抽象类,一个接口可以实现多个接口;
2、抽象类可以有自己的数据成员,也可以有非抽象的成员方法,接口中只能定义常量且所有方法都是抽象的。
3、抽象类可以有构造方法,接口不能。抽象类不能直接创建抽象类的实例对象,但实例化子类的时候,就会初始化父类,不管父类是不是抽象类都会调用父类的构造方法,初始化一个类,先初始化父类。
4、抽象类中的抽象方法的访问类型可以是public、protect、friendly,但是接口只能是public,默认是public abstract。
5、抽象类中可以包含静态方法,接口中不能。
应用场景:
抽象类表示的是“is-a”的关系(继承)
接口表示的是“has-a”的关系(组成)
18、static关键字。
作用:方便在没有创建对象的情况下来进行调用(方法/变量)。
- static变量
静态变量被所有的对象所共享,在内存中只有一个副本,他当且仅当类初始时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。Static成员变量的初始化顺序按照定义的顺序进行初始化。
- static方法
由于静态方法不依赖于任何对象就可以进行访问,static方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和成员方法.因为当static方法被调用时,这个类的对象可能还没被创建,即使已经被创建了,也无法确定调用哪个对象的方法.同理,static方法也不能访问非static类型的变量。
- static代码块
一般用于初始化类中的静态变量。Static代码块只执行一次。
- static和final结合使用
变量:使用 static + final 修饰,赋值后不能修改,通过类名访问.
方法:使用 static + final 修饰,该方法不能被覆盖,通过类名访问.
- static内部类
内部静态类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。
非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。
非静态内部类不能脱离外部类实体被创建,非静态内部类可以访问外部类的数据和方法。
- 静态导包
一般我们导入一个类都用 import com…..ClassName;而静态导入是这样:import static com…..ClassName.*;这里的多了个 static,还有就是类名ClassName后面多了个.* ,意思是导入这个类里的静态方法。当然也可以只导入某个静态方法,只要 把 .* 换成静态方法名就行了。然后在这个类中,就可以直接用方法名调用静态方法,而不必用ClassName.方法名的方式来调 用。简化一些操作。
Static关键字的误区:
- static关键字会改变类中成员的访问权限吗?
Java中的static关键字不会影响到变量或者方法的作用域。
- 能通过this访问静态变量吗?
静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问 权限足够)
- Static能作用于局部变量吗?
static是不允许用来修饰局部变量。
19、实现多线程的方法:Thread、Runnable和Callable。
继承Thread类、实现Runnable接口和实现Callable接口。
区别:
- Thread是Runnable接口的子类,实现Runnable接口的方式解决了Java单继承的局限。
- Runnable接口实现多线程比继承Thread类更加能描述数据共享的概念。
注意:通过实现Runnable接口解决了Java单继承的局限,所以不管其他的区别联系是什么,这一点就决定了多线程最好是通过实现Runnable接口的方式。
Start()和run()方法的区别:
如果直接调用run()方法会被当做一个普通的函数调用,程序中只有主线程这一个线程.start()方法能够异步的调用run()方 法,start()让该线程处于就绪状态而非运行状态
Runnable和Callable的区别:
- Callable规定的方法是call(),Runnable规定的方法是run()。其中Runnable可以提交给Thread来包装下,直接启动一个线程来执行,而Callable则一般都是提交给ExecuteService来执行。
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的
- call方法可以抛出异常,run方法不可以
- 运行Callable任务可以拿到一个Future对象,c表示异步计算的结果。
20、线程同步的方法。
Synchronized和lock的区别:
synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock();
synchronized适合在并发量较小的情况下;
Lock适合在高并发;
- 同步方法
通过synchronized修饰方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用方法前,需要获得内置锁,否则就处于阻塞状态。
注意:synchronized关键字也可以修饰静态方法,此时若果调用该静态方法,将会锁住整个类。
- 同步代码块
通过synchronized关键字修饰的语句块。被该关键字修饰的语句块自动被加上内置锁,从而实现同步。
注意:同步是一种高开销的操作,因此应该尽量减少同步的内容。
- Wait和notify
在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁标志,进入等待状态,并且可以调用notify()或者notifyAll()方法通知正在等待的其他线程。notify()通知等待队列中的第一个线程,notifyAll()通知的是等待队列中的所有线程。
- 使用特殊域变量(volatile)
volatile关键字为域变量的访问提供了一种免锁机制。
使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新。
volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取。
volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
禁止指令重排序。
注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。 用final域,有锁保护的域和volatile域可以避免非同步的问题。
- 使用重入锁(ReentrantLock)
ReentrantLock特性:
尝试获得锁(锁获取超时)tryLock。
获取到锁的线程响应中断lockInterruptibly。
ReentrantLock:
效果和synchronized一样,都可以同步执行,lock方法获得锁,unlock方法释放锁。
Await方法:
通过创建Condition对象来使线程wait,必须先执行lock.lock方法获得锁。
Signal方法:
Condition对象的signal方法可以唤醒wait线程。
创建多个Condition对象:
一个condition对象的signal(signalAll)方法和该对象的await方法是一一对应的,也就是一个condition对象的signal(signalAll)方法不能唤醒其他condition对象的await方法。
ReentrantLock类可以唤醒指定条件的线程,而object的唤醒是随机的。
Lock的公平锁和非公平锁:
公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先lock的线程不一定先获得锁。
tryLock和lock和lockInterruptibly的区别:
tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回false
lock能获得锁就返回true,不能的话一直等待获得锁
lock和lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常
ReentrantLock是完全互斥排他的,这样其实效率是不高的。
读写锁:
读读共享,写写互斥,读写互斥。
总结:
Lock类也可以实现线程同步,而Lock获得锁需要执行lock方法,释放锁需要执行unLock方法
Lock类可以创建Condition对象,Condition对象用来是线程等待和唤醒线程,需要注意的是Condition对象的唤醒的是用同一个Condition执行await方法的线程,所以也就可以实现唤醒指定类的线程
Lock类分公平锁和不公平锁,公平锁是按照加锁顺序来的,非公平锁是不按顺序的,也就是说先执行lock方法的锁不一定先获得锁
Lock类有读锁和写锁,读读共享,写写互斥,读写互斥
- 使用局部变量(ThreadLocal)
设计理念:
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程相互影响。
底层实现:
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量ThreadLocals,这个成员变量就是用来存储实际变量副本的,键值为当前THreadLocal变量,value为变量副本。
初始时,在Thread里面,threadlocals为空,当通过ThreadLocal变量调用get方法或者set方法,就会对Thread类 的ThreadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLacal要保留的副本变量为value,存到threadLocals中。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadlocals里面查找。
应用场景:数据库连接,session管理等。
- 使用阻塞队列(BlockingQueue)
阻塞队列提供了线程安全的访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。
阻塞队列的操作方法:
四种不同的行为方法解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。
BlockingQueue的实现类:
ArrayBlockQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象必须明确大小,像数组一样。
LinkedBlockQueue:一个可改变大小的阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象如果没有明确大小,默认值是Integer.MAX_VALUE。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
SynchronousQueue:同步队列。同步队列没有任何容量,每个插入必须等待另一个线程移除,反之亦然。
阻塞队列原理:
其实阻塞队列实现阻塞同步的方式很简单,使用的就是lock锁的多条件(condition)阻塞控制。使用BlockingQueue封装了根据条件阻塞线程的过程,而我们就不用关心繁琐的await/signal操作了。
public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity];//初始化类变量数组items lock = new ReentrantLock(fair);//初始化类变量锁lock notEmpty = lock.newCondition();//初始化类变量notEmpty Condition notFull = lock.newCondition();//初始化类变量notFull Condition } //添加元素的方法 public void put(E e) throws InterruptedException { checkNotFull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while(count == items.length) notFull.await(); //如果队列不满就入队 enqueue(e); } finally { lock.unlock(); } } //入队的方法 private void enqueue(E x) { final Object[] items = this.items; items[putIndex] = x; if(++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); } //移除元素的方法 public E take() throws InterruptedException{ final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try{ while(count == 0) notEmpty.await(); return dequeue(); }finally{ lock.unlock(); } } //出队的方法 private E dequeue(){ final Object[] itemsObjects = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; items[takeIndex] = null; if(++takeIndex == items.length) takeIndex = 0; count--; if(itrs != null) itrs.elementDequeued(); notFull.signal(); return x; }
生产者消费者模式的三种实现方式:
背景:
生产者生产数据到缓冲区中,消费者从缓冲区中取数据。如果缓冲区已经满了,则生产者线程阻塞;如果缓冲区为空,那么消费者线程阻塞。
- 阻塞队列实现生产者消费者模式
阻塞队列的最常用的例子就是生产者和消费者模式,且是最优的选择:
package producerConsumer; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; //使用阻塞队列BlockingQueue解决生产者消费者 public class BlockingQueueConsumerProducer { public static void main(String[] args) { Resource3 resource = new Resource3(); //生产者线程 ProducerThread3 p = new ProducerThread3(resource); //多个消费者 ConsumerThread3 c1 = new ConsumerThread3(resource); ConsumerThread3 c2 = new ConsumerThread3(resource); ConsumerThread3 c3 = new ConsumerThread3(resource); p.start(); c1.start(); c2.start(); c3.start(); } } /** * 消费者线程 * @author tangzhijing * */ class ConsumerThread3 extends Thread { private Resource3 resource3; public ConsumerThread3(Resource3 resource) { this.resource3 = resource; //setName("消费者"); } public void run() { while (true) { try { Thread.sleep((long) (1000 * Math.random())); } catch (InterruptedException e) { e.printStackTrace(); } resource3.remove(); } } } /** * 生产者线程 * @author tangzhijing * */ class ProducerThread3 extends Thread{ private Resource3 resource3; public ProducerThread3(Resource3 resource) { this.resource3 = resource; //setName("生产者"); } public void run() { while (true) { try { Thread.sleep((long) (1000 * Math.random())); } catch (InterruptedException e) { e.printStackTrace(); } resource3.add(); } } } class Resource3{ private BlockingQueue resourceQueue = new LinkedBlockingQueue(10); /** * 向资源池中添加资源 */ public void add(){ try { resourceQueue.put(1); System.out.println("生产者" + Thread.currentThread().getName() + "生产一件资源," + "当前资源池有" + resourceQueue.size() + "个资源"); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 向资源池中移除资源 */ public void remove(){ try { resourceQueue.take(); System.out.println("消费者" + Thread.currentThread().getName() + "消耗一件资源," + "当前资源池有" + resourceQueue.size() + "个资源"); } catch (InterruptedException e) { e.printStackTrace(); } } }
- synchronized、wait和notify
package producerConsumer; //wait 和 notify public class ProducerConsumerWithWaitNofity { public static void main(String[] args) { Resource resource = new Resource(); //生产者线程 ProducerThread p1 = new ProducerThread(resource); ProducerThread p2 = new ProducerThread(resource); ProducerThread p3 = new ProducerThread(resource); //消费者线程 ConsumerThread c1 = new ConsumerThread(resource); //ConsumerThread c2 = new ConsumerThread(resource); //ConsumerThread c3 = new ConsumerThread(resource); p1.start(); p2.start(); p3.start(); c1.start(); //c2.start(); //c3.start(); } } /** * 公共资源类 * @author * */ class Resource{//重要 //当前资源数量 private int num = 0; //资源池中允许存放的资源数目 private int size = 10; /** * 从资源池中取走资源 */ public synchronized void remove(){ if(num > 0){ num--; System.out.println("消费者" + Thread.currentThread().getName() + "消耗一件资源," + "当前线程池有" + num + "个"); notifyAll();//通知生产者生产资源 }else{ try { //如果没有资源,则消费者进入等待状态 wait(); System.out.println("消费者" + Thread.currentThread().getName() + "线程进入等待状态"); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 向资源池中添加资源 */ public synchronized void add(){ if(num < size){ num++; System.out.println(Thread.currentThread().getName() + "生产一件资源,当前资源池有" + num + "个"); //通知等待的消费者 notifyAll(); }else{ //如果当前资源池中有10件资源 try{ wait();//生产者进入等待状态,并释放锁 System.out.println(Thread.currentThread().getName()+"线程进入等待"); }catch(InterruptedException e){ e.printStackTrace(); } } } } /** * 消费者线程 */ class ConsumerThread extends Thread{ private Resource resource; public ConsumerThread(Resource resource){ this.resource = resource; } @Override public void run() { while(true){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } resource.remove(); } } } /** * 生产者线程 */ class ProducerThread extends Thread{ private Resource resource; public ProducerThread(Resource resource){ this.resource = resource; } @Override public void run() { //不断地生产资源 while(true){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } resource.add(); } } }
- lock和condition的await、signalAll
package producerConsumer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 使用Lock 和 Condition解决生产者消费者问题 * @author tangzhijing * */ public class LockCondition { public static void main(String[] args) { Lock lock = new ReentrantLock(); Condition producerCondition = lock.newCondition(); Condition consumerCondition = lock.newCondition(); Resource2 resource = new Resource2(lock,producerCondition,consumerCondition); //生产者线程 ProducerThread2 producer1 = new ProducerThread2(resource); //消费者线程 ConsumerThread2 consumer1 = new ConsumerThread2(resource); ConsumerThread2 consumer2 = new ConsumerThread2(resource); ConsumerThread2 consumer3 = new ConsumerThread2(resource); producer1.start(); consumer1.start(); consumer2.start(); consumer3.start(); } } /** * 消费者线程 */ class ConsumerThread2 extends Thread{ private Resource2 resource; public ConsumerThread2(Resource2 resource){ this.resource = resource; //setName("消费者"); } public void run(){ while(true){ try { Thread.sleep((long) (1000 * Math.random())); } catch (InterruptedException e) { e.printStackTrace(); } resource.remove(); } } } /** * 生产者线程 * @author tangzhijing * */ class ProducerThread2 extends Thread{ private Resource2 resource; public ProducerThread2(Resource2 resource){ this.resource = resource; setName("生产者"); } public void run(){ while(true){ try { Thread.sleep((long) (1000 * Math.random())); } catch (InterruptedException e) { e.printStackTrace(); } resource.add(); } } } /** * 公共资源类 * @author tangzhijing * */ class Resource2{ private int num = 0;//当前资源数量 private int size = 10;//资源池中允许存放的资源数目 private Lock lock; private Condition producerCondition; private Condition consumerCondition; public Resource2(Lock lock, Condition producerCondition, Condition consumerCondition) { this.lock = lock; this.producerCondition = producerCondition; this.consumerCondition = consumerCondition; } /** * 向资源池中添加资源 */ public void add(){ lock.lock(); try{ if(num < size){ num++; System.out.println(Thread.currentThread().getName() + "生产一件资源,当前资源池有" + num + "个"); //唤醒等待的消费者 consumerCondition.signalAll(); }else{ //让生产者线程等待 try { producerCondition.await(); System.out.println(Thread.currentThread().getName() + "线程进入等待"); } catch (InterruptedException e) { e.printStackTrace(); } } }finally{ lock.unlock(); } } /** * 从资源池中取走资源 */ public void remove(){ lock.lock(); try{ if(num > 0){ num--; System.out.println("消费者" + Thread.currentThread().getName() + "消耗一件资源," + "当前资源池有" + num + "个"); producerCondition.signalAll();//唤醒等待的生产者 }else{ try { consumerCondition.await(); System.out.println(Thread.currentThread().getName() + "线程进入等待"); } catch (InterruptedException e) { e.printStackTrace(); }//让消费者等待 } }finally{ lock.unlock(); } } }
21、锁的等级:方法锁、对象锁和类锁。
对象锁(包括方法锁) 类锁
一个锁的是类对象,一个锁的是实例对象。
若类对象被lock,则类对象的所有同步方法全被lock;
若实例对象被lock,则该实例对象的所有同步方法全被lock。
22、ThreadPool用法和优势。
优势:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行;
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。线程池可以应对突然大爆发量的访问,通过有限个固定线程为大量的操作服务,减少创建和销毁线程所需的时间。
线程池的创建:
//创建一个定时任务的线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
//创建单线程的线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
//创建缓存线程池(重用先前的线程)
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//创建一个带有固定线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(4);
创建一个ThreadPoolExecutor线程池一般需要以下几个参数:
- corePoolSize(线程池的基本大小):
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
- maximumPoolSize(线程池最大大小):
线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime(线程活动保持时间):
线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
- TimeUnit(线程活动保持时间的单位):
可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS)等。
- workQueue(任务队列):
用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
- threadFactory:
用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
- handler(饱和策略):
当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。
我们尽量优先使用Executors提供的静态方法来创建线程池,如果Executors提供的方法无法满足要求,再自己通过ThreadPoolExecutor类来创建线程池。
向线程池提交任务:
1.我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。
threadsPool.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub }});
2.使用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(); }
线程池的关闭
我们可以通过调用线程池(ExecutorService的对象)的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。
线程池的工作流程
当提交一个新任务到线程池时,线程池的处理流程如下:
(我的理解:提交任务—>如果线程数未达到corePoolSize,则创建线程执行任务—>如果达到corePoolSize,仍让提交了任务,则会有任务等待,所以将任务保存在任务队列中,直到任务队列workQueue已满—>如果workQueue已满,仍然有任务提交,但未达到最大线程数,则继续创建线程执行任务,直到线程数达到maximumPoolSize,如果达到了maximumPoolSize,则根据饱和策略拒绝该任务。这也就解释了为什么有了corePoolSize还有maximumPoolSize的原因。)
线程的监控
通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。
23、wait和sleep的区别。
- 这两个方法来自不同的类分别是Thread和Object
- 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁)。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
- sleep必须捕获异常,在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有finally语句块)以及以后的代码。而wait,notify和notifyAll不需要捕获异常。
24、sleep和yield的区别。
1)执行sleep()方法的线程在制定时间内肯定不会被执行,yield()方法使得当前线程回到可执行状态,所以调用yield()方法的线程可能进入到可执行状态后马上又被执行.
2)sleep()方法给其他线程运行时不考虑优先级,yield()方法只会给同优先级或者更高优先级的线程以运行的机会
25、foreach与for效率比对。
在固定长度或长度不需要计算的时候for循环效率高于foreach.。
在不确定长度,或计算长度有性能损耗的时候,用foreach比较方便。
26、JAVA IO与NIO。
面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
27、反射的作用与原理。
作用:
在JAVA中,只有给定类的名字,就可以通过反射机制来获取类的所有信息,可以动态的创建对象和编译。
原理:
JAVA语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。
反射的实现主要借助以下四个类:
Class:类的对象
Constructor:类的构造方法
Field:类中的属性对象
Method:类中的方法对象
28、泛型的特点。
类型安全。
泛型的一个主要目标就是提高 Java 程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高。
消除强制类型转换。
泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。
向后兼容。
支持泛型的 Java 编译器(例如 JDK5.0 中的 Javac)可以用来编译经过泛型扩充的 Java 程序(GJ 程序),但是现有的没有使用泛型扩充的 Java 程序仍然可以用这些编译器来编译。
层次清晰,恪守规范。
无论被编译的源程序是否使用泛型扩充,编译生成的字节码均可被虚拟机接受并执行。也就是说不管编译器的输入是 GJ 程序,还是一般的 Java 程序,经过编译后的字节码都严格遵循《Java 虚拟机规范》中对字节码的要求。可见,泛型主要是在编译器层面实现的,它对于 Java 虚拟机是透明的。
性能收益。
目前来讲,用 GJ 编写的代码和一般的 Java 代码在效率上是非常接近的。 但是由于泛型会给 Java 编译器和虚拟机带来更多的类型信息,因此利用这些信息对 Java 程序做进一步优化将成为可能。
29、解析XML的工具,区别。
(1)DOM解析
DOM是html和xml的应用程序接口(API),以层次结构(类似于树型)来组织节点和信息片段,映射XML文档的结构,允许获取和操作文档的任意部分,是W3C的官方标准
【优点】
①允许应用程序对数据和结构做出更改。
②访问是双向的,可以在任何时候在树中上下导航,获取和操作任意部分的数据。
【缺点】
①通常需要加载整个XML文档来构造层次结构,消耗资源大。
(2)SAX(Simple API for XML)解析
流模型中的"推"模型分析方式。通过事件驱动,每发现一个节点就引发一个事件,事件推给事件处理器,通过回调方法完成解析工作,解析XML文档的逻辑需要应用程序完成
【优势】
①不需要等待所有数据都被处理,分析就能立即开始。
②只在读取数据时检查数据,不需要保存在内存中。
③可以在某个条件得到满足时停止解析,不必解析整个文档。
④效率和性能较高,能解析大于系统内存的文档。
【缺点】
①需要应用程序自己负责TAG的处理逻辑(例如维护父/子关系等),文档越复杂程序就越复杂。
②单向导航,无法定位文档层次,很难同时访问同一文档的不同部分数据,不支持XPath。
(3)DOM4J(Document Object Model for Java)
简单易用,采用Java集合框架,并完全支持DOM、SAX和JAXP
【优点】
①大量使用了Java集合类,方便Java开发人员,同时提供一些提高性能的替代方法。
②支持XPath。
③有很好的性能。
【缺点】
①大量使用了接口,API较为复杂。
(4)STAX(Streaming API for XML)
流模型中的"拉"模型分析方式。提供基于指针和基于迭代器两种方式的支持,JDK1.6新特性。
补充:
30、内部类。
内部类分为四种:
1)静态内部类:不依赖于外部类实例而被实例化,不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态方法(包括私有类型);
2)成员内部类:可以引用外部类的属性和方法,但是不可以定义静态的属性和方法,只有在外部类被实例化之后,才能被实例化;
3)局部内部类:不可以被public protected private static 修饰 只能访问方法中final类型的局部变量其余和1)2)特性相同;
4)匿名内部类:不能使用class,extends,implements,必须继承其他类或者实现其他接口. 匿名内部类不能有构造函数;不能有静态成员,方法和类;不能是public,protected,private,static;只能创建一个实例;和局部内部类的限制相同
31、JAVA位运算。
32、解决hash冲突。
1.Hash函数
散列函数:
1)直接地址2)乘法3)除留取余法4)分段叠加法5)伪随机6)查表7)平方取中法8)数字分析法
解决冲突的办法:
1)开放地址法:
a)线性探测法:
发生冲突,算法会便利hash表找到下一个空槽;
b)线性补偿探测法:
hash=(hash+Q)%m=hash%m+Q%m,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
c)伪随机探测
将线性探测的步长从常数改为随机数,即令: hash=(hash+RN)%m ,其中 RN 是一个随机数。
2)拉链法
引入链表
33、线程间的状态转换。
34、线程的终止。
stop()方法会释放已经锁定的所有监视资源,导致程序不确定,suspend()方法不会释放锁,会造成死锁.
一般建议使用某种方式让线程能够自动结束run()方法的执行.
Thread.interrupt()设置线程的终端标志位,在线程阻塞的时候抛出InterruptedException异常,中断线程
如果程序因为I/O停滞,无法通过interrupt()来使线程离开run().但可以用流的close()方法来关闭流,引发IO异常,run()捕获这个异常来结束线程
35、Join的作用。
join()是让线程在执行完run()方法后,在执行join方法等待线程结束,用于实现同步功能.
当A线程执行到了B线程的join()方法时,A就会等待,等B线程都运行完,A线程才会执行。使用join()方法时,会产生异常。
36、volatile与synchronized区别。
1)volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2)volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
3)volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性
4)volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
37、乐观锁和悲观锁。
悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
应用场景:读取频繁使用乐观锁,写入频繁使用悲观锁。
38、JAVA并发工具包concurrent的结构。
java.util.concurrent 包是专为 Java并发编程而设计的包。包下的所有类可以分为如下几大类:
• locks部分:显式锁(互斥锁和速写锁)相关;
• atomic部分:原子变量类相关,是构建非阻塞算法的基础;
• executor部分:线程池相关;
• collections部分:并发容器相关;
• tools部分:同步工具相关,如信号量、闭锁、栅栏等功能;