Java高级工程师面试要点

AOP

AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。

一 AOP的基本概念

(1)Aspect(切面):通常是一个类,里面可以定义切入点和通知

(2)JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用

(3)Advice(通知):AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around

(4)Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式

(5)AOP代理:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类

二 Spring AOP

Spring中的AOP代理还是离不开Spring的IOC容器,代理的生成,管理及其依赖关系都是由IOC容器负责,Spring默认使用JDK动态代理,在需要代理类而不是代理接口的时候,Spring会自动切换为使用CGLIB代理,不过现在的项目都是面向接口编程,所以JDK动态代理相对来说用的还是多一些。

2.通知类型介绍

(1)Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可

(2)AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值

(3)AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名

来访问目标方法中所抛出的异常对象

(4)After:在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式

(5)Around:环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint

IOC

2.1、IoC(控制反转)
  首先想说说IoC(Inversion of Control,控制反转)。这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好、qq号、电话号、ip号、iq号………,想办法认识她们,投其所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。传统的程序开发也是如此,在一个对象中,如果要使用另外的对象,就必须得到它(自己new一个,或者从JNDI中查询一个),使用完之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类藕合起来。

  那么IoC是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友,比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个mm,我们只需要去和她谈恋爱、结婚就行了。简单明了,如果婚介给我们的人选不符合要求,我们就会抛出异常。整个过程不再由我自己控制,而是有婚介这样一个类似容器的机构来控制。Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

2.2、DI(依赖注入)
  IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。

  理解了IoC和DI的概念后,一切都将变得简单明了,剩下的工作只是在spring的框架中堆积木而已。

线程池

线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,有以下好处:

1、降低资源消耗;

2、提高响应速度;

3、提高线程的可管理性。

Java1.5中引入的Executor框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。

ThreadPoolExecutor

Exectors工厂类提供了线程池的初始化接口,主要有如下几种:

newFixedThreadPool

初始化一个指定线程数的线程池,其中corePoolSize == maximumPoolSize,使用LinkedBlockingQuene作为阻塞队列,不过当线程池没有可执行任务时,也不会释放线程。

newCachedThreadPool

1、初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;

2、和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;

所以,使用该线程池时,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。

newSingleThreadExecutor

初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行,内部使用LinkedBlockingQueue作为阻塞队列。

newScheduledThreadPool

初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。

实现原理

除了newScheduledThreadPool的内部实现特殊一点之外,其它几个线程池都是基于ThreadPoolExecutor类实现的。

分布式锁

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。

基于数据库做分布式锁

基于乐观锁

利用主键唯一的特性,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,当方法执行完毕之后,想要释放锁的话,删除这条数据库记录即可。

