Hi!请登陆

2万字Java高频面试指南带你通关跳槽季,复习1个月拿下大厂offer

2021-5-6 41 5/6

今日分享开始啦,请大家多多指教~

开春了,现在面试的人很多,对于面试有很多疑问。本篇文章整理了一些面试题目资料,将一些面试经历以及网络收集的高频问题总结发布,希望帮助各位快速达到面试状态。话不多说,正文开始啦~

1.项目相关

介绍一下你简历上写的项目?自己主要做了什么?

你觉得项目里给你最大的挑战是什么?遇到了什么问题?如何解决的?从中学到了什么?

项目里面会不断出现各种问题,比如数据量过大造成的内存溢出问题,如何让程序运行效率更高,如何证明我们的算法比别人的算法效率高,如何找到新的观点来支撑我们现有的理论,如何向导师和师兄进行沟通完成接下来的工作。

项目的架构图能画一下不?

觉得项目有哪些地方可以改进完善?(比如:可以加一个 redis 缓存把热点数据缓存起来)

有没有遇到过内存泄漏的场景?

2.基础问题

2.1 进程和线程的区别?

a)进程是资源分配的最小单位,线程是任务执行的最小单位。

b)进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此 CPU 切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

c)线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

d)但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

2.2 进程的调度算法有哪些?(主要)

a)先来先去服务

b)时间片轮转法

c)短作业优先

d)多级反馈队列调度算法

e)优先级调度

2.3 常用 IO 模型?

关注消息通信机制:

a)同步:调用一个功能,在功能结果没有返回之前,一直等待结果返回。

b)异步:调用一个功能,调用立刻返回,但调用者不能立刻得到结果。调用者可以继续后续的操作,其结果一般通过状态,回调函数来通知调用者。

等待调用结果时的状态:

c)阻塞:调用一个函数,当调用结果返回之前,当前线程会被挂起,只有得到结果之后才会返回。

d)非阻塞:调用一个函数,不能立刻得到结果之前,调用不能阻塞当前线程。一个输入操作通常包括两个阶段:

等待数据准备好

从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

e)阻塞 IO 模型:应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。

f)非阻塞IO模型:进程发起 IO 系统调用后,内核返回一个错误码而不会被阻塞;应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成。如果内核缓冲区有数据,内核就会把数据返回进程。

g)IO 复用模型:使用 select 或者 poll 等待数据,可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后把数据从内核复制到进程中。(在多路复用 IO 模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个
socket,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。)

h)信号驱动 IO 模型:当进程发起一个 IO 操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用 IO 读取数据。

i)异步 IO 模型:当进程发起一个 IO 操作,进程返回不阻塞,但也不能返回结果;内核把整个 IO 处理完后,会通知进程结果。如果IO操作成功则进程直接获取到数据。

2.4 select、poll 和 epoll 的区别?epoll 的底层使用的数据结构。

select,poll 和 epoll 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。

select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。

select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制,poll 中的描述符是 pollfd 类型的数组;

poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。

如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。

select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。

当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象。

创建 epoll 对象后,可以用 epoll_ctl 向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait 便可以得到事件完成的描述符。

就绪列表:epoll 使用双向链表来实现就绪队列,是一种能够快速插入和删除的数据结构。索引结构:epoll 使用红黑树去监听并维护所有文件描述符。

epoll 的描述符事件有两种触发模式:LT(水平触发)和 ET(边沿触发)。

当 epoll_wait 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait会再次通知进程。

和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait 时不会再得到事件到达的通知。

边沿触发仅触发一次,水平触发会一直触发。

2.5 进程的通信方式有哪些?线程呢?

2.5.1 进程间的通信方式

a) 管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。

b) 有名管道(Names Pipes): 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。

c)消息队列(Message
Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比
FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺。

d) 信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;(对于异常情况下的工作模式,就需要用「信号」的方式来通知进程,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。比如,Ctrl+C 产生 SIGINT 信号,表示终止该进程,Ctrl+Z 产生 SIGSTP,表示停止该进程,但还未结束)

e) 信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。(信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。)

f) 共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。(共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中)

h) 套接字(Sockets): 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

int socket(int domain, int type, int protocal)

2.5.2 线程间的通信方式:

a) 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。

b) 信号量(Semphares):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。

c) 事件(Event):Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

2.6 fork 函数的作用?

