JAVA工程师面试技术点汇总(持续更新中)

来源:互联网 发布:0day扫描什么端口 编辑:程序博客网 时间:2024/05/16 12:34

一:mysql

1、mysql Nested-Loop算法,Block-Nested-Loop算法,join优化

答:Nested-Loop:选取(mysql自动优化选择)一个表作为驱动表,循环驱动表结果集,查询下一个表的数据,然后合并结果集。如果是多表join,则将前一次合并的结果作为循环数据,查询下一个表。
Block-Nested-Loop(默认开启):在NL算法的基础上,将外层循环的结果缓存起来,内层循环一次比较多条数据,减少总循环次数。比如,外层查询有100条结果,缓存10条,每次内层循环比较10条数据,则只需要循环10次。

优化:

    1. 使用小结果集作为驱动表,减少总循环次数(自动优化)
    2. 优先优化内层查询,减少每一次循环的时间
    3. 关联查询条件建立索引
    4. 设置适当的join_buffer_size,当join_buffer_size大于外层结果集时,再增大join_buffer_size不会变得更快

2、mysql索引原理

答:mysql使用B+Tree(平衡多路查找树)作为索引的数据结构,而innoDB和MyIsAm对索引的实现方式略有不同。使用B+Tree而不使用平衡二叉树、红黑树等结构的原因和计算机物理结构有关。索引本身需要存储,而索引一般比较大,因此索引往往存在磁盘中。而磁盘IO效率非常低,所以判断索引结构好坏的一个重要指标就是磁盘IO次数。计算机为提高磁盘IO效率,在读取数据时会进行预读,预读的大小为页的整数倍。数据库系统将B+Tree结点的大小刚好设置为一页的大小,因此一次IO就能完全载入一个结点。因为B+Tree的一个结点中会存储多个关键字,所以B+Tree的高度相比其他几种树会低很多,IO次数也会少很多。
MyIsAm索引:索引文件与数据文件是分离的。索引的叶子结点存储数据行的地址。因为相邻的叶子节点分配的物理地址并不一定相邻,所以这种索引是非聚簇索引。

InnoDB的主键索引:数据文件就是索引文件。索引的叶子结点存储完整的数据记录。逻辑相邻的数据行在物理上的存储也是相邻的,属于聚簇索引。若没有定义主键,则mysql选取第一个唯一非空的索引作为聚簇索引。若也没有唯一非空的索引,则会创建一个隐藏的聚簇索引。建表时最好使用无意义的自增列作为主键,每次插入数据只需要按顺序往后排即可。如果主键不是自增的,插入新结点时可能导致结点分裂(一个结点保存的数据记录超过一定大小就会分裂),进而导致后序其他的结点分裂,在数据量大的时候,效率非常低下。

InnoDB的普通索引:叶子结点存储主键值,普通索引的顺序与主键索引不一定一样,所以是非聚簇索引。

3、索引的选择性

答:索引的选择性=不重复的值得个数/总记录数,取值范围为(0,1]。索引的选择性越小,使用索引的效果越不明显(越接近全表扫描)。极端情况下,假设只有一种不重复的值,使用索引(需要扫描整个B+Tree或hash值查出的记录为所有记录)和全表扫描完全一样。

4、hash索引和B+Tree索引的区别

答:
  1. hash索引只能用于=、in和!=的查询,不能用于范围查询,不能用于排序。因为hash索引通过查找hash值一样的数据来进行索引,而hash之前的值大小与hash值大小不一定存在对应关系。
  2. 对于组合索引,不能利用前面的一个或多个索引查询
  3. hash存在冲突的情况,因此查到一个hash值之后,还需要在多个数据记录之间选择
  4. 除上述情况外,hash索引效率远高于B+Tree

5、mvcc(多版本并发控制)机制