基于悲观锁

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁 (注意: InnoDB 引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给要执行的方法字段名添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

基于 Redis 做分布式锁

setnx()

setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。

expire()

expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

几种MAP

HashMap

最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为Null(多条会覆盖);允许多条记录的值为 Null。非同步的。

TreeMap

能够把它保存的记录根据键(key)排序,默认是按升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。TreeMap不允许key的值为null。非同步的。
Hashtable

与 HashMap类似,不同的是:key和value的值均不允许为null;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢。
LinkedHashMap

保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.在遍历的时候会比HashMap慢。key和value均允许为空,非同步的。

ConcurrentHashMap

ConcurrentHashMap是一个线程安全,并且是一个高效的HashMap。
但是,如果从线程安全的角度来说,HashTable已经是一个线程安全的HashMap,那推出ConcurrentHashMap的意义又是什么呢?
说起ConcurrentHashMap,就不得不先提及下HashMap在线程不安全的表现,以及HashTable的效率!

HashMap 与 HashTable 区别
默认容量不同,扩容不同
线程安全性:HashTable 安全
效率不同:HashTable 要慢,因为加锁

《《如何高效学习》》阅读笔记

整体性学习策略

整体性学习的关键在于创建信息的网络,类似于我们的大脑的神经网络,建立知识之间的关联。

结构

结构好比大脑中的城市,他包含了知识之间的联系。

数学知识和语言知识就是最大的城市,例如北京、上海。

模型

模型就好像城市中的建筑物。

模型的目的是压缩信息。

书的目录就是压缩信息,看了目录基本就知道这本书讲什么了。

高速公路

好比城市之间的高速公路。这里比喻的是知识之间的联系。

比如我看这本书,我需要用到我的语言知识(汉语),生物知识(神经网络),那么我学习的如何学习的知识实际上是和其他知识建立了连接的。

以下是高速公路的表现形式

  1. 感知结构

声音、图像和情感。

  1. 关系结构

人与人之间的关系。

  1. 基础数学结构

如果有人不明白次序增长的意义。那下面这个他就会明白

1.01的1000次方等于20959,微小的量变会引起质变。

  1. 比喻法

学习计划

最近在多抓鱼上买了些书。

如何高效学习

学习到底有没有方法,我觉得是有的,不然怎么解释有的人学的又快又好。其实人与人之间的差别还没有那么大。

代码大全

这本书可以看作是个编程范式的集合,如何写出优雅、健壮的代码。

重构

在大多数时候我们都是在维护代码,重构可以说是我们工作中很重要的一部分。

设计模式

要写出优雅、健壮的代码有时候还是需要些设计模式的,倒不是说不用就不行了,这毕竟是受大家认可的方法。

计算机程序的构造和解释

以我个人的理解,这是一本关于编程的哲学书,在不一样的角度去看待编程,所以这本书用什么语言去描述都不重要了。

编码

这本书通过讲故事的方法去理解深奥的计算机系统。

算法

基本上是一部算法方面的教科书、拿来作为参考资料很不错,最主要的是本书图文并茂。

深入理解计算机系统

这本书知识点讲的很细,很适合想要学习计算机的底层知识。

《计算机科学导论》读后感

感觉?

这本书给我最大的感觉就是不会用很多枯燥的数学公式或理论去讲述知识。

她会有很多图再配上很多的实例,写的通俗易懂。

适合什么人读?

我感觉如果是计算机相关专业的话,那么书上的知识多多少少都学过或者了解。但是我仍然推荐阅读,其一是把知识梳理了一遍,其二是因为作者独特的写作能让你有不一样的体会。

当然由于本书只是导论,所以写的很浅显,如果要深入学习里面的知识,还是需要看相关的书籍。

如果不是计算机专业的,通过阅读本书能对计算机系统有个大致了解,不会犯一些低级错误。

给MongoDB设置密码

启动MongoDB

1
2

mongod --dbpath ~/data/db

连接MongoDB

1
2
3
4
5
6
7
mongo

use admin;

db.createUser({ user: "admin", pwd: "qw3erTYU", roles: [{ role: "userAdminAnyDatabase", db: "admin" }] })

db.auth("admin", "qw3erTYU")

退出MongoDB

1
mongod --dbpath ~/data/db --auth

连接数据库

1
2
3
4
5
use admin;

db.auth("admin", "qw3erTYU")

db.createUser({ user: "huiqia", pwd: "huiqia", roles: [{ role: "readWrite", db: ["doc", "crawler"] }] })

Ubuntu 18.04 安装Docker和Docker Componse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sudo apt-get remove docker docker-engine docker.io

sudo apt-get update

sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -


sudo apt-key fingerprint 0EBFCD88

sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"

sudo apt-get update

sudo apt-get install docker-ce

apt-cache madison docker-ce

sudo apt-get install docker-ce=<VERSION>
1
2
3
sudo curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

sudo chmod +x /usr/local/bin/docker-compose

保持SSH连接

在我的工作中,总会有远程连接服务器的场景。

但是如果有一段时间没有活跃的会话的话,连接就会断开。每次都得重新连接。很麻烦,有没有方法可以永不断开呢?

有。但是需要服务器和客户端都得配置。


Server

1
2
3
4
5
6
7
8
9
10
11
12
13
# 打开sshd的配置文件
vim /etc/ssh/sshd_config

# 保持连接
TCPKeepAlive yes
# 心跳检测间隔
ClientAliveInterval 60
# 最大失败次数,超过这个得不到响应就断开
ClientAliveCountMax 3
# 不适用密码认证
PasswordAuthentication no

sudo service sshd restart

Client

1
2
3
4
5
vim ~/.ssh/config

# 和服务器的设置同理
ServerAliveInterval 60
ServerAliveCountMax 3

这样妈妈再也不用担心连接断开了。

从零开始写数据库(四)

实现LevelDB的日志系统

参考设计文档Log Format

前面创建了一个数据库,但是数据是存储在一个csv文件中,在实际的项目中,数据是以二进制的形式存储

我们最终的目的是写一个类似levelDB的数据库,我们先要了解下LevelDB的运作机制

在LevelDB中写入数据大概是这样几步

  1. 接收到请求Write(k, v)
  2. 把请求写入日志Op log
  3. 把数据写入MemTable
  4. 第三步完成就可以通知客户端写入成功了
  5. 当memtable的数据达到临界值的时候,转变成immutable memtable
  6. 压缩immutable memtable
  7. 写入sstable

这个信息量有点大,我们先来解释下几个名词

Op log

这个Op是对数据做改变的操作,所以PutDelete都会写入Op log

op log文件是以块的形式存储的,每个块的大小固定为32KB,每个文件会有1个和多个块,这里我们简单点一个Block对应一个文件

具体来说,最终会在磁盘上像0.log1.log2.log的形式存储

Op Log的格式

1
2
3
4
5
6
7
8
9
10
11
+---------+-----------+-----------+--- ... ---+
|CRC (4B) | Size (2B) | Type (1B) | Payload |
+---------+-----------+-----------+--- ... ---+

CRC = 32bit hash computed over the payload using CRC
Size = Length of the payload data
Type = Type of record
(kZeroType, kFullType, kFirstType, kLastType, kMiddleType )
The type is used to group a bunch of records together to represent
blocks that are larger than kBlockSize
Payload = Byte stream as long as specified by the payload size

我们先生成定义日志的格式

record.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Record struct {
CRC uint32
Size uint16
Type uint8
Payload []byte
}
// 新建记录,根据类型和数据
func NewRecord(typ uint8, payload []byte) Record {
// 新建数据缓冲区,长度是type的长度加上数据的长度,type一个字节长度
buf := bytes.NewBuffer(make([]byte, 1 + len(payload)));

buf.WriteByte(typ)
buf.Write(payload)

// 计算crc32
crc := crc32.ChecksumIEEE(buf.Bytes())

return Record{
crc,
uint16(len(payload)),
typ,
payload,
}
}