在 Linux 中 fork 函数是非常重要的函数,它的作用是从已经存在的进程中创建一个子进程,而原进程称为父进程。

调用 fork,当控制转移到内核中的 fork 代码后,内核开始做:

分配新的内存块和内核数据结构给子进程。

将父进程部分数据结构内容拷贝至子进程。

将子进程添加到系统进程列表。

fork返回开始调度器,调度。

特点:

1)调用一次,返回两次并发执行

2)相同但是独立的地址空间

3)fork 的返回值:fock 函数调用一次却返回两次;向父进程返回子进程的 ID,向子进程中返回 0,

4)fork 的子进程返回为 0;

5)父进程返回的是子进程的 pid。

fork 调用失败的原因

1)系统中有太多进程。

2)实际用户的进程数超过限制。

2.7 协程的概念?

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

对操作系统而言,线程是最小的执行单元,进程是最小的资源管理单元。无论是进程还是线程,都是由操作系统所管理的。

协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。

协程既不是进程也不是线程,协程仅仅是一个特殊的函数,协程它进程和进程不是一个维度的。

一个进程可以包含多个线程,一个线程可以包含多个协程。

一个线程内的多个协程虽然可以切换,但是多个协程是串行执行的,只能在一个线程内运行,没法利用 CPU 多核能力。

协程与进程一样,切换是存在上下文切换问题的。

2.8. linux 进程和线程?

进程通过 fork创建

线程通过 pthread_create 函数创建

2.9 通过进程id查看占用的端口,通过端口号查看占用的进程 id?

通过进程id查看占用的端口:

netstat -nap | grep 进程id

通过端口号查看占用的进程id :

netstat -nap | grep 端口号

2.10 如何查看占用内存比较多的进程?

head :-N可以指定显示的行数,默认显示10行。

ps :a—指代所有的进程,u—userid—执行该进程的用户id,x—指代显示所有程序,不以终端机来区分。ps -aux的输出格式如下:

sort -k4nr 中:k 代表从根据哪一个关键词排序,后面的数字 4 表示按照第四列排序;n 指代 numberic sort,根据其数值排序;r 指代 reverse,这里是指反向比较结果,输出时默认从小到大,反向后从大到小。%MEM 在第 4 个位置,-k4 按照内存占用排序。%CPU 在第三个位置,-k3 表示按照cpu占用率排序。

2.11 僵尸进程产生的原因?

僵尸进程是指它的父进程没有等待(调用 wait/waitpid)。如果子进程先结束而父进程后结束,即子进程结束后,父进程还在继续运行但是并未调用 wait/waitpid 那子进程就会成为僵尸进程。但如果子进程后结束,即父进程先结束了,但没有调用 wait/waitpid 来等待子进程的结束, 此时子进程还在运行,父进程已经结束。那么并不会产生僵尸进程。应为每个进程结束时,
系统都会扫描当前系统中运行的所有进程,看看有没有哪个进程时刚刚结束的这个进程的子 进程,如果有就有 init 来接管它,成为它的父进程。

进程设置僵尸状态的目的是维护子进程的信息,以便父进程在以后某个时间获取。要在当前 进程中生成一个子进程,一般需要调用 fork 这个系统调用,fork 这个函数的特别之处在于一次调用,两次返回,一次返回到父进程中,一次返回到子进程中,可以通过返回值来判断其 返回点。如果子进程先于父进程退出, 同时父进程又没有调用 wait/waitpid,则该子进程将成为僵尸进程。

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存。但是 仍然保留了一些信息(如进程号 pid 退出状态 运行时间等)。这些保留的信息直到进程通过调用 wait/waitpid 时才会释放。这样就导致了一个问题,如果没有调用 wait/waitpid 的话,那 么保留的信息就不会释放。比如进程号就会被一直占用了。但系统所能使用的进程号的有限
的,如果产生大量的僵尸进程,将导致系统没有可用的进程号而导致系统不能创建进程。所 以我们应该避免僵尸进程。

如果进程不调用 wait / waitpid 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用 的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

2.13 孤儿进程产生的原因?

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤 儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程是没有父进程的进程,管理孤儿进程这个重任就落到了 init 进程身上,因此孤儿进程并 不会有什么危害。

2.14 讲一下虚拟内存。虚拟内存和物理内存的关系是什么?

虚拟内存使得应用程序认为它拥有一个连续的地址空间,而实际上,它通常是被分隔成多个物理内 存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。