答:mvcc只有在Read Commited和Repeatable Read隔离级别下有效。在Read Uncommited下,每次读都是当前读(读最新行,而不是符合系统版本号的行),在Sirializable级别下,所有读都加锁,因此这两种级别不适用mvcc。在InnoDB的 mvcc是通过在每行记录后面保存两个隐藏的列来实现的。这两个列一个保存了行的创建时系统版本号,一个保存了行的删除时系统版本号。每开始一个新事务,系统版本号都会递增。事务开始时的系统版本号就是事务版本号。下面是crud的mvcc实现(注:在事务a、b并发执行的情况下,假设事务a读取行,事务b插入(更新、删除)行。若事务b先执行,则会锁行,事务a会等b执行完之后读取最新结果,这种情况与mvcc机制无关。若事务a先执行,b是插入操作时,b的版本号大于a,a不能查到新增的行;b是更新操作时,新增的行版本号大于a,读到的仍是原始行;b是删除操作时,删除版本号大于a的版本号,仍能查到b删除的这行。以上情况无论b是否已经提交都成立,这样就解决了快照读下的脏读、不可重复读以及幻读):
  1. select:只有符合以下两个条件的记录才会作为返回结果
    • 行的创建版本号小于或等于事务版本号。这可以确保事务读取的行,要么是事务开始前已经存在的,要么是事务自身插入或者修改过得。。
    • 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读到的行,在事务开始之前未被删除。
  2. insert:新插入的每一行保存当前系统版本号作为行版本号。
  3. delete:删除的每一行保存当前系统版本号作为行删除版本号。
  4. update:插入一条新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号作为原来的行的删除版本号。

6、如何解决当前读引起的幻读

答:mvcc无法解决因当前读(增、删、改均先有一次当前读)引起的幻读。示例:
  1. 事务a:select * from table_name
  2. 事务b:insert into table_name (a) values (1),commit b
  3. 事务a:select * from table_name
  4. 事务a:update table_name set a=1 where a=1
  5. 事务a:select * from table_name,commit a
1、3之间虽然被事务b插入了一条数据,但这两次查询结果是一样的,这符合mvcc的规则,此时没有出现幻读。执行4的时候,由于update是当前读,会读到事务b提交的最新数据,这次更新操作会把新行的版本号和原始行的删除版本号设置为事务a的版本号。因此,执行查询5的时候,不能查到b新增的行(删除版本号相等),却查出了更新之后的新行(行版本号相等)。同一个事务里面的三次查询,第三次查到的结果和第一第二次查询结果不同,也可以算是幻读。
解决方法:为查询加共享锁LOCK IN SHARE MODE或排它锁FOR UPDATE,如select * from table_name lock in share mode,若字段a没有唯一索引,还需要使用RR隔离级别(在InnoDB中,由于mvcc的存在,RR与RC的唯一区别就是RR使用间隙锁解决了当前读的幻读问题)。

7、InnoDB的锁

答:根据锁的获取权限,可以分为共享锁s,排它锁x,以及意向共享锁is,意向排它锁ix。根据锁的范围,可以分为Record Lock:锁定一行记录的索引;Gap Lock:锁定所在记录行的索引两侧的不存在的索引;Next-Key Lock:上述两种加起来。
  1. 共享锁和排它锁锁定行,意向锁锁定表。加锁顺序为先加意向锁,获得意向锁之后再加其他锁。这四种锁的兼容关系如下:

由图可知,一个事务获取了意向锁,其他事务也可以获取该表的意向锁。一个事务获取了共享锁之后,其他事务只能获取意向共享锁和共享锁。一个事务获取排它锁之后,其他事务不能获取任何类型的锁。
  1. 锁是加在索引上的,不同类型的索引加的锁类型不同,示例语句delete from table_name where a = 8;
    • a是主键索引,在这条记录的索引上加行锁
    • a是非主键唯一索引,这条记录的唯一索引和主键索引均加行锁

    • a是非唯一索引,在RC隔离级别下,所有满足条件的记录均加行锁。在RR隔离级别下,除行锁外,还会加间隙锁(满足条件的值两侧的间隙),这两个锁合起来即Next-Key Lock。如数据库有记录5,8,9,则锁的范围是(5,9),因为如果再插入一条8,则新结点一定插入在(5,9)这个间隙的结点上。正是间隙锁解决了当前读的幻读问题。唯一索引不需要间隙锁是因为唯一索引下的同一个值只会插入一次。

    • a无索引,在RC隔离级别下,表的所有行加行锁。在RR隔离级别下,所有行加行锁和间隙锁。

