首页 > 学院 > 开发设计 > 正文

面经总结

2019-11-06 06:51:44
字体:
来源:转载
供稿:网友

一、操作系统

1. 介绍一下信号量和互斥锁

信号量是非负数,只有两个操作wait,signal 互斥量是0,1,只能用于一个资源的互斥访问 互斥量用于线程的互斥,信号线用于线程的同步。

有人做过如下类比: Mutex是一把钥匙,一个人拿了就可进入一个房间,出来的时候把钥匙交给队列的第一个,一般的用法是用于串行化对临界区代码的访问,保证这段代码不会被并行的运行。 Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。

2. 介绍一下死锁

是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁的发生必须具备以下四个必要条件:

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

3. 线程的五大状态

新建状态(New): 当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。就绪状态(Runnable): 一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。运行状态(Running):当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.阻塞状态(Blocked):所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。死亡状态(Dead)有两个原因会导致线程死亡: 1) run方法正常退出而自然死亡, 2) 一个未捕获的异常终止了run方法而使线程猝死。 为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

二、计算机网络

1.tcp的三次握手,四次挥手

三次握手过程:

第一次:A给B发送一个数据包,其中syn=1,ack=0,seq=1234(随机),A进入SYN_SENT状态。

第二次:B给A发送一个数据包,其中syn=1,ack=1,ack number=1235(第一个数据包的seq+1,表示确认了),seq=5678(随机),B进入SYN_RCVD状态。

第三次:A给B发送一个数据包,其中ack=1,ack number=5679(表示B给我的请求已经确认),这时同时进入ESTABLISHED状态。

为啥不能是两次或四次呢?

因为至少3次通信才能保证两方的发信和收信功能都是正确的。

第一次:A方发送功能正常(因为发出去了)

第二次:B方接收功能正常(因为看到了A的请求)、B方发送功能也正常(因为发出去了)

第三次:A方接收功能正常(因为看到了B的请求)

知乎上看到一个段子形象的描述了三次握手:

三次握手: A:“喂,你听得到吗?” B:“我听得到呀,你听得到我吗?” A:“我能听到你,今天balabala……” 两次握手:(注意此时A听不到B说话!!) A:“喂,你听得到吗?” B:“我听得到呀” A:“喂喂,你听得到吗?” B:“草,我听得到呀!!!!” A:“你TM能不能听到我讲话啊!!喂!” “……” 四次握手: A:“喂,你听得到吗?” B:“我听得到呀,你听得到我吗?” A:“我能听到你,你能听到我吗?” B:“……不想跟傻逼说话” 作者:匿名用户 链接:https://www.zhihu.com/question/24853633/answer/114872771 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

四次挥手过程:

第一次:A给B发送一个fin,进入FIN_WAIT_1状态,序号为1234(随机)。

第二次:B收到了A的fin,发送ACK,序号为1235,进入CLOSE_WAIT状态。

第三次:B再发送一个fin,进入LAST_ACK状态,序号为5678(随机)。(表示B不再接收A的数据,只接收最后一个ACK,但还会发数据)

第四次:A收到了fin后,发送一个ACK给B,序号为5679,B收到后进入CLOSED状态。(表示A不再接收B的数据,但还会发)

为啥是四次呢?

这个答案比较简单了,因为保证双向通信的关闭。即A->B和B->A都要关闭,每次都要双方确认。

三、java编程

(一) 考查Java源码

1. ArrayList

ArrayList继承了抽象类AbstractList,实现了List,Randomaccess,Cloneable,Serializable接口。

RandomAccess标识其支持快速随机访问;Cloneable标识其支持对象复制;Serializable标识其可序列化;

然后介绍一下具体实现:

首先底层是用一个数组实现的,数组的长度默认为10,不过也可以在new 的时候指定。

那么说说增删改查吧~~

增嘛,就是add方法了,我们在lc里都非常熟悉。然后ArrayList的add源码是酱紫:

public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true;}

所以是先调用ensureCapacityInternal方法再往数组里面加东西。那么ensureCapacityInternal方法又长什么样呢?

PRivate void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity);}

首先判断数组是不是空的,若是,则确保数组容量至少为初始值和size+1的较大值,然后再进入ensureExplicitCapacity方法。

private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity);}

modCount我们已经很熟悉了,记录修改次数的,防止在多线程环境下读写冲突,这里因为add操作调用了他,所以modCount++了。

然后就是关键了,如果minCapacity(传进来的是size+1)比数组长度小,那就装不下了,要grow。

grow方法如下:

private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);}

恩我们看到了,在这里是首先把旧容量扩充到原来的1.5倍,然后讨论了两种情况: * 扩充了还是不够:那么就扩大到minCapacity。 * 新容量比MAX_ARRAY_SIZE(这是个常量,为231−9)还大,那就扩大到Integer.MAX_VALUE,如果还是不够,抛出OutOfMemoryError异常。

然后把数组搬过去即可。

说完add再说与之对应的remove。

public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue;}

首先检查一下index是不是不合法的,这里我觉得怪怪的。。。。。

讲道理来说,对一个数组的越界访问不管是偏大还是偏小都要抛出IndexOutOfBoundsException,但是在这段代码里面,如果index偏大,是在rangeCheck方法里面抛出的,而如果是偏小(例如传一个负数),则是在引用elementData[index]里面抛出的,不知道这样做的理由是什么。如果需要使用方法来约束数组范围,那么负值也要考虑的;如果在引用数组下标elementData[index]`时约束范围,那这个rangeCheck方法存在的意义又是什么?

源码中的解释是:It is always used immediately prior to an array access,which throws an ArrayIndexOutOfBoundsException if index is negative.哪位读者能解释一下? 如果下标是正确的,那么修改次数+1,然后取出旧值,把后面的往前搬一下,清空数组末尾的值(置空)。

然后Add还有这样的方法:

public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++;}

可是这里的rangeCheckForAdd又检测负值了。。。。。。。。。。。。。

改和查很简单,贴一下源码就可以了。

public E get(int index) { rangeCheck(index); return elementData(index);}public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue;}

indexOf和lastIndexOf也很简单,就是一个从头找, 一个从尾找。都是遍历操作。

2.LinkedList

LinkedList的底层实现是个双向链表,其中节点类描述如下:

private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }

然后对象中维护了头结点和尾节点,因此就可以得到任意一个节点了。

同理,我们还是从增删改查四个方面研究LinkedList:

先说add,二话不说上源码:

public boolean add(E e) { linkLast(e); return true;}void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++;}

相当简单,学过数据结构里面链表相关章节的都很容易看明白。

然后上remove源码:

public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false;}

这里就是讨论了一下节点内储存了null的特殊情况,然后关键部分在unlink方法:

E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element;}

同样也很简单,就是数据结构里面学过的双端链表的删除操作,需要注意的就是特殊情况,也就是要删除的节点在边上。

因为是双端链表实现的嘛,LinkedList比ArrayList多了addFirst,addLast,removeFirst,removeLast方法,内部实现都差不多也就不再赘述。

然后get和set,先上源码 :

public E get(int index) { checkElementIndex(index); return node(index).item;}public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal;}

这个也都比较简单,同样是先检查index是否越界再get或set。

最后他俩的区别就是我们在数据结构课上非常熟悉的,顺序存储和随机存储的优势啦~~ArrayList是随机存储的,所以get和set会快,而LinkedList是顺序存储的,所以add和remove快。

3.ThreadLocal

这东西比较简单,他是对一个类的封装,被ThreadLocal封装的变量每一个线程都有一个独立的副本,每一个线程都可以独立改变自己的副本而不影响别的线程。

在以前,底层实现是一个HashMap,key是当前线程,value是该实例。

但是现在的设计思路改了!!现在的底层实现是Thread个HashMap,每个HashMap的key是这个ThreadLocal实例,value是那个对象的副本。

为什么这样搞呢?如果是原来的设计方案,那么在大型项目里有很多Thread和很多ThreadLocal的前提下,就会有ThreadLocal个HashMap,每个里面就有Thread个元素。在Thread很多的情况下性能会低。

还有一点,当一个线程停止时,对应的ThreadLocal副本都不存在了,可以销毁一个HashMap。但用第一种设计思路的话这些HashMap都在。

4. sleep()和wait()

sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行.

wait是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者,例如:

sleep和wait的区别有:

这两个方法来自不同的类分别是Thread和Object最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 synchronized(x){ x.notify() //或者wait() }sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

5.finalize()方法

finalize()是Object的protected方法,有点像c++里面的析构函数,它是在对象被回收之前调用的。垃圾回收器准备释放内存的时候,会先调用finalize()。

但是和C++的析构函数不太一样的是,C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性。

6. Integer 的缓存机制

在Java编译器中,把原始类型(8种)自动转换为封装类的过程,称为自动装箱,相当于valueOf方法。

例如:

Integer i=10;这行代码是等价于Integer i=Integer.valueOf(10);的。根据jdk源码,valueOf方法是先去一个叫IntegerCache类(是一个Integer数组cache[]的再封装)里面查找这个数是否被缓存了,如果有,直接拿出来,如果没有则new一个对象。

IntegerCache的缓存范围是从-128到high,这个high是通过JVM的启动参数指定的。 那么看两道题:

public class Hello { public static void main(String[] args) { int a = 1000, b = 1000; System.out.println(a == b); //true,因为是基本类型的比较 Integer c = 1000, d = 1000; System.out.println(c == d); //false,因为1000是没缓存的,系统调用了两次new,而对象类型比较的是地址,所以false Integer e = 100, f = 100; System.out.println(e == f); //true,100是缓存的,所以直接从cache里面拿的,是同一个地址 } } public class Main { public static void main(String[] args){ int a=0; Integer b=0; Integer c=new Integer(0); Integer d=Integer.valueOf(0); System.out.println(a==b);//true,因为涉及到基本类型的比较,要解包 System.out.println(a==c); System.out.println(b==c);//b是缓存区里拿出来的,c是new出来的,所以false System.out.println(c==d);//d也是缓存区里拿出来的,所以b==d是true,c==d是false System.out.println(b==d); }}

(二)设计模式

1. 单例模式

单例模式有以下特点: 1. 单例类只能有一个实例。 2. 单例类必须自己创建自己的唯一实例。 3. 单例类必须给所有其他对象提供这一实例。

单例模式有两种,分别叫做饱汉式和饿汉式。(起名字的一定是个粗糙的大叔,叫萌萌的妹子不好么?)

饱汉式就是说,这个人有钱,资源很多,所以什么时候需要这个实例什么时候new,写法如下:

public class SingletonDemo{ private static SingletonDemo instance = null; private SingletonDemo(){ } public static SingletonDemo getInstance(){ if(instance==null){ instance = new SingletonDemo(); } return instance; }}

饿汉式就是说,这个人很没钱,怕饿死,所以在类加载的时候就new好,写法如下:

public class SingletonDemo{ private static SingletonDemo instance = new SingletonDemo(); private SingletonDemo(){ } public static SingletonDemo getInstance(){ return instance; }}

很显然,如果是大型项目,饱汉式的单例类很多的情况下,会导致系统的启动很慢。饿汉式的问题是在多线程下会出现线程不安全的情况。因为如果多个线程同时运行到if(instance==null)这句里面,就都进来了,这样就new了好多instance。

解决方法有3种:

方法一(利用同步方法):把饱汉式的public static Singleton getInstance()加上synchronized关键字,但这种的粒度太大了,在并发很高的情况下会导致性能降低。 我们只需要控制new 语句就可以了,因此请看方法二:

方法二(双重同步锁):

public static SingletonDemo getInstance(){ if(null == instance ) { synchronized(SingletonDemo.class){ if(null == instance) instance = new SingletonDemo(); } } return instance;}

首先解释一下,为什么要判断两遍呢?进入同步块不是已经确保了instance==null么?别忘了这是并发环境啊~~~接下来是见证奇迹的时刻!!

假设去掉同步块里面的if语句啊。。。。设两个线程1和2,同时调用了getInstance()方法,由于instance==null成立,则线程1进入同步块,线程2等待,那么线程1退出同步块的时候线程2就进入了,可是此时instance已经new出来了,线程2又new 了一遍!!!

所以这两个if语句看起来逼格很高吧~~表示已经被帅到有没有~

但是,这种方法也有问题!!!!

问题出在哪里呢?这个希望能背下来,如果被问到了说不定可以hold住面试官哦~~

当然这种问题出现的前提是该单例类是很复杂的。假设线程A执行到了第2行,那么instance==null正确,进入同步块,然后他开始new这个实例,但是 这里假设new 的开销非常大!!! 我们都知道jvm加载一个类是先寻找二进制文件,创建Class对象,再生成引用,再赋值、执行构造方法等操作。

那么静态代码块非常复杂的时候,线程B也执行到了第二行的时候,已经生成了引用,那么instance 不为null,可是此时该实例还没初始化完全啊!!就return了!!对不对!!

那么有什么办法呢,由此提出了方法三~~

方法三:静态内部类

public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; }}

这个用的是内部类,首先外部类被加载的时候内部类不立即加载,这样就做到了延迟加载,而JVM在初始内部类的时候已经实现了线程安全,而不需要关键字来限定。

(三)JVM的问题

1. JVM的内存模型

JVM的基本结构分为数据区、本地方法接口、执行引擎、类加载器。 text

数据区是最重要的一部分,分为如下几部分:

(1)虚拟机栈VM Stack: 它主要用来存储线程执行过程中的局部变量,方法的返回值,以及方法调用上下文。每一次函数调用都对应一个栈帧(stack frame),用于存储当次函数调用的局部变量、返回值等信息。栈区不够用了会抛出StackOverflowError,一般由于函数调用层数太高(如死递归)导致。每一个线程对应一个栈区。

(2)堆区Heap:它是所有线程共享的,用来保存各种对象。如数组,实例等等,然后根据存在时间还可以分为新生代和老生代。

(3)方法区Method:也是所有线程共享的,用于存储已被加载的类信息、常量、静态变量等信息。这里面还有个运行时常量池Runtime Constant Pool,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。(How to understand?)

(4)程序计数器PC Register:这块内存空间比较小,作用就是存储当前线程所执行的代码行号。是线程独立的。

(5) 本地方法栈Native Method Stack:类似于VM Stack,但是这里是为Native方法服务,也是线程独立的。

执行引擎:负责执行class文件中包含的字节码指令.

本地方法接口:主要是调用C或C++实现的本地方法及返回结果。

类加载器:在JVM启动时或者在类运行时将需要的class加载到JVM中。

2. GC

GC主要有两大块内容,一是垃圾检测,二是垃圾回收。所谓垃圾检测,就是系统判断什么内存空间是垃圾,一般有两种方法: * 引用计数法:对每个对象增加一个计数器,该对象被引用了就+1,没被引用就-1. 可是这样有一个很严重的问题,例如有两个对象ObjA和ObjB,他们之间是互相耦合的,但是没有其他对象引用他们。事实上他们已经是垃圾了,可是引用计数不为0,就不能回收。 * 可达性分析算法:以根对象GC Root为起点进行搜索,如果有对象是不可达的,就是垃圾。GC Root集包括栈区中引用的、常量池中引用的、静态区引用的对象等等。

垃圾检测说完了,那么说说是怎么回收的,如果判定一个对象从Gc Root是不可达的,系统就会回收这些对象。那么如何回收呢,下面由浅入深介绍几种策略:

(1)标记-清除(Mark-sweep):

若一个对象是垃圾,则标记起来,然后统一回收。但是这样会产生大量碎片,如果再new一个很大的对象还要触发内存整理。

(2)复制(Copying):

把内存分为两部分,每次使用其中一个区域。当这一块的内存需要清理了,则复制到另一个区域中。这样就不产生碎片,但是有一半内存空间浪费了。

(3) 标记-整理(Mark-Compact): 标记的过程同策略1,但是清除垃圾对象的同时,把不是垃圾的对象“压缩”到一起,这样也解决了碎片,且不浪费一半空间。

分代gc:

为什么要分代回收?-

因为在实际工程中,不同对象的生命周期不一样,例如socket连接、线程等对象是要从工程启动开始就始终持续的,而如String List等对象往往使用一次就成为垃圾了。因此需要将对象按生命周期划分,不同周期使用不同策略。

如何按周期划分?

将对象按其生命周期的不同划分成:年轻代(Young Generation)年老代(Old Generation)持久代(Permanent Generation)。然后Young又细分为Eden,From Survivor,To Survivor三部分,一个新对象优先是放在Eden区的,Eden区满了的时候,触发一次MinorGC,这时”存活”的对象将进入Survivor区,同时也判定一下Survivor区存活对象的”年龄”。对象在Survivor区每“熬过”一次MinorGC,就增加一岁。如果增加到一定年龄(默认为15岁),则进入Old Generation.

如下图所示。(图转侵删。)

Alt text

由图可知,两个Survivor区是对称的,没有先后之分。

那么老年代满了怎么办?那就触发一次Full GC,回收整个堆内存。

由上面介绍可知,年轻代的gc用到了复制算法, 老年代的gc用到的是标记-整理算法。

垃圾回收器

垃圾回收器是内存回收的具体实现。《深入理解jvm》一书中以hotspot JVM为例,介绍了7种收集器。

Serial(串行GC)收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

ParNew(并行GC)收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。

Parallel Scavenge(并行回收GC)收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

Serial Old(串行GC)收集器

Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。

Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS(并发GC)收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

①.初始标记(CMS initial mark)②.并发标记(CMS concurrenr mark)③.重新标记(CMS remark)④.并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,主要有三个显著缺点:* CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。 * CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。 * 最后一个缺点,CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。

G1收集器

G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。

3. 类加载

四、算法与数据结构

1. 找出一个字符串中第一个只出现一次的字符

例如:”abcdeab”,找出”c”.

思路:先遍历一遍字符串,用m[26]统计每个字符出现的次数,然后再扫一遍字符串只要m[a[i]-‘a’]==1就返回a[i].

2.将m个有序链表合并成一个有序链表(Leetcode 23.Merge K sorted lists)

http://blog.csdn.net/cmershen/article/details/51550304

方法一:把这些链表扔到优先队列里面,然后每次取出的链表都是头元素最小的那个,然后删掉头结点再扔回队列里面去,直到队列全空

/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */public class Solution { public ListNode mergeKLists(ListNode[] lists) { if (lists==null||lists.length==0) return null; PriorityQueue<ListNode> queue= new PriorityQueue<ListNode>(lists.length,new Comparator<ListNode>(){ @Override public int compare(ListNode o1,ListNode o2){ if (o1.val<o2.val) return -1; else if (o1.val==o2.val) return 0; else return 1; } }); ListNode dummy = new ListNode(0); ListNode tail=dummy; for (ListNode node:lists) if (node!=null) queue.add(node); while (!queue.isEmpty()){ tail.next=queue.poll(); tail=tail.next; if (tail.next!=null) queue.add(tail.next); } return dummy.next; }}

方法二:归并排序(待完善)

3.最大连续子数组和(Leetcode 53,一定背下来!!)

Kanade算法:

用dp[n]代表结尾为n的连续子数组的和最大值,则有如下转移方程: dp[n]={dp[n−1]+a[n]a[n]dp[n−1]>0others

即如果以n-1为结尾的子数组和是>0的,则前n-1项对n为结尾是“有贡献”的,那么以n为结尾的子列和就带上前面那个数组。否则从第n项开始。最后返回dp[n]里面最大值即可。因为dp[n]只跟dp[n-1]有关,故维护dp[n-1]一个变量就可以了。

public class Solution { public int maxSubArray(int[] nums) { int maxsum = -9999999; int sumk = 0; for(int num : nums) { sumk = (sumk + num > num) ? sumk + num : num; maxsum = (maxsum > sumk) ? maxsum : sumk; } return maxsum; }}

4.求A的B次方的后三位

快速幂取模算法,我都有模板了,其实本质就是二进制展开,如果这一位是0就没关系,如果是1就乘进去

http://blog.csdn.net/u013486414/article/details/42318931

#include <bits/stdc++.h>using namespace std;typedef long long ll;int main() { int a,b; cin>>a>>b; int ans=1; while(b) { if(b&1) ans=(ans*a)%1000; a=(a*a)%1000; b>>=1; } cout<<ans<<endl;}

5.翻转单链表(Leetcode 206)

先把头结点后面的链表翻转过来,再把头结点安到末尾即可。

ListNode* reverseList(ListNode* head) { if(!head || !head->next) return head; else { ListNode *temp=head->next; ListNode *newHead=reverseList(head->next); temp->next=head; head->next=NULL; return newHead; }}

6.Two sum(Leetcode 1)

方法一:排序+双指针,时间O(nlogn),空间1(这样找不到下标)

方法二:用Map存储(如果不要求下标的话set也行),扫两遍数组,第一遍记录有哪些数(以及下标),第二遍去map(set)里面查sum-i在不在

五、其他

1.聚簇索引和非聚簇索引的区别

聚集索引:表数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序一致。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页。 在一张表上最多只能创建一个聚集索引,因为真实数据的物理顺序只能有一种。

非聚集索引:表数据存储顺序与索引顺序无关。对于非聚集索引,叶结点包含索引字段值及指向数据页数据行的逻辑指针,其行数量与数据表行数据量一致。

2.数据库事务

事务(transaction)是由一系列操作序列构成的程序执行单元,这些操作要么都做,要么都不做,是一个不可分割的工作单位。 数据库事务的四个基本性质(ACID) 1. 原子性(Atomicity) 事务的原子性是指事务中包含的所有操作要么全做,要么全不做(all or none)。

一致性(Consistency) 在事务开始以前,数据库处于一致性的状态,事务结束后,数据库也必须处于一致性状态。

拿银行转账来说,一致性要求事务的执行不应改变A、B 两个账户的金额总和。如果没有这种一致性要求,转账过程中就会发生钱无中生有,或者不翼而飞的现象。事务应该把数据库从一个一致性状态转换到另外一个一致性状态。

隔离性(Isolation) 事务隔离性要求系统必须保证事务不受其他并发执行的事务的影响,也即要达到这样一种效果:对于任何一对事务T1 和 T2,在事务 T1 看来,T2 要么在 T1 开始之前已经结束,要么在 T1 完成之后才开始执行。这样,每个事务都感觉不到系统中有其他事务在并发地执行。

持久性(Durability) 一个事务一旦成功完成,它对数据库的改变必须是永久的,即便是在系统遇到故障的情况下也不会丢失。数据的重要性决定了事务持久性的重要性。

事务并发执行,不加锁,会引起的四个问题:(线程不加锁也会因此这四个问题)

1、丢失修改 事务T1,T2读取同一个数据,之后先后将修改的内容,写回数据库,会导致一个事务丢失修改。 例子: 数据a = 1; T1, T2对a加1, 先后将读取a的之后, 又先后将2写进a,这样导致丢失修改。正确的a的值应该是3。

2、脏读 T1修改某个数据a,这是T2去读,之后T1撤销事务,a回到原来的值,这是T2读到的a的值就是一个错误的值,即脏数据。

3、不可重复读 T1读取了一个数据之后,之后T2修改了这数据,T1在读这个数据,发现和之前读的不相同。

4、幻读 T1按照某个条件从数据库中查找出了某些数据,之后T2对表的记录进行插入和删除,T1在按相同的条件从数据库中,查找数据,发现记录条数多了或者少了,就像出现幻觉一样。

3.Spring的IoC(控制反转)和AOP(面向切面)怎么理解?

IoC(Inversion of Control):IoC就是应用本身不依赖对象的创建和维护而是交给外部容器(这里为spring),这要就把应用和对象之间解耦,控制权交给了外部容器。看一个小例子:

package com.ljq.test;public interface HelloService { public void sayHello(); }

那么现在我们需要HelloService里面的方法,我们先配置一下helloworld.xml:

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- id 表示组件的名字,class表示组件类 --> <bean id="helloService" class="com.ljq.test.HelloServiceImpl" /></beans>

我们看到了, 这里配置的Bean是他的实现类HelloServiceImpl!!!!具体怎么实现的?I don’t care!!!

接下来我们在工程里使用这个方法:

public void testHelloWorld() { // 1、读取配置文件实例化一个IOC容器 applicationContext context = new ClassPathXmlApplicationContext("helloworld.xml"); // 2、从容器中获取Bean,注意此处完全“面向接口编程,而不是面向实现” HelloService helloService = context.getBean("helloService", HelloService.class); // 3、执行业务逻辑 helloService.sayHello();}

那么如果sayHello需要改动,我们只需要改HelloServiceImpl即可,大不了换了个其他类,这样就实现了低耦合。

AOP即面向切面编程,可以将一个与各组件关系不大的但需要大量重复的功能(如记录日志)以类似于“切片”的方式插入系统的组件中,实现代码的重用。

还是用一个例子来说明。一个人需要吃苹果,那么吃苹果之前要洗手。同理吃什么之前都要洗手,我们把”洗手”这个动作看做切片,注入到吃苹果方法的前面: 首先写一个接口IToDo,里面只有吃苹果一个方法:

package com.service.imp;public interface IToDo { public String toEat();}

实现出来,这里也只是输出吃苹果:

package com.service;import org.springframework.stereotype.Service;import com.service.imp.IToDo;@Servicepublic class ToDo implements IToDo { @Override public String toEat() { System.out.println("吃苹果"); return "吃苹果"; }}

接下来完成洗手方法的接口和实现类(略):

package com.service.imp;public interface ipreDo { public String toPre();}

applicationContext.xml配置

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> <context:component-scan base-package="com.service" /> <aop:aspectj-autoproxy /> <aop:config proxy-target-class="true"> <aop:aspect ref="preDo"> <aop:pointcut expression="execution(* com.service.ToDo.toEat(..))" id="register" /> <aop:before method="toPre" pointcut-ref="register" /> </aop:aspect> </aop:config></beans>

然后我们实现这个吃苹果的工程:

public class application { public static void main(String[] args) { ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext.xml"); IToDo tdo = (IToDo)appCtx.getBean("toDo"); tdo.toEat(); }}

显然从容器里拿出来了一个叫IToDo的Bean,在这里我们只看到了吃苹果,那么洗手是怎么来的呢?奥秘就在xml配置里面!

这里面配置了preDo类,作为com.service.ToDo.toEat方法的前置方法,且是在方法执行前切入。

那么如果我们吃梨、吃桃子、吃别的、以及其他需要洗手的动作执行前也加上洗手这一动作,只需要修改配置文件即可。不必在每个函数前面都加上洗手方法。

4.数据库的索引

数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。

在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表