虚拟内存可以让程序可以拥有超过系统物理内存大小的可用内存空间。虚拟内存让每个进程拥有一 片连续完整的内存空间。

局部性原理表现在以下两个方面:

1)时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。

2)空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问。

操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块, 每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都 必须在物理内存中。当程序引用到不在物理内存中的页时,会将缺失的部分从磁盘装入物理内存。

页面置换算法:

OPT 页面置换算法(最佳页面置换算法):所选择的被换出的页面将是最长时间内不再被访问, 通常可以保证获得最低的缺页率。

FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。

LRU (Least Currently Used)页面置换算法(最近最久未使用页面置换算法):将最近最久未使用的页面换出。需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到 链表表头。这样就能保证链表表尾的页面是最近最久未访问的。力扣-实现LRU

LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法):该置换算法选择在之前时期使用最少的页面作为淘汰页。力扣-实现LFU

2.15 分段和分页讲一下?以及对应的场景?

操作系统的内存管理机制了解吗?内存管理有哪几种方式?

块式管理 : 将内存分为几个固定大小的块,每个块中只包含一个进程。

页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址 和物理地址。

段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页实际并无任何实际意义。 段式管理把主存分为一段段的,最重要的是段是有实际意义的,每个段定义了一组逻辑信息。 段式管理通过段表对应逻辑地址和物理地址。例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。

段页式管理:段页式管理机制结合了段式管理和页式管理的优点。段页式管理机制就是 把主存先分成若干段,每个段又分成若干页。

分段和分页:

共同点

分页机制和分段机制都是为了提高内存利用率,较少内存碎片。

页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中 的内存是连续的。

区别

页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的 程序。

分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中 可以体现为代码段,数据段,能够更好满足用户的需要。

2.16 讲一下用户态和内核态?所有的系统调用都会进入到内核态吗?

操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序。根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:

用户态(user mode) : 用户态运行的进程或可以直接读取用户程序的数据。

内核态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。

运行的程序基本都是运行在用户态。如果我们调用操作系统提供的内核态级别的子功能那就需要系统调用了。

系统调用:与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都 必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。

系统调用是操作系统为应用程序提供能够访问到内核态的资源的接口。补充:

用户态切换到内核态的几种方式

系统调用: 系统调用是用户态主动要求切换到内核态的一种方式, 用户应用程序通过操作系统调用内核为上层应用程序开放的接口来执行程序。

异常:当 cpu 在执行用户态的应用程序时,发生了某些不可知的异常。 于是当前用户态的应用进程切换到处理此异常的内核的程序中去。

硬件设备的中断: 当硬件设备完成用户请求后,会向 cpu 发出相应的中断信号,这时 cpu 会暂停执行下一条即将要执行的指令,转而去执行与中断信号对应的应用程序, 如果先前执行的指令是用户态下程序的指令,那么这个转换过程也是用户态到内核态的转换。

2.17 平常用什么 linux 命令比较多?如何打开文件并进行查找某个单词?怎么在某个目录下找到包含 txt 的文件?

pwd:显示当前所在位置

sudo + 其他命令:以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行。

grep:要搜索的字符串 要搜索的文件 --color : 搜索命令,–color 代表高亮显示

ps - ef/ps aux: 这两个命令都是查看当前系统正在运行进程,两者的区别是展示格式不同。如果想要查看特定的进程可以使用这样的格式:

ps aux|grep redis

(查看包括redis的进程),也可使用

pgrep redis -a

注意:如果直接用ps((Process Status))命令,会显示所有进程的状态,通常结合 grep 命令查看某进程的状态。

kill -9 进程的 pid : 杀死进程(-9 表示强制终止),先用 ps 查找进程,然后用 kill 杀掉。

find 目录 参数 : 寻找目录(查)。在/home目录下查找以 .txt 结尾的文件名:

find /home -name “*.txt”

ls 或者 ll :(ll 是 ls -l 的别名,ll 命令可以看到该目录下的所有目录和文件的详细信息): 查看目录信息。

free : 显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。

tar -zcvf 打包压缩后的文件名 要打包压缩的文件 : 打包并压缩文件,一般情况下打包和压缩是一起进行的,打包并压缩后的文件的后缀名一般 .tar.gz。c:压缩。