8、如何避免死锁

答:死锁产生的原因是不同的事务获取锁的顺序相反(不一定非要多条sql语句才会形成死锁,如两个事务分别通过两个非主键索引更新数据,这两个索引对应的主键顺序刚好相反时就形成了死锁)。常见的避免死锁的方法有:
  1. 尽量以相同的顺序访问表,若某两个事务因使用不同的非主键索引引起死锁,可以尝试拆分sql语句,通过非主键索引查出主键,再用主键更新记录。
  2. 尝试升级锁定的颗粒度,通过表锁减少死锁的概率

二:java

1、内存模型

答:线程共享部分:方法区(在Hotspot中实现为永久代)、堆。线程独占部分:虚拟机栈、本地方法栈、程序计数器。在jdk1.8中,元数据区取代了永久代,类信息存放在本地内存中,常量和静态变量放在堆中。

2、CopyOnWriteArrayList和Vector或通过Collections.synchronizedList()获取到的SynchronizedList的区别

答:SynchronizedList和Vector属于同步容器,所有方法均加锁,并发性能很差。而CopyOnWriteArrayList只对写操作加锁,修改操作并不直接修改原始数组的内容,而是创建一个新数组并把原数组的引用指向新数组。由于该数组引用是volatile的并且更新和添加操作直接替换引用,因此不需要对读加锁。CopyOnWriteArrayList体现了读写分离的思想,对于并发读取的性能远高于SynchronizedList。CopyOnWriteArrayList没加读锁,因此获取到的数据不能保证实时一致,比如正在执行add操作,但还没有执行到数组赋值的那一行,则读到的还是add之前的数据。另外,CopyOnWriteArrayList占用内存较多,只有在遍历、获取操作远多于写操作是才考虑使用。SynchronizedList无法在遍历时修改(可以使用iterator的方法修改,但需要额外的同步处理),而CopyOnWriteArrayList可以(但不能通过iterator方法修改,其iterator没有提供相应的方法),但遍历时不会获取到修改的部分(遍历的是那一刻的数组副本)。

3、ConcurrentHashMap和HashTable或通过Collections.synchronizedMap()获取到的SynchronizedMap的区别

答:HashTable和SynchronizedMap属于同步容器,所有方法均加锁。ConcurrentHashMap不对整个方法加锁。get操作直接获取内存最新值,put操作若table的当前位置为空,则使用CAS插入新结点,若不为空,则对当前位置加锁后插入。ConcurrentHashMap相对同步容器大大增加了并发度。由于get、size等方法是无锁的,因此不能实时的获取(如get一个key的同时put,get获取不到但put操作完成后这个key是存在的),size计算的也不是准确值。同步容器只能在使用iterator遍历时,使用iterator的remove方法做删除操作,ConcurrentHashMap可以在遍历的同时调用put或remove。

4、HashMap的容量为什么是2的k次幂

答:HashMap的结构是一个链表(或红黑树)数组,不同的Node根据hash值散列在table数组中,hash值相同的Node组成一个链表(或红黑树)。table.length>64并且链表长度>8时,会将链表结构转化成红黑树,以此来提升hash值相同时的查找效率。当table.length>threshold(容量*负载因子),或者table.length<=64并且链表长度>8时,会发生扩容,每次扩容之后的容量都是2的k次幂(左移一位)。Node的hash值对table.length取模就可以得到Node在table中的下标,但取模运算%是非常耗时的。我们把table.length记为n,当n=2^k(2的k次幂)时满足下列等式:hash%n=hash&(2^k-1)=hash&(n-1),因此将table.length设置为2的k次幂即可把低效的取模运算转化成高效的位运算。

5、HashMap resize原理