tar -xvf 压缩文件 - C 解压的位置 : 解压压缩包。x: 解压。

wget : 是从远程下载的工具。

vmstat : 虚拟内存性能监控、CPU 监控。

top : 常用来监控Linux的系统状况,比如CPU、内存的使用,显示系统上正在运行的进程。load average:系统负载,就是进程队列的长度。当这个值>cpu核心数的时候就说明有进程在等待处理了,是负载过重。

2.18 用过 ping 命令么?简单介绍一下。TTL 是什么意思?

ping : 查看与某台机器的连接情况。TTL:生存时间。数据报被路由器丢弃之前允许通过的网段数量。

2.19 怎么判断一个主机是不是开放某个端口?

telnet IP 地址 端口

telnet 127.0.0.1 3389

2.20 说一下你最用的比较多得模式(我说的工厂模式和观察者模式),再实现一个单例模式。

为什么要用 voliate 修饰?出现synchronized 为啥还需要 voliate,以及 synchronized 能保证啥?

观察者模式:观察者模式定义了一系列对象之间的一对多关系。当一个对象改变状态, 其他依赖着都会受到通知。车辆的数据时不断更新的,需要监控数据的变化,当有新数据时就通知 观测者observers。

迭代器模式:提供一种顺序访问聚合对象元素的方法。 hasNext 和 next 方法。

代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

JDK 动态代理和 CGLIB 动态代理均是实现 Spring AOP 的基础。

适配器模式

2.21 排序算法哪些是稳定的,为什么直接插入排序是稳定的,各种排序算法的时间复杂度和空间复杂度?

什么时候快速排序的时间复杂度最高。讲一下什么情况该使用什么排序算法?堆排序算法如何实现?

影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时 间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利 于软件的维护。一般而言,需要考虑的因素有以下四点:

a)待排序的记录数目 n 的大小;

b)记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;

c)关键字的结构及其分布情况;

d)对排序稳定性的要求。

1)当 n 较大,则应采用时间复杂度为 O(nlog2n)O(nlog2n) 的排序方法:快速排序、堆排序或归并排序。

快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分 布时,快速排序的平均时间最短;

堆排序 :堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况,也就是排序效率稳定。

归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的 序列,然后再合并,在效率上将有所提高。

2)当 n 较大,内存空间允许,且要求稳定性 => 归并排序

3) 当 n 较小,可采用直接插入或直接选择排序。

4) 一般不使用或不直接使用传统的冒泡排序。

5) 基数排序

它是一种稳定的排序算法,但有一定的局限性:

a) 关键字可分解。

b) 记录的关键字位数较少,如果密集更好

c) 如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开 排序。

2.22 如何进行二叉树的各种遍历的非递归算法实现?简要讲述。

从当前节点开始遍历:(当入栈时访问节点内容,则为前序遍历;出栈时访问,则为中序遍 历)

若当前节点存在,就存入栈中,并访问左子树;

直到当前节点不存在,就出栈,并通过栈顶节点访问右子树;

不断重复 12,直到当前节点不存在且栈空。

只需要在入栈、出栈的时候,分别进行节点访问输出,即可分别得到前序、中序的非递归遍 历代码!从当前节点开始遍历:

若当前节点存在,就存入栈中,第一次访问,然后访问其左子树;

直到当前节点不存在,需要回退,这里有两种情况:

a) 从左子树回退,通过栈顶节点访问其右子树(取栈顶节点用,但不出栈)

b) 从右子树回退,这时需出栈,并取出栈节点做访问输出。(需要注意的是,输出完 毕需要置当前节点为空,以便继续回退。具体可参考代码中的 cur == null)

不断重复 12,直到当前节点不存在且栈空。

2.23 硬链接和软链接?

硬链接: 硬连接指通过索引节点 inode 来进行的连接,即每一个硬链接都是一个指向对应区域的文件。

软链接: 保存了其代表的文件的绝对路径,是另外一种文件,在硬盘上有独立的区块, 访问时替换自身路径。

2.24 中断的分类?

中断可以分为同步中断(synchronous)和异步中断(asynchronous)。

中断可分为硬中断和软中断。

中断可分为可屏蔽中断(Maskable interrupt)和非屏蔽中断(Nomaskable interrupt)。

同步中断是在指令执行时由 CPU 主动产生的,受到 CPU 控制,其执行点是可控的。异步中断是 CPU 被动接收到的,由外设发出的电信号引起,其发生时间不可预测。

2.25 软中断和硬中断?

从本质上讲,中断(硬)是一种电信号,当设备有某种事情发生的时候,他就会产生中断,通过 总线把电信号发送给中断控制器。如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到中断处理程序的入口点, 进行中断处理。

硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的 IRQ(中断请求)。

软中断是由当前正在运行的进程所产生的。

软中断比硬中断少了一个硬件发送信号的步骤。产生软中断的进程一定是当前正在运行的进 程,因此它们不会中断 CPU。但是它们会中断调用代码的流程。如果硬件需要 CPU 去做一些事情,那么这个硬件会使 CPU 中断当前正在运行的代码。

2.26 红黑树和平衡二叉树?

排序二叉树虽然可以快速检索,但在最坏的情况下:如果插入的节点集本身就是有序的,要 么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节 点只有左节点(如果插入节点集本身是大到小排列);或所有节点只有右节点(如果插入节 点集本身是小到大排列)。在这种情况下,排序二叉树就变成了普通链表,其检索效率就会 很差。

红黑树

性质 1:节点非红即黑。

性质 2:根节点永远是黑色的。

性质 3:所有的叶节点都是空节点(即 null),并且是黑色的。

性质 4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)

性质 5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

红黑树最重要的性质:从根到叶子的最长的可能路径小于等于最短的可能路径的两倍长。 红黑树并不是真正意义上的平衡二叉树,但在实际应用中,红黑树的统计性能要高于平衡二叉树,但极端性能略差。(对于AVL树,任何一个节点的两个子树高度差不会超过 1;对于红黑树,则是不会相差两倍以上)对于给定的黑色高度为 N 的红黑树,从根到叶子节点的最短路径长度为 N-1,最长路径长度为 2 *
(N-1)2∗(N−1)。对于红黑树,插入,删除,查找的复杂度都是 O(log N)O(logN)。任何不平衡都会在3次旋转之内解决。

红黑树通过上面这种限制来保证它大致是平衡的——因为红黑树的高度不会无限增高,这样 保证红黑树在最坏情况下都是高效的,不会出现普通排序二叉树的情况。

由于红黑树只是一个特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的 只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能比排序二叉树要好很多。

但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删 除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。

平衡二叉树的最差情形:

由平衡二叉树的定义可知,左子树和右子树最多可以相差1层高度,那么多个在同一层的子树 就可以依次以相差1层的方式来递减子树的高度,如下图所示是一个拥有4棵子树的树的层高 最大差情形。也就是说,一颗高度为H的平衡二叉树,其内部子树高度差最多为 [H / 2]。

红黑树的最差情形:

红黑树中红节点的父亲和孩子必须是黑节点,且从根到叶子节点经过的黑节点个数相同,因此红黑树最小深度是路径上只有黑节点,最大深度是路径上红黑节点相互间隔(重要),因此最大深度 <=最小深度的两倍,最大深度是 2 * log2(n+1)2∗log2(n+1)。

对于 AVL 树,任何一个节点的两个子树高度差不会超过 1;对于红黑树,则是不会相差两倍以上。红黑树的插入删除元素的效率高于平衡二叉树,而查询时间差于平衡二叉树。红黑树的树高 可能更高。

3 Java 基础

3.1 StringBuilder 和 StringBuffer

StringBuffer 是线程安全的 StringBuilder 是不安全的

3.2 Java实现连续空间的内存分配?

基本数据类型的数组,存放在栈内存里,连续分配对象数组,在栈内存里的引用是连续分配的,实际数据分配在堆内存,不是连续分配的;

3.3 创建对象的方式有哪几种?

new Obj…

clone:使用 Object 类的 clone 方法。

反射

调用 public 无参构造器 ,若是没有,则会报异常:

Object o = clazz.newInstance;

有带参数的构造函数的类,先获取到其构造对象,再通过该构造方法类获取实例:

通过反序列化来创建对象: 实现 Serializable 接口。

3.4 接口和抽象类有什么区别?

3.5 深拷贝和浅拷贝区别?

3.6 讲一讲封装,继承,多态(重要)。

编译时多态

方法重载 都是编译时多态。根据实际参数的数据类型、个数和次序,Java 在编译时能够确定执行重载方法中的哪一个。

方法覆盖 表现出两种多态性,当对象引用本类实例时,为编译时多态,否则为运行时多态。