答:
  1. 该链表只有一个链表结点,直接rehash
  2. 该结点是树结点,略。。
  3. 该链表有多个结点:由于是根据取模操作计算位置,而不同的hash值取模可能相同,因此不同的hash值也可能存在同一个链表上。而扩容之后hash值对newCap取模与原来不一定相同,因此不能简单的移动整个链表。HashMap根据hash&(n-1)计算key在table中的位置,其中n=2^k,扩容后n=2^(k+1),因此n-1与扩容前相比在高位多了一个1.若hash在对应的位置为0,则&操作之后与扩容前相同;若hash在对应的位置为1,则&操作之后相当于增加了100....(等于oldCap的值)。即原链表上的结点在扩容之后的位置只有两种可能,要么还在原位置,要么在原位置+oldCap的位置。遍历原链表,根据hash对应oldCap的最高位是0还是1分成两个链表。下图a为扩容前两个hash值对n-1做&操作的过程,图b为扩容后的过程。

6、JAVA的CAS(compare and swap)

答:cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级 是:寄存器-高速缓存-内存。多个线程并发执行时,缓存里面的值可能和内存值不一致,因此导致计算结果错误。以下几种方式可以确保线程安全

  1. synchronized是悲观锁,在锁定范围内无论未来是否会发生冲突都同时只能有一个线程可以执行。获取锁的时候把读取内存值到缓存,释放锁的时候把缓存写入内存。
  2. 使用volatile关键字,每次更新操作都写入内存,因此可以保证更新操作对其他线程可见,但不能保证复合操作的原子性,如i++这种读-改-写操作。如果修饰的是对象或者数组,只能保证引用的可见性,不能保证对象的属性或数组元素可见性。
  3. 使用CAS。CAS操作:只有当内存值V和预期值A相同时,才将内存值V修改成新值B,否则不修改。下图是Unsafe类中的一个方法,这个方法使用CAS实现了i+=n的操作。这种使用死循环进行cas操作代替互斥锁(如synchronize,需要进行挂起操作)的方式也称为cas自旋锁。这个方法相当于乐观锁,并不在一开始就加锁,而是发生冲突时等待直到冲突解决。CAS通常与volatile或getIntVolatile()一起使用,先获取内存值,然后在CAS中判断刚刚获取到的值是否过期,没过期则修改。使用CAS算法可以保证单个变量的复合操作也是原子的,但不能保证多个变量的操作原子性,多个变量的原子性只能使用synchronize。JAVA中可以通过Unsafe类执行CAS操作(Unsafe封装了很多直接操作内存的方法,类似C语言),但并不建议直接使用Unsafe类。jdk并发相关的类中大量使用了Unsafe类。

7、线程池原理

答:线程池ExecutorService可以通过Executors的工厂方法创建,返回一个ThreadPoolExecutor对象。ThreadPoolExecutor中保存了两个数据结构,即BlockingQueue和HashSet。BlockingQueue中保存的是通过submit()或execute()方法生产的任务,HashSet中保存的是消费者Worker。每个Worker都会新建一个线程,并无限循环调用BlockingQueue的take()方法获取任务执行。当Worker执行任务被中断时跳出无限循环,并将该worker从HashSet中清除。调用shutdown()方法会把线程池状态设置为SHUTDOWN,并中断空闲的Worker(worker执行任务时加锁,shutdown()时会尝试获取锁,获取不到锁则不中断)。shutdownNow()方法把线程池状态设置为STOP,并中断所有Worker。只有RUNNING状态才能提交任务,因此调用shutdown()和shutdownNow()之后不能继续添加任务。而中断worker是通过调用worker中保存的Thread的interrupt()方法做到的,如果任务中没有因wait、join、sleep、可中断的I/O操作(NIO)等而阻塞,并且没有人为地判断线程中断标志,则线程仍会继续执行,因此执行shutdownNow()之后程序并不一定会立即停止运行。由于线程池创建的线程是用户线程并在线程内部无限循环,不会主动结束,因此如果不调用shutdown()或shutdownNow()程序将永远不会停止。
  1. 固定大小的线程池(如newFixedThreadPool())中的BlockingQueue是LinkedBlockingQueue,提交任务时,若Worker没有达到最大数量,则新建一个Worker并保存到HashSet中,同时在该Worker对象中执行本次任务,否则将任务放入BlockQueue,BlockQueue的大小没有限制。
  2. 计划任务线程池newScheduledThreadPool()与固定大小线程池类似,只是BlockingQueue的类型为DelayedWorkQueue。
  3. cache类型的线程池(如newCachedThreadPool())中的BlockingQueue是SynchronousQueue,提交新任务时首先判断能否将任务放入阻塞队列中(即有空闲的Worker在等待任务。对于SynchronousQueue,只有当存在消费者调用take()方法等待生产者时,offer()或add()方法才返回true;反之亦然,只有当生产者调用put()方法等待消费者时,poll()方法才不返回null。生产者和消费者一一匹配执行,队列中可以保存的任务数量为0),放入队列失败则新建Worker并保存在HashSet中,Worker数量没有限制。