运行时多态

通过父类对象引用变量引用子类对象来实现。当父类对象引用子类实例时。通过接口类型变量引用实现接口的类的对象来实现 。运行时多态主要是通过继承和接口实现的。

3.7 泛型是什么?类型擦除?

泛型:将类型当作参数传递给一个类或者是方法。

类型擦除:Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉, 正确理解泛型概念的首要前提是理解类型擦除。

Java 的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型 擦除。

原始类型: 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用 Object)替换。

Java 编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。当具体的类型确定后,泛型提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。

3.8 如何实现静态代理?有啥缺陷?

为现有的每一个类都编写一个 对应的 代理类,并且让它实现和目标类相同的接口。

在创建代理对象时,通过构造器塞入一个目标对象,然后在代理对象的方法内部调用目 标对象同名方法,并在调用前后增加一些其他方法。比如打印日志。代理对象 = 增强代码 + 目标对象。需要为每一个目标类编写对应的代理类,工作量太大了。

3.9 动态代理的作用?在哪些地方用到了?(AOP、RPC 框架中都有用到,面试笔试中经常要求手写一个动态代理)

为其它对象提供一种代理以控制对这个对象的访问控制,在程序运行时,通过反射机制动态生成。JDK动态代理的调用处理程序必须实现 InvocationHandler 接口,及使用 Proxy 类中的 newProxyInstance 方法动态的创建代理类。

3.10 JDK 的动态代理和 CGLIB 有什么区别?

JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。

3.11 谈谈对 Java 注解的理解,解决了什么问题?

Java 语言中的类、方法、变量、参数和包等都可以注解标记,程序运行期间我们可以获取到相应的注解以及注解中定义的内容,这样可以帮助我们做一些事情。比如说 Spring 中如果检测到说你的类被@Component 注解标记的话,Spring 容器在启动的时候就会把这个类归为自己管理,这样你就可以通过 @Autowired 注解注入这个对象了。

3.12 Java 反射?反射有什么缺点?你是怎么理解反射的(为什么框架需要反射)?

反射介绍:

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及 动态调用对象的方法的功能称为 Java 语言的反射机制。

反射的优缺点如下:

优点:运行期类型的判断,动态加载类,提高代码灵活度。

缺点:性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多。

安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。

3.13 为什么框架需要反射技术?

在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机 制没有用,实际上有很多设计、开发都与反射机制有关。动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。

我们在使用 JDBC 连接数据库时使用 Class.forName 通过反射加载数据库的驱动程序;

Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;

动态配置实例的属性;

3.14 获取 Class 对象的两种方式

如果我们动态获取到这些信息,我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了两种方式获取 Class 对象:

知道具体类的情况下可以使用:

Class alunbarClass = TargetObject.class;

但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象

通过 Class.forName 传入类的路径获取:

Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject");

3.15 内存泄露和内存溢出的场景。

内存泄漏:内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不 到及时释放,从而造成内存空间的浪费称为内存泄漏。

Java 内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生 于 OLD 段或 Perm 段垃圾回收后,仍然无内存空间容纳新的 Java 对象的情况。

内存泄露的场景

静态集合类引起内存泄漏:静态成员的生命周期是整个程序运行期间。比如:Map 是在堆上动态分配的对象,正常情况下使用完毕后,会被 gc 回收。而如果 Map 被 static 修饰,且没有删除机制,静态成员是不会被回收的,所以会导致这个很大的 Map 一直停留在堆内存中。懒初始化 static 变量,且尽量避免使用。

当集合里面的对象属性被修改后,再调用 remove方法时不起作用:修改 hashset 中对象的参数值,且参数是计算哈希值的字段。当一个对象被存储进 HashSet 集合中以后, 就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初 存储进 HashSet 集合中时的哈希值就不同了。

各种连接对象( IO 流对象、数据库连接对象、网络连接对象)使用后未关闭:因为每个流 在操作系统层面都对应了打开的文件句柄,流没有关闭,会导致操作系统的文件句柄一 直处于打开状态,而jvm会消耗内存来跟踪操作系统打开的文件句柄。

监听器的使用:在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

不正确使用单例模式是引起内存泄漏:单例对象在初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。

解决措施

尽量减少使用静态变量,类的静态变量的生命周期和类同步的。

声明对象引用之前,明确内存对象的有效作用域,尽量减小对象的作用域,将类的成员 变量改写为方法内的局部变量;

减少长生命周期的对象持有短生命周期的引用;

使用 StringBuilder 和 StringBuffer 进行字符串连接,Sting 和 StringBuilder 以及 StringBuffer 等都可以代表字符串,其中 String 字符串代表的是不可变的字符串,后两者表示 可变的字符串。如果使用多个 String 对象进行字符串连接运算,在运行时可能产生大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。

对于不需要使用的对象手动设置 null 值,不管 GC 何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象;

各种连接(数据库连接,网络连接,IO 连接)操作,务必显示调用 close 关闭。

内存溢出场景

JVM Heap(堆)溢出:OutOfMemoryError: Java heap space: 发生这种问题的原因是 java 虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已 经用满了。JVM 在启动的时候会自动设置 JVM Heap 的值, 可以利用 JVM 提供的-Xmn -Xms -Xmx 等选项可进行设置。Heap的大小是新生代和老年代之和。

解决方法:手动设置 JVM Heap(堆)的大小。检查程序,看是否有死循环或不必要地重复创建大量对象。

Metaspace溢出:java.lang.OutOfMemoryError: Metaspace 程序中使用了大量的 jar 或 class,使 java 虚拟机装载类的空间不够,与 metaspace 大小有关。方法区用于存放 Java 类型的相关信息。在类装载器加载 class 文件到内存的过程中,虚拟机会提取其中的类型信息,并将这些信息存储到方法区。当需要存储类信息而方法区的
内存占用又已经达到 -XX:MaxMetaspaceSize 设置的最大值,将会抛出 OutOfMemoryError 异常。对于这种情况的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。

解决方法:通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 设置永久代大小即可。

栈溢出: java.lang.StackOverflowError : Thread Stack space:线程的方法嵌套调用层次太多(如递归调用),以致于把栈区溢出了。

解决方法:修改程序。通过 -Xss: 来设置每个线程的 Stack 大小即可。

3.16 讲一下,强引用,弱引用,软引用,虚引用。

强引用:被强引用关联的对象不会被回收。使用 new 一个新对象的方式来创建强引用。

Object obj = new Object;

软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。使用 SoftReference 类来创建软引用。

弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发 生之前。使用 WeakReference 类来创建弱引用。

3.17 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用 得到一个对象。

3.18 讲一下 Java 的 NIO,AIO, BIO?

BIO (Blocking I/O):

同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。

NIO (Non-blocking/New I/O):

NIO 是一种同步非阻塞的 I/O 模型,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。Java NIO使我们可以进行非阻塞IO操作。比如说, 单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后, 线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一
些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。JDK 的 NIO 底层由 epoll 实现。

通常来说 NIO 中的所有 IO 都是从 Channel(通道) 开始的。

从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。

从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

AIO (Asynchronous I/O):异步非阻塞IO模型,异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的 线程进行后续的操作。AIO 的应用还不是很广泛。

3.19 Java 中 finalize方法的使用?

finalize是 Object的protected 方法,子类可以覆盖该方法以实现资源清理工作,GC 在回收对象之前调用该方法。

finalize方法中一般用于释放非 Java 资源(如打开的文件资源、数据库连接等),或是调用非

Java方法(native方法)时分配的内存(比如 C 语言的 malloc系列函数)。

避免使用的原因:

首先,由于 finalize方法的调用时机具有不确定性,从一个对象变得不可到达开始,到 finalize方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖 finalize方法能 及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc 却仍未触发,因而通常的做法是提供显示的 close方法供客户端手动调用。另外,重写
finalize方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。

3.20 GC Root 对象有哪些

方法区中的静态变量和常量引用的对象

虚拟机栈中引用对象

本地方法栈中引用对象

3.21 Java 中 Class.forName 和 ClassLoader 的区别?

类的加载:

装载:通过类的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构,在内存中生成 Java.lang.class 对象;

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;

校验:检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节 码验证,符号引用验证)

准备:给类的静态变量分配内存并初始化内存空间; 解析:将常量池中的符号引用转成直接引用;

初始化:激活类的静态变量的初始化 Java 代码和静态 Java 代码块,并初始化程序员设置的变量值。