8、在线程池中使用ThreadLocal

答:ThreadLocal为每一个线程保存一个变量副本,在线程池中由于线程会重复利用,并不是一个线程对应一个任务,而是一个线程对应多个任务,因此在任务中使用完ThreadLocal之后必须将值清除掉以避免多个任务获取到的值是相同的。

三:web

1、get和post的区别

答:

    1. get只读,gost非只读 
    2. get幂等(多次请求和一次请求效果一样),post非幂等
    3. get可缓存,post不可缓存 
    4. get请求会出现在url中 
    5. get只能发送普通字符,post可以发送二进制 
    6. get请求长度有限制(2k),post无限制

2、http1.0,1.1,2.0区别

答:

    1. 1.0默认不是长连接,如果需要长连接需手动设置keep-alive,1.1默认长连接。
    2. 1.1比1.0多了host字段
    3. 1.1可以只发送头信息,不带body(服务器返回100或者401)
    4. 2.0使用多路复用技术,一个链接可以同时处理多个请求。
    5. 2.0支持header数据压缩
    6. 2.0支持服务器推送:一个请求可以有多个响应,可以提前缓存其他页面需要的内容

3、tcp三次握手四次挥手

四:算法&数据结构

1、根据二叉树的两种(前中,后中)遍历结果画出二叉树

答:先序遍历为根左右,中序遍历为左根右,后序遍历为左右根。先序遍历的第一个值就是根结点,后序遍历最后一个值是根结点,在中序遍历中找到根结点,左边的是左子树,右边的是右子树。分别在左右子树中列出前中(后中)序列,重复上述方法直到所有结点都找到位置。如先序4,2,1,0,3,5,9,7,6,8,中序0,1,2,3,4,5,6,7,8,9。根据先序确定根结点是4,根据中序确定左子树为0123,右子树为56789。左子树0123(中序)的先序为2103,确定2为根结点,左子树为01,右子树为3。一直循环下去即可画出完整的树。再如后序0,1,3,2,6,8,7,9,5,4,根据后序知道根为4,根据中序知道左子树为0123,左子树的后序为0132,则左子树的根为2,依次循环即可。

2、根据二叉树给出先(中、后)序遍历结果

答:

根据箭头流向,第一次经过的结点序列就是先序结果,第二次经过的结点序列是中序结果,第三次经过的结点序列是后序结果

3、二叉排序树的删除

答:二叉排序树满足左结点比根小,右结点比根大,因此整个树最左边的结点是最小的,最右边的结点是最大的。删除结点分三种情况:
  1. 该结点p是叶子结点:删除叶子结点不影响树的性质,直接删掉即可。
  2. 该结点p只有一个子节点c,用c替换该结点即可。如:p是父结点f的左孩子,则把c设置为f的左孩子;p是f的右孩子,则把c设置为f的右孩子。
  3. 该结点有两个子结点。用左结点cl替换该结点,然后将右结点cr设置为cl的最右边子树的右结点cmax(cr大于cl中最大的子结点cmax)。

4、一致性hash算法

5、链表转红黑树

6、B+Tree与B-Tree

答:
  1. n阶(叉)B+Tree包含n个关键字,而B-Tree是n-1个
  2. b-的关键字在结点中只出现一次,而b+可能出现多次。b+的非叶子结点只是叶子结点的索引,叶子结点增加一个脸指针,中包含所有的关键字
  3. b-可以在非叶子结点命中,b+只能在叶子结点命中


五:linux

1、io模型

0 0
原创粉丝点击