在 java 中 Class.forName和 ClassLoader 都可以对类进行加载。ClassLoader 就是遵循 双亲委 派模型 最终调用启动类加载器的类加载器,实现的功能是通过一个类的全限定名来获取描述 此类的二进制字节流,获取到二进制流后放到 JVM 中。classloader 只干一件事情,就是将 .class 文件加载到jvm中,不会执行 static 中的内容。

Class.forName 方法实际上也是调用的 CLassLoader 来实现的。Class.forName除了将类的 .class 文件加载到 jvm 中之外,还会对类进行初始化,执行类中的 static 块。

最后调用的方法是 forName0 这个方法,在这个 forName0 方法中的第二个参数被默认设置为了 true,这个参数代表是否对加载的类进行初始化,设置为 true 时会类进行初始化,代表会 执行类中的静态代码块,以及对静态变量的赋值等操作。

3.22 讲一下 CopyOnWriteArrayList 和 CopyOnWriteArraySet?

CopyOnWrite 容器:

写时复制的容器。当我们往一个容器添加元素的时候,不直接往当前容 器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添 加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite
容器也是一种读写分离的思想,读和写不同的容器。以下代码是向 ArrayList 里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会 Copy 出N个副本出来。

读的时候不需要加锁,如果读的时候有多个线程正在向 ArrayList 添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的 ArrayList。

CopyOnWrite 并发容器用于读多写少的并发场景。

CopyOnWrite 的缺点

CopyOnWrite容 器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意。

内存占用问题。因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引 用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份 对象内存)。如果这些对象占用的内存比较大,比如说 200M 左右,那么再写入 100M 数据进去,内存就会占用
300M,那么这个时候很有可能造成频繁的 Yong GC 和 Full GC。针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如 果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用 CopyOnWrite 容器,而使用其他的并发容器,如 ConcurrentHashMap。

数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

3.23 单例模式(重要)

1、懒汉式,线程不安全

是否 Lazy 初始化:是

是否多线程安全:否

实现难度:易

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

接下来介绍的几种实现方式都支持多线程,但是在性能上有所差异。

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:易

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

优点:第一次调用才初始化,避免内存浪费。

缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

getInstance 的性能对应用程序不是很关键(该方法使用不太频繁)。

3、饿汉式

是否 Lazy 初始化:否

是否多线程安全:是

实现难度:易

描述:这种方式比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。

缺点:类加载时就初始化,浪费内存。

它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

4、双检锁/双重校验锁(DCL,即 double-checked locking)

JDK 版本:JDK1.5 起

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:较复杂

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

getInstance 的性能对应用程序很关键。

5、登记式/静态内部类

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:一般

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder
类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance
显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

6、枚举

JDK 版本:JDK1.5 起

是否 Lazy 初始化:否

是否多线程安全:是

实现难度:易

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

不能通过 reflection attack 来调用私有构造方法。

3.24 Java 中>>和>>>的区别

Java 中的位运算符:

表示带符号右移,如:int i=15; i>>2 的结果是 3,移出的部分将被抛弃。

转为二进制的形式可能更好理解,0000 1111(15)右移 2 位的结果是 0000 0011(3),0001 1010(18)右移 3 位的结果是 0000 0011(3)。

无符号右移:

按二进制形式把所有的数字向右移动对应巍峨位数,低位移出(舍弃),高位的空位补零。对于正数来说和带符号右移相同,对于负数来说不同。

其他结构和>>相似。

表达式为:

例如:

4. 计网

4.1 为什么网络要分层?

说到分层,我们先从我们平时使用框架开发一个后台程序来说,我们往往会按照每一层做不 同的事情的原则将系统分为 三层(复杂的系统分层可能会更多):

Repository(数据库操作)

Service(业务操作)

Controller(数据交互)

网络分层的原则:每一层独立于其它层完成自己的工作,而不需要相互依赖,上下层之间通 过标准结构来互相通信,简单易用又具有拓展性。复杂的系统需要分层,因为每一层都需要专注于一类事情。我们的网络分层的原因也是一 样,每一层只专注于做一类事情。

为什么计算机网络要分层呢? ,我们再来较为系统的说一说:

各层之间相互独立:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只 需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)。这个 和我们对开发时系统进行分层是一个道理。

提高了整体灵活性 :每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。这个和我们平时开发系统的时候要求的高内 聚、低耦合的原则也是可以对应上的。

大问题化小 :分层可以将复杂的网络间题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。这 个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解 的更小的问题是相对应

相关推荐