文章目录
-
- 1、请你说说线程和协程的区别
- 2、请你说说MySQL索引以及它们的好处坏处
- 3、请你说说多线程
- 4、说说怎样保证线程安全
- 5、请你说说死锁定义及发生的条件
- 6、亲你说说进程间通信方式
- 7、说说你对MVC的理解
- 8、详细说说Redis的数据类型
- 9、请你说说乐观锁和悲观锁
- 10、设计模式了解吗
- 11、说说你对AOP的理解
- 12、说说Redis的持久化策略
- 13、请你讲讲单例模式,手写一下单例模式
- 14、说说虚拟内存与物理内存的区别
- 15、说说你对IOC的理解
- 16、请你说说内存管理
- 17、请你说说IO多路复用(select,poll,epoll)
- 18、请你说说线程和协程的区别
- 19、请你说说MySQL的事务隔离级别
- 20、如何利用Redis实现一个分布式锁
- 21、请说说你对反射的了解
- 22、请你说说ArrayList和LinkedList的区别
- 23、数据库为什么不用红黑树而用B+树
- 24、请你说说Redis的数据类型
- 25、请你说说ConcurrentHashMap
- 25、说说缓存穿透,击穿,雪崩的区别
- 26、Redis如何与数据库保持双写一致性
- 27、说说你了解的线程同步方式
- 28、请你说说innodb和myisam的区别?
- 29、String、StringBuffer、StringBuilder有什么区别?
- 30、说说了解的JVM内存模型
- 31、说说JVM的垃圾回收机制
- 32、说说类加载机制
- 33、请你说说抽象类和接口的区别
- 34、请你说说==和equals()的区别
- 35、说说synchronize的用法及原理
- 36、说说你对AQS的理解
- 37、Java那些地方使用了CAS
- 38、说说JVM的垃圾回收算法
- 39、请你说说Redis数据类型中的zset,它和set有什么区别?底层是怎么实现的?
- 40、说说static修饰符的用法
- 41、说说线程的状态
- 42、说说你对ThreadLocal的理解
- 43、说说Spring Boot常用的注解
- 44、说说Bean的生命周期
1、请你说说线程和协程的区别
得分点:
地址空间、开销、并发性、内存。
标准回答 :
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
- 进程有独立的地址空间,线程有自己的堆栈和局部变量,单线程之间没有单独的地址空间;
- 进程和线程切换时,需要切换进程河鲜城的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,耗费资源较大,效率就要差一些;
- 进程的并发性较低,线程的并发行较高;
- 每个独立的进程有一个程序运行的入口,顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
- 系统在运行的时候会为每个进程分配不同的内存空间,而对线程而言,除了CPU外,系统不会为线程分配内存,(线程所使用的资源来自其所属进程的资源)线程组之间只能共享资源;
- 一个进程奔溃后,在保护模式下不会对其他进程产生影响,但是一个线程奔溃整个进程都会死掉,所以多进程要比多线程健壮。
2、请你说说MySQL索引以及它们的好处坏处
得分点:
检索效率,存储资源,索引维护。
标准回答 :
索引就像表行的指针,是一种允许查询操作快速确定哪些行符合WHERE子句中的条件,并检索到这些行的其他列值的数据结构。
- 索引主要有普通索引、唯一索引、主键索引、外键索引、全文索引、复合索引几种;
- 在大数据量的查询中,合理使用索引的优点:
- 大幅提高匹配where条件的检索效率;
- 用于排序和分组操作的加速;
- 索引使用不当的坏处:
- 索引必定会增加存储资源的消耗;
- 同时增加了插入、更新、删除操作的维护成本,因为每个增删改查操作后相应列的索引都必须被更新
- 加分回答:
- 只要创建了索引,就一定会走索引吗,不一定。比如在使用组合索引的时候,如果没有遵从“最左前缀”的原则进行搜索,则索引是不起作用的;
- 假设在id、name、age字段上已经成功建立了一个名为Multildx的组合索引,索引行中按id、name、age的顺序存放可以搜索id、(id、name)、(id、name、age)字段的组合,如果列不构成索引最左面的前缀,那么MySQL不能使用局部索引,如(age)或(name,age)组合咋不能使用该索引查询。
3、请你说说多线程
得分点:
线程与进程的关系,为什么使用多线程。
标准回答 :
线程是操作系统调度的最小单元,它可以让一个进程并发的处理多个进程,所以,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈、局部变量,并且能够共享进程内的资源,由于共享资源,处理器便可以在这些线程之间快速切换,从而让使用者感觉这些线程在同时执行。
- 总的来说,操作系统可以同时执行多个任务,每个任务就是一个进程,进程可以同时执行多个任务,每个任务就是一个线程,一个程序运行之后至少有一个进程,而一个进程可以包含多个线程,但至少要包含一个线程,使用多线程会给开发人员带来显著的好处;
- 使用多线程的原因主要有以下几点:
- 更多的CPU核心。现代计算机处理器性能的提升方式,已经从追求更高的主频向追求更多的核心发展,所以处理器的核心数量会越来越多,充分的利用处理器的核心则会显著的提高程序的性能,而程序使用多线程技术,就可以将计算逻辑分配到多个处理器核心上,显著减少程序的处理时间,从而随着更多处理器核心的加入而变得更有效率;
- 更快的响应时间。我们进程要针对复杂的业务编写出复杂的代码,如果使用多线程技术就可以将数据一致性不强的操作派发给其他线程处理,(也可以是消息队列),如上传图片,发送邮件,生成订单等。这样的响应用户请求的线程就能够尽快的完成处理,大大的缩短了响应时间,提升了用户体验;
- 更好的编程模型。Java为多线程提供了良好且一致的编程模型,使开发人员能够更加专注于问题的解决,开发者只需要为此问题建立合适的业务模型,而无需绞尽脑汁的考虑如何实现多线程。一旦开发人员建立好了业务模型,稍加修改就可以将其方便的映射到Java提供的多线程编程模型上。
4、说说怎样保证线程安全
得分点:
原子类、volatile、锁。
标准回答 :
Java保证线程安全的方式有很多,其中较为常用的就三种,按照资源占用情况由轻到重排列,这三种保证了线程安全的方式分别是原子类,volatile、锁。
- jdk从1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单,性能高效,线程安全的更新基本类型、原子更新引用类型,原子更新属性,原子更新数组。无论原子更新哪种类型,都要遵循“比较和替换”规则,即比较要更新的值是否等于期望值,如果是则更新,如果不是则失败;
- volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全,可见性问题是由处理器核心的缓存导致的,每个核心均有各自的缓存,而这些缓存均要与内存进行同步,volatile具有如下的内存语义:当一个volatile变量时,该线程本地内存中的共享变量的值会被立刻刷新到主内存;当读一个volatile只能保证单个共享变量的线程安全,锁则可以保证临界区内的多个共享变量的线程安全。
- java中加锁的方式有两种,分别是synchronized关键字和Lock接口,synchronized是比较早期的API,在设计之初没有考虑到超时机制,非阻塞形式,以及多个条件变量,若想通过升级的方式让它支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码,因此,jdk的开发团队就在1.5之后新增了Lock接口,并通过Lock支持了上述的功能,支持响应中断,支持超时机制,支持以非阻塞的方式获取锁,支持多个条件变量(阻塞队列)。
- 加分回答:
- 实现线程安全的方式又很多种,除了上述三种方式之外,还有如下几种方式:
-
- 无状态设计。线程安全问题是由多线程并发修改共享变量引起的,如果在并发环境中,没有设计共享变量,则自然就不会出现线程安全问题。这种代码实现可以称作“无状态实现”,所谓状态就是指共享变量。
-
- 不可变设计。如果在并发环境中,不得不设计共享变量,则应该优先考虑共享变量是否为只读的,如果是只读场景就可以将共享变量设计为不可变的,这样自然也不会出现线程安全问题。具体来说就是在变量前加final修饰符,使其不可被修改,如果变量是引用类型,则将其设计为不可变类型。
-
- 并发工具java.util.comcurrent包提供了几个有用的并发工具类,一样可以保证线程安全,:Semaphore:就是信号量,可以控制同时访问特定资源的线程数量。CountDownLatch:允许一个或多个线程等待其他线程完成操作。CyclicBarrier:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。
-
- 本地存储:我们也可以考虑使用ThreadLocal存储变量,ThreadLocal可以方便地为每一个线程单独存一份数据,也就是将需要并发访问的资源复制成多份,这样一来,就可以避免多线程访问共享变量了,它们访问的是自己独占的资源,它从根本上隔离了多个线程之间的数据共享。
5、请你说说死锁定义及发生的条件
得分点:
争夺共享资源,相互等待,互斥条件,请求和宝成条件,不剥夺条件,环路等待条件。
标准回答 :
- 死锁。两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁。这些永远在相互等待的进程称为死锁进程。
- 产生死锁的必要条件:
- 互斥条件:只进程对所分配到的进程进行排他性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求资源,则请求者只能等待,直到占有资源的进程用完释放;
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源请求已被其他进程占有,此时请求进程阻塞,但又对自己已获得的其他资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完成时自己释放;
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,…,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,…,Pn正在等待已被P0占用的资源。
6、亲你说说进程间通信方式
得分点:
管道,命名管道,信号,消息队列,共享内存,内存映射,信号量,Socket。
标准回答 :
- 管道:管道也叫无名(匿名)管道,它是UNIX系统中IPC(进程间通信)的最古老形式,所有的UNIX系统都支持这种通信机制,管道的本质其实是内核中维护的一块内存缓冲区,Linux系统中通过pipe()函数创建管道,会生成两个文件描述符,分别对应管道的读端和写端。无名管道只能用于具有亲缘关系的进程间的通信。
- 命名管道:匿名管道,由于 没有名字,只能用于亲缘关系的进程间通信,为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。有名管道(FIFO),不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与FIFO创建进程不存在亲缘关系的进程,只要可以访问该路径就能够彼此通过FIFO相互通信,因此,通过FIFO不相关的进程也能交换数据;
- 信号:信号是LInux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称为软件中断,它是在软件层面上对中断机制的一种模拟,是一种异步通信的方式,信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理一个突发事件;
- 消息队列:消息队列就是一个消息的链表,可以把消息看作是一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程就可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读消息,消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内存持续的;
- 共享内存:共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种IPC机制无需内核介入,所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用,与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快;
- 内存映射:内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件;
- 信号量:信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调它们的活动,对信号量的操作分为P操作和V操作,P操作是将信号量的值减1,V操作是将信号量的值加1,当信号量的值小于等于0之后,在进行P操作时,当前进程或线程会被阻塞,直到另一个进程或线程执行了V操作将信号量的值增加到大于0之时;
- Socket套接字:就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制,Socket一般用于网络中不同主机上的进程之间的通信。
7、说说你对MVC的理解
得分点:
MVC概念,model,view,Controller模块功能。
标准回答 :
- Model代表的是数据,view代表的是用户界面,Controller代表的是数据的处理逻辑,它是Model和View这两层的桥梁。将软件分层的好处是,可以将对象之间的耦合度降低,便于代码的维护。
- Model:指从现实世界中抽象出来的对象模型,是应用逻辑的反应,它封装了数据和对数据的操作,是实际进行数据处理的地方(模型层与数据库才有交互)。在MVC的三个部件中,模型拥有最多的处理任务,被模型返回的数据是中立的,模型与数据格式无关,这样一个迷信能为多个视图提供数据,由于应用于模型的代码只需要写一次就可以被多个视图重用,所以减少了代码的重复性。
- View:负责进行模型的展示,一般就是我们见到的用户界面。
- Controller:控制器负责将视图和模型之间的交互,控制对用户输入的响应,响应方式和流程;它主要负责两方面的动作,一是把用户的请求分发到相应的模型,二是把模型的改变及时地反映到视图上。
加分回答
- 为了解耦以提升代码的可维护性,服务端开发一般会对代码进行分层,服务端代码一般会分为三层:表现层,业务层,数据访问层。在浏览器访问服务器时,请求会先到达表现层,最典型的MVC就是jsp+servlet+javabean模式,以JavaBean作为模型,既可以作为数据模型来封装业务数据,又可以作为业务逻辑模型来包含应用的业务操作,JSP作为视图层,负责提供页面来为用户展示数据,提供相应的表单(form)来用于用户的请求,并在适当的时候(点击按钮)向控制器发出请求来请求模型进行更新。Serlvet作为控制器,用来接收用户的请求,然后获取请求中的数据,将之转换为业务模型需要的数据模型,然后调用业务模型相应的业务方法进行更新,同时根据业务执行结果来选择要返回的视图,Spring MVC框架是基于Java的实现了MVC框架模式的请求驱动类型的轻量级框架。前端控制器是DispatchServlet接口实现类,映射器处理器是HandlerMapping接口实现类,视图解析器是ViewResolver接口实现类,页面控制器是Controller接口的实现类。
8、详细说说Redis的数据类型
得分点:
Redis的5种数据结构。
标准回答 :
Redis主要提供了5中数据结构,字符串(String),哈希(Hash),列表(List),集合(Set),有序集合(ZSet)。Redis还提供了Bitmap,HyperLogLog。Geo类型,但这些类型都是基于上述核心数据类型实现的,5.0版本中,Redis新增加了Streams数据类型,它是一个功能强大的支持多播的,可持久化的消息队列。
- String可以存储字符串,数字和二进制数据,除了值可以是String外,所有的键也可以是String,String最大可以存储大小为2M的数据;
- List保证数据线性有序且元素可以重复,它支持Ipush,bIpush,rpop,brpop等操作,可以当作简单的消息队列使用,一个list最多可以存储2^32-1个元素;
- hash的值本身也是一个键值对结构,最多能存储2^32-1个元素;
- set是无序不可重复的,它支持多个set求交集,并集,差集,适合实现共同关注之类的需求,一个set最多可以存储2^32-1个元素;
- zset是有序不可重复的,它通过给每个元素设置一个分数来作为排序的依据,一个zset最多可以存储2^32-1个元素。
加分回答
- 每种类型支持多个编码,每一种编码采取一个特殊的结构来实现,各类数据结构内部的编码及结构:string:编码分为int,raw,embstr;int底层实现为long,当数据为整数型并且可以用long类型表示时可以用long存储,embstr底层实现为占一块内存的SDS结构,当数据长度不超过32个字节的字符串时,选择以此结构连续存储元数据和值;raw底层实现为占两块的SDS,用于存储长度超过32字节的字符串数据,此时会在两块内存中分别存储元数据和值;
- list分为ziplist,linkedlist和quicklist,ziplist底层实现为压缩列表,当元素数量小于2且所有元素长度都小于64字节时,使用这种结构来存储,linkedlist底层实现为双端链表,当数据不符合ziplist条件时,使用这种结构来存储;3.2版本之后list一般采用quicklist的快速列表结构来代替前两种;
- hash编码分为ziplist,hashtable两种,其中ziplist底层实现为压缩列表,其键值对数量小于2,并且所有的兼职长度都小于64字节时,使用这种结构进行存储;
- hashtable底层实现为字典,当不符合压缩列表存储条件时,使用字典进行存储;
- set,编码分为inset和hashset,inset底层实现为整数集合,当所有元素都是整数值且数量不超过2个的时候使用该结构存储,否则使用字典结构存储,
- zset,编码分为ziplist和skiplist,当元素数量小于128,并且每个元素长度都小于64字节时,使用ziplist压缩列表结构存储,否则使用skiplist的字典+跳表的结构存储。
9、请你说说乐观锁和悲观锁
得分点:
乐观锁和悲观锁定义及试用场景。
标准回答 :
乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用这个版本号机制和CAS算法实现。
- 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
- 悲观锁,总是假设最坏的情况每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其它线程)。
- 传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前上锁。
加分回答
两种锁使用场景:
- 乐观锁:GIT,SVN,CVS等代码版本控制管理器,就是乐观锁使用很好的场景,例如:A,B程序员,同时从SCV服务器上下载了code,html文件,当A完成提交后,此时B再提交,那么会报版本冲突,此时需要B进行版本处理合并后,再提交到服务器,这其实就是乐观锁的实现全过程,如果此时使用的是悲观锁,那么意味着所有程序员都必须一个一个等待操作提交完,才能访问文件。
- 悲观锁:悲观锁的好处在于可以减少并发,但是当并发量非常大的时候,由于消耗资源,锁定时间过长等原因,很容易导致系统性能的下降,资源消耗严重,因此一般我们可以在并发量不是很大,并且出现并发情况导致的异常用户和系统都很难以接受的情况下,会选择悲观锁进行。
10、设计模式了解吗
得分点:
单例模式,
标准回答 :
创建型模式:
- 单例模式,工厂方法模式,抽象工厂模式,健在者模式和原型模式,
结构型模式:
- 代理模式,装饰模式,适配器模式,组合模式,桥梁模式,外观模式,享元模式。
行为型模式:
- 模板方法模式,命令模式,责任链模式,策略模式,迭代器模式,中介者模式,外观者模式,备忘录模式,访问者模式,状态模式和解释器模式
11、说说你对AOP的理解
得分点:
AOP的概念,AOP 的作用,AOP的实现方式。
标准回答 :
AOP是一种编程思想,是通过预编译方式和运行期动态代理的方式实现不修改源代码的情况下给程序动态统一添加功能的技术。
- 面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
- 所谓切面相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。AOP技术是一种称为“横切”的技术,剖解开封装对象的内部,将影响多个类的公共行为封装到一个可重用的模块中,并将其命名为切面。
- 所谓的切面,简单点来说就是与业务无关,却为业务模块所共同调用的逻辑,将其封装起来便于减少系统的重复代码了,降低模块的耦合度,有利用未来的可操作性和可维护性。
- 利用AOP可以对业务逻辑各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。
- AOP可以有多种实现方式,而Spring AOP支持以下两种方式:
- JDK动态代理:这是Java提供的动态代理技术,可以运行时创建接口的代理实例,Spring AOP默认采取这种方式,再接口的代理实例中织入代码
- CGLib动态代理:采用底层的字节码技术,在运行时创建子类代理的实例,当目标对象不存在时,Spring AOP就会采用这种方式在子类中织入代码。
加分回答
- 在应用场景方面,Spring AOP为IOC的使用提供了多个模块的功能抽象出来,并通过简单的AOP的使用,灵活的编制到模块中,比如可以通过AOP 实现应用程序中的日志功能,另一方面,在Spring内部,例如事务处理之类的一些支持模块也是通过Spring AOP来实现的,AOP不能增强的类:
- Spring AOP只能对IOC容器中的Bean进行增强,对于不受容器管理的对象不能增强,
- 由于CGLib采用动态创建子类的方式生成代理对象,所以不能对final修饰的类进行代理。
12、说说Redis的持久化策略
得分点:
RDB、AOF
标准回答 :
Redis4.0之后,Redis有RDB持久化,AOF持久化,RDB-AOF混合持久化这三种持久化方式。
- RDB会创建一个经过压缩的二进制文件,这个文件以.rdb结尾,内部存储了各个数据库的键值对等信息,RDB持久化过程有手动触发和自动触发两种方式,手动触发是指通过SAVE或BGSAVE命令触发RDB持久化操作,创建“.rdb”文件;自动触发是指通过配置选项,让服务器在满足指定条件时自动执行BGSAVE命令。RDB持久化的优点是其生成的紧凑压缩的二进制文件体积小,使用该文件恢复数据的速度非常快,缺点则是BGSAVE每次运行都要执行fork操作创建子进程,这属于重量级操作,不宜频繁执行,因此,RDB没法做到实时的持久化。AOF以独立日志的方式记录了每次写入的命令,重启时再重新执行AOF文件中命令来恢复数据。AOF持久化的优点是与RDB持久化可能丢失大量的数据相比,AOF持久化的安全性要高得多。通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1秒之内,其缺点则是,AOF文件存储的是协议文本,它的体积要比二进制格式的“.rdb”文件大得多。
- AOF需要通过执行AOF文件中的命令来恢复数据库,其恢复速度比RDB慢的多。AOF在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞。AOF解决了数据持久化的实时性,是目前Redis主流的持久化方式。
- RDB-AOF混合持久化模式是Redis4.0开始引入的,这种迷失是基于AOF持久化构建而来的,用户可以通过配置文件中的“aof-use-rdb-preamble-yes”配置项开启AOF混合持久化,Redis服务器在执行AOF重写操作时,会像执行BGSAVE命令一样,根据数据库当前的状态生成相应的RDB数据,并将其写入AOF文件中,对于重写之后执行的Redis命令,则以协议文本的方式追加到AOF文件的末尾,即RDB数据之后。通过使用RDB-AOF混合持久化,用户可以同时获得RDB持久化和AOF持久化的优点,服务器既可以通过使用AOF文件包含的RDB数据来实现快速的数据恢复操作,又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之内。
13、请你讲讲单例模式,手写一下单例模式
得分点:
饿汉式单例模式,懒汉式单例模式,线程安全的懒汉式单例模式,
标准回答 :
单例模式(Singleton Pattern)是最简单的创建型设计模式,它会确保一个类只有一个实例存在,单例模式最重要的特点就是构造函数私有,从而避免外界直接使用构造函数直接实例化该对象。
-
单例模式在java中通常有两种表现形式:
- 饿汉式:类加载时就进行对象实例化;
- 懒汉式:第一次引用类时才进行对象实例化。
-
饿汉式单例模式:在类加载时就会初始化静态变量instance,这时候类的私有构造函数就会被调用,创建唯一的实例。public class Singleton{prevate statiic Singleton instance = new Singleton();//构造方法私有,确保外界不能直接实例化
private Singleton(){}//通过公有的静态方法获取对象实例 public static Singleton getInstance(){return instance;}}
-
懒汉式单例模式:类在加载时不会初始化静态变量instance,而是在第一次被调用时将自己初始化。
加分回答
-
单例模式的优点:
- 在一个对象需要频繁的销毁、创建,而销毁、创建性能又无法优化时,则可以通过在启用时直接产生一个单例模式,然后用永久驻留内存的方式来解决;
- 单例模式可以避免对资源的多重占用,因为只有一个实例,避免可对一个共享资源的并发操作;
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,
-
单例模式的缺点
- 单例模式无法创建子类,扩展困难,若要扩展,除了修改代码,基本上没有第二种途径可以实现;
- 单例模式对测试不利,在并行开发环境中,如果采用单例模式的类没有完成,是不能进行测试的;
- 单例模式与单一职责原则有冲突,一个类应该只实现一个逻辑,而不是关心它是否是单例的,是不是要用单例模式取决于环境。
14、说说虚拟内存与物理内存的区别
标准回答 :
- 物理内存:以前,没有虚拟内存概念的时候,程序寻址用的都是物理内存,程序能寻址的范围是有限的,这取决于CPU的地址线条数,比如在32位平台下,寻址的范围都是2^32也就是4G,并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能出现很多问题:
- 因为物理内存是有限的,当有多个进程要执行的时候,都要给4G内存,很显然内存不够,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存,这种频繁的装入内存的操作效率很低;
- 由于指令都是直接访问物理内存的,那么任何进程都可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的;
- 虚拟内存:由于物理内存有很多问题,所以出现了虚拟内存,虚拟内存是计算机系统内存管理的一种技术,它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常被分割成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
15、说说你对IOC的理解
得分点:
控制反转与依赖注入含义;
标准回答 :
IOC是控制反转的意思,是一种面向对象编程的设计丝线。在不采用这种思想的情况下,我们需要自己维护对象与对象之间的依赖关系,很容易造成对象之间的耦合度过高,在一个大型的项目中这十分不利于代理的维护。
-
IOC则可以解决这种问题,它可以帮助我们维护对象与对象之间的依赖注入并且降低对象之间的耦合度,说到IOC就不得不说DI,DI是注入的意思,它是IOC实现的实现方式,由于IOC这个词汇比较抽象而DI比较直观,所以很多时候我们就用DI来代替它,在很多时候我们简单的将IOC和DI划等号,这是一种习惯。
-
实现依赖注入的关键是IOC容器,它的本质就是一个工厂。
加分回答
-
IOC是java EE企业应用开发中的就偶组件之间复杂关系的利器,在以Spring 为代表的轻量级java EE开发风行之前,实际开发中是使用更多的EJB为代表的开发模式。
-
早EJB开发模式中,开发人员需要编写EJB组件,这种组件需要满足EJB规范才能在EJB容器中运行,从而完成获取事务,生命周期管理等基本服务;
-
Spring提供的服务和EJB并没有什么区别,只是在具体怎样获取服务的方式上两者的设计有很大不同:Spring Ioc提供了一个基本的Javabean容器,通过IOC模式管理依赖关系,并通过依赖注入和AOP切面增强了为Javabean这样的pojo对象或者Javabean对象,依次降低了用开发对于J2EE技术规范的依赖。在应用开发中开发人员设计组件时往往需要引用和调用其他组件的服务,这种依赖关系如果固化在组件设计中,会造成依赖关系的僵化和维护难度的增加,这个时候使用IOC人把资源获取的方向反转,让IOC容器主动管理这些依赖关系,将这些依赖关系注入到组件中,这就会让这些依赖关系的适配器和管理更加灵活。
16、请你说说内存管理
得分点:段页式内存管理方式。
标准回答 :
Linux操作系统采用段页式内存管理方式:页式存储管理能有效地提高内存利用率(解决内存碎片),而分段存储管理能反映程序的逻辑结构并有利于段的共享,将这两种存储管理方式结合起来,就形成了段页式存储管理方式。
- 段页式存储管理方式即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名;
- 在段页式系统中,为了实现从逻辑地址到物理地址的转换,系统中需要同时配置段表和页表,每个分段有一张页表,段表表项中至少包括段号,页表长度和页表始址,页表表项中至少包括页号和块号,在进行地址转换时,首先通过段表查到页表始址,然后通过页表找到也帧号,最终形成物理地址。
17、请你说说IO多路复用(select,poll,epoll)
得分点:
概念、select,poll,epoll。
标准回答 :
I/O多路复用能够在单个线程中,通过监视多个I/O流的状态来同时管理多个I/O流,一旦检测到某个文件描述符上我们关心的事件发生(就绪),能够通知程序进行相应的处理(读写操作),LInux下实现I/O复用的系统调用主要有select,poll,epoll。
- select的主旨思想:
- 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中,这个文件描述符的列表数据类型为fd_set,它是一个整型数组,总共是1024个比特位,每一个比特位代表一个文件描述符的状态,比如当需要select检测时,这一位为0就表示不检测对应的文件描述符的事件,为1表示检测对应的文件描述符的事件;
- 调用select()系统调用,监听该列表中的文件描述符的事件,这个函数是阻塞的,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回,并修改文件描述符的列表中对应的值,0表示没有检测到该事件,1表示检测到该事件。函数对文件描述符的检测的操作是由内核完成的;
- select()返回时,会告诉进程有多少描述符要进行I/O操作,接下来遍历文件描述符的列表进行I/O操作。
- select的缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;
- select支持的文件描述符数量太小了,默认是1024(由fd_set决定);
- 文件描述符集合不能重用,因为内核每次检测到事件都会修改,所以每次都需要重置;
- 每次select返回后,只能直到有几个fd发生了事件,但是具体哪几个还需要遍历文件描述符集合进一步判断
- poll的原理和select类似,poll支持的文件描述符没有限制,
- epoll是一种更加高级的IO复用技术,epoll的使用步骤及原理如下:
- 调用epoll_crearea()会在内核中创建一个eventpoll结构体数据,称之为epoll对象,在这个结构体中有两个比较重要的数据成员,一个是需要检测的文件描述符的信息struct_root rbr(红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息(双向链表);
- 调用epoll_ctrl()可以向epoll对象中添加、删除、修改要监听的文件描述符及事件;
- 调用epoll_wt()可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。
- epoll的两种工作模式:
- LT模式(水平触发)(Level — Triggered)是缺省的工作方式,并且同时支持Block和Nonblock Socket ,在这种做法中,内核检测到一个文件描述符就绪了,然后可以对这个就绪的fd进行IO操作,如果不作任何操作,内核还是会继续通知。
- ET模式(边缘触发)(Edge - Triggered)是高速工作方式,只支持Nonblock socket,在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll检测到。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了,但是注意,如果一直不对这个fd进行IO操作,(从而导致它再次编程未就绪),内核不会发送更多的通知(only once)。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比ET模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
18、请你说说线程和协程的区别
标准回答 :
- 线程是操作系统的资源,线程是创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;
- 线程在多核换件下是能做到真正意义上的并行,而协程是为并发而产生的;
- 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;
- 线程进程都是同步机制,而协程则是异步;
- 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;
- 操作系统对于线程开辟数量限制在千的级别,而协程可以达到千万的级别。
19、请你说说MySQL的事务隔离级别
得分点:
未提交读,已提交读,可重复读,可串行化;
标准回答 :
- SQL标准定义了四种隔离级别,这四种合理级别分别是:
- 读未提交(READ UNCOMMITTED);
- 读提交(READ COMMITTED);
- 可重复读(REPEATABLE READ);
- 串行化(SERIALIZABLE);
- 事务隔离是为了解决脏读,不可重复读,幻读问题;
- InnoDB存储引擎在默认的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别;
加分回答
- READ UNCOMMITTED:它是性能在最好,也是最野蛮的方式,因为它压根就不加锁,所以谈不上什么隔离效果,可以理解为没有隔离,其他事务不能并发写也不能并发读;
- REPEATABLE READ & READ COMMITTED:为了解决不可重复度,MySQL采用了MVVC(多版本并发控制)的方式。我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个版本的字段,即为row trx_id,而这个字段就是使其产生的事务的id,事务ID记为transation id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
20、如何利用Redis实现一个分布式锁
得分点:
为什么要实现分布式锁、实现分布式锁的方式;
标准回答 :
在分布式的环境下,会发生多个server并发修改同一个资源的情况,这种情况下,由于多个server是多个不同的JRE环境,而Java自带的锁局限于当前JRE,所以java自带的锁机制在这个场景下是无效的,那么就需要我们自己来实现一个分布式锁,采用Redis实现分布式锁,我们可以在Redis中存一份代表锁的数据,数据格式通常使用字符串即可。
- 首先加锁的逻辑可以通过‘setnx key value’来实现,但如果客户端忘记解锁,那么这种情况就很有可能造成死锁,但如果直接给锁增加过期时间即新增‘expire key seconds’又会发生其他问题,即这两个命令并不是原子性的,那么如果第二步失败,依然无法避免死锁问题,考虑到如上问题,我们最终可以通过‘set…nx…’命令,将加锁,过期命令编排到一起,把他们变成原子操作,这样就可以避免死锁。写法为‘set key value nx ex seconds’.
- 解锁就是将代表锁的那份数据删除,但不能用简单的‘del key’,因为会出现一些问题,比如此时有进程A,如果进程A在任务没有执行完毕时,锁被到期释放了,这种情况下进程A在任务完成后依然会尝试释放锁,因为它的代码逻辑规定它在任务结束后释放锁,但是它的锁早已经被释放过了,那这种情况他释放的就可能是其他线程的锁。为解决这种情况,我们可以在加锁时为key赋一个随机值来充当进程的标识,进程要记住这个标识。当进程解锁的时候进行判断,是自己持有的锁才能释放,否则不能释放。另外判断,释放这两步需要保持原子性,否则如果第二步失败,就会造成死锁,而获取和删除命令不是原子的,这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。
- 总之,就是在单个主节点的架构上实现分布式锁,是无法保证高可用的,若要保证分布式锁的高可用,则是采用多个节点的实现方案,这种方案有很多,而Redis的官方给出的建议是采用RedLock算法的实现方案,该算法基于多个Redis节点,它的基本逻辑如下:
- 这些节点相互独立,不存在主从复制或者集群协调机制;
- 加锁,以相同的KEY向N个实例加锁,只要超过一把节点成功,则认定加锁成功;
- 解锁:向所有的实例发送DEL命令,进行解锁;我们可以自己实现该算法,也可以直接使用Redission框架。
21、请说说你对反射的了解
得分点:
发射概念,通过反射机制可以实现什么;
标准回答 :
Java程序中,许多对象在运行时都会有编译时异常和运行时异常两种,例如多态情况下Car c = new Audi();这行代码运行时会生成一个c变量,在编译时该变量的类型是Car,运行时该变量类型是Audi;另外还有更极端的情况,例如程序在运行时接收到了外部传入的一个对象,这个对象的编译时类型是Object,但程序又需要调用这个对象运行时类型的方法,这种情况下,有两种解决办法:
- 第一种做法是假设在编译时和运行时都完全直到类型的具体信息,在这种情况下,可以先试用instanceof运算符进行判断,再利用强制类型转化将其转换成其运行时类型的变量,
- 第二种做法就是编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。
- 具体来说,通过反射机制,我们可以实现如下的操作:
- 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息
- 程序运行时,可以通过反射创建一个类的实例,并访问该实例的成员;
- 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象;
加分回答
- Java的反射机制在实际项目中应用广泛,常见的应用场景有:
- 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
- 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
- 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。
22、请你说说ArrayList和LinkedList的区别
得分点:
数据结构、访问效率
标准回答 :
- ArrayList的实现是基于数据,LinkedList的实现是基于双向链表
- 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依赖地址指针和它后一个元素;连接在一起,查找某个元素的时间复杂度是O(N).
- 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList的任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引;
- LinkedList比ArrayList更占内存,因为LInkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
23、数据库为什么不用红黑树而用B+树
得分点:
磁盘IO
标准回答 :
首先,红黑树是一种近似平衡二叉树(不完全平衡),结点非黑即红的树,它的树高最高不会超过2*log(n),因此查找的时间复杂度为O(log(n)),无论是增删改查,它的性能都十分稳定;
- 但是红黑树本质还是二叉树,在数据量非常大时,需要访问+判断的节点数还是会比较多,同时数据是存在磁盘上的,访问需要进行磁盘IO,导致效率较低;
- 而B+数是多叉的,可以有效减少磁盘IO次数,同时B+树增加了叶子结点间的连接,能保证范围查询时找到起点和终点后快速取出需要的数据。
加分回答
- 红黑树做索引底层数据结构的缺陷,试想一下,以红黑树作为底层数据结构在面对这些表诗句动辄数百万数千万的场景时,创建的索引它的树高得有多高?索引从根节点开始查找,而如果我们需要查找的数据在底层的叶子节点上,那么树的高度是多少,就要进行多少次查找,数据存在磁盘上,访问需要进行磁盘IO,这会导致效率过低;那么红黑树作为索引数据结构的弊端即是:树的高度过高导致查询效率变慢。
24、请你说说Redis的数据类型
得分点:
string、hash、list、set、zset;
标准回答 :
Redis主要提供了5种数据结构:字符串(String)、哈希(Hash)、列表(set)、集合(set)、有序集合(zset)。
- String是一组字节,在Redis数据库中,字符串是二进制安全的,这意味着它们具有已知长度,并且不受任何特殊终止字符的影响,可以在一个字符串中存储最多2兆字节的内容,Redis列表定义为字符串列表,按照插入顺序排序,可以将元素添加到Redis列表的头部或尾部,列表的最大长度为232-1个元素(超过40亿个元素)。
- 哈希是键值对的集合,在Redis中哈希是字符串字段和字符串值之间的映射,因此,他们适合表示对象,每个哈希可以存储多达232-1字段-值对。
- 集合是Redis数据库中无序字符串集合,在Redis中,在Redis sorted sets里面当items内容大于64的时候同时使用了和skiplist两种设计实现,这也会为了排序和查找性能做的优化,关于时间复杂度:添加和删除都需要修改skiplist,所以复杂度为O(log(n)),但是如果仅仅是查找元素的话可以直接使用hash,其复杂度为O(1),其他的range操作复杂度一般为O(log(n)),当然如果是小于64的时候,因为是采用了ziplist的设计,其时间复杂度O(n)集合中的最大成员数232-1个元素(超过40亿个元素)。
- Redis有序集合类似于Redis集合,也是一组非重复的字符串集合,但是排序集的每个成员都与一个分数相关联,该分数用于获取从最小到最高分数的有序排序集,虽然成员是独特的,但可以重复分数。
加分回答
Redis还提供了BItmap、HyperLogLog,Geo类型,但这些类型都是基于上述核心数据类型实现的,5.0版本中,Redis新增加了Streams数据类型,它是一个功能强大的,支持多播的,可持久化的消息队列。
25、请你说说ConcurrentHashMap
得分点:
数据+链表+红黑树,锁的粒度;
标准回答 :
在JDK8中,ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树”的形式,同时,它又采用锁定头结点的方式降低了锁粒度,以较低的性能代价实现了线程安全,底层数据结构的逻辑可以参考HashMaori的实现,下面介绍线程安全的实现机制。
- 初始化数组或头结点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换(原子操作,基于Unsafe类的原子操作API);
- 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头结点,所以ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能最好;
- 扩容时会进行加锁处理,锁定的仍然是头结点,并且支持多个线程同时对数组扩容,提高并发能力,每个线程需先以CAS操作抢任务,争抢一段连续槽位的数据转移权,抢到任务后,该线程或锁定槽内的头结点,然后将链表或树中的数据迁移到新的数组里;
- 查找数据时并不会加锁,所以性能很好,另外,在扩容的过程中,依然可以支持查找操作,如果某个槽还未进行迁移,则直接可以从旧数组里找到数据,如果某个槽已经迁移完毕,但是整个扩容还没结束,则扩容线程会创建一个转发节点存入旧数组,届时查找线程根据转发节点的提示,从新数组中找到目标数据。
加分回答
- ConcurrentHashMap实现线程安全的难点在于多线程并发扩容,即当一个线程在插入数据时,若发现数组正在扩容,呢么它就会立即参与扩容操作,完成扩容后再插入数据到新数组,在扩容的时候,多个线程共同分担数据迁移任务,每个线程负责的迁移数量是(数组长度>>>3)/CPU核心数,也就是说,为线程分配的迁移任务,是充分考虑了硬件的处理能力的,多个线程依据硬件的处理能力,平均分摊一部分分槽的迁移工作,另外,如果计算出来的迁移数量小于16,则强制将其改为16,这是考虑到目前服务器领域主流的CPU运行速度,每次处理的任务过少,对于CPU的算力也是一种浪费。
25、说说缓存穿透,击穿,雪崩的区别
得分点:
三种问题的发生原因以及解决方式
标准回答 :
- 缓存穿透:是指客户端查询了根本不存在的数据,使得这个请求直达存储层,导致其负载过大甚至造成宕机,这种情况可能是由于业务层误将缓存和库中的数据删除造成的,当然也不排除有人恶意攻击,专门访问库中不存在的数据导致缓存穿透,我们可以通过缓存空对象的方式和布隆过滤器两种方式来解决这一问题,缓存空对象是指当存储层未命中后,仍然将空值存入缓存层,当客户端再次访问数据时,缓存层直接返回空值,还可以将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值;
- 缓存击穿:当一份访问量非常大的热点数据缓存失效的瞬间,大量的请求直达存储层导致服务奔溃,缓存击穿可以通过热点数据不设置过期时间来解决,这样就不会出现上述的问题,这是“物理”上永不过期,缓存击穿可以通过热点数据不设置过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存,处理永不过期的方式,我们也可以通过加互斥锁的方式来解决缓存击穿,即对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待,这个线程访问过后,缓存中的数据将会重建,届时其他线程就可以直接从缓存中取值;
- 缓存雪崩的解决方式有三种
- 第一种是在设置过期时间时,附加一个随机数,避免大量的key同时过期;
- 第二种是启用降级和熔断措施,即发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回;
- 第三种是构建高可用的Redis服务,也就是采用哨兵或集群模式,部署多个Redis实例,这样即使个别节点宕机,依然可以保持服务的整体可用。
26、Redis如何与数据库保持双写一致性
得分点:
四种同步策略即其可能出现的问题,重试机制
标准回答 :
保存缓存和数据库的双写一致性,共有四种同步策略,即先更新缓存再更新数据库、先更新数据库再更新缓存,先删除缓存再更新数据库,先更新数据再删除缓存。
- 先更新缓存的优点是每次数据变化时都能及时地更新缓存,这样不容易出现查询未命中的情况,但这种操作的消耗很大,如果是写入数据比较频繁的场景,可能会导致频繁的更新缓存却没有业务来读取该数据;
- 删除缓存的优点是操作简单,无论更新的操作复杂与否,都直接删除缓存中的数据,这种做法的缺点是,当删除了缓存之后,下一次查询容易出现未命中的情况,那么这时就需要再次读取数据库,那么对比而言,删除缓存无疑是更好的选择;
- 先操作数据库和后操作数据库的区别:
- 先操作数据库但删除缓存失败的话,如果第二步骤失败可能导致缓存和数据库得到相同的旧数据;
- 先操作数据库但删除缓存失败的话则会导致缓存和数据库得到的结果不一致,出现上述问题的时候,我们一般采用重试机制解决,而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行,当我们采用重试机制之后由于存在并发,先删除缓存依然可能存在缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致的情况。所以我们得到结论:先更新数据库,再删除缓存是影响更小的方案,如果第二步出现失败的情况,则可以采用重试机制解决问题。
27、说说你了解的线程同步方式
得分点:
synchronized、lock;
标准回答 :
Java主要通过加锁的方式实现线程同步,而锁有两类,分别是synchronized和Lock,synchronized可以加在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同:
- 加在普通方法上,则锁是当前的实例(this);
- 加在静态方法上,则锁是当前类的Class对象;
- 加在代码块上,则需要在关键字后面的小括号里,显式的指定一个对象作为锁对象,不同的锁对象,意味着不同的锁粒度,所以我们应该根据锁定的范围,准确的选择锁对象,从而准确的确定锁的粒度,降低锁带来的性能开销;
- synchronized是比较早期的API,在设计之初,没有考虑到超时机制,非阻塞形式,以及多个条件变量,若想通过升级的方式让synchronized支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码,因此,JDK的开发团队在1.5引入了Lock接口,并通过Lock支持了上述的功能,Lock支持的功能包括:支持响应中断,支持超时机制,支持以非阻塞的方式获取锁,支持多个条件变量(阻塞队列)。
加分回答
- synchronized采用“CAS+Mark Word”实现,为了性能考虑,并通过锁升级机制降低锁的开销,在开发环境中,synchronized会随着多线程竞争的加剧,按照如下步骤逐步升级:无锁,偏向锁,轻量级锁,重量级锁。Lock则采用“CAS+volatile”实现,其实现的核心是AQS,AQS是线程同步器,是一个线程同步的基础框架,它是基于模板方式模式,在具体的Lock实例中,锁的实现是通过AQS来实现的,并且可以根据锁的使用场景,派生出公平锁,不公平锁,读锁,写锁等具体的实现。
28、请你说说innodb和myisam的区别?
得分点:
事务、锁,读写功能;
标准回答 :
Innodb是具有事务,回滚和奔溃修复能力的事务安全型引擎,它可以实现行级锁来保证高性能的大量数据中的开发操作;MyISAM是具有默认支持全文索引,压缩功能及较高查询性能的非事务性引擎。
- 事务:InnoDB支出事务,MyISAM不支持;
- 数据锁:InnoDB支持行级锁,MyISAM只支持表级锁;
- 读写功能:InnoDB增删改性能更优,MyISAM查询性能更优;
- 全文索引:InnoDB不支持(但可通过插件等方式支持);MyISAM默认支持;
- 外键:INnoDB支持外键,MyISAM不支持;
- 存储结构:InnoDB需要更多的内存和存储;MyISAM支持三种不同的存储格式:静态表(默认),动态表,压缩表;
- 移植:InnoDB在数量上小时可通过数据文件、备份binlog,mysqldump工具移植,数据量大时比较麻烦;MyISAM可单独对某个表通过拷贝表文件移植;
- 奔溃恢复:InnoDB有奔溃恢复机制;MyISAM没有,默认推荐:InnoDB是MySQL5.5之后的默认引擎。
加分回答
- InnoDB中行级锁是怎么实现的?
- InnoDB行级锁是通过索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁,当表中锁定其中的某几行时,不同的事务可以使用不同的索引锁定不同的行,另外,不论使用主键索引,唯一索引还是普通索引,InnoDB都会使用行级锁对数据加锁。
29、String、StringBuffer、StringBuilder有什么区别?
得分点:
字符串是否可变,StringBuffer、StringBuilder线程安全问题;
标准回答 :
- java提供了String和StringBuffer两个类来封装字符串,并且提供了一系列方法来操控字符串对象;
- String是一个不可变类,也就是说,一个String对象创建以后,直到这个对象撤销为止,对象中的字符序列都不能被改变;
- StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer对象被创建之后,我们可以通过StringBuffer提供的append(),insert(),reserse(),setCharAt(),setLength()等方法来改变这个字符串对象的字符序列。当通过StringBuffer得到期待中字符序列的字符串时,就可以通过toString()方法将其转换为String对象;
- StringBuilder类是JDK1.5中新增的类,它也代表了字符串对象,和StringBuilder类相比,他们有着共同的父类‘AbstractStringBuilder’,二者无论是构造器还是方法都基本相同,不同的一点是,StringBuilder没有考虑线程安全问题,也正是如此,StringBuilder比StringBuilder性能略高,因此,如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuilder类。
30、说说了解的JVM内存模型
得分点:
类加载子系统、执行引擎,运行时数据区;
标准回答 :
JVM由三部分组成:类加载子系统,执行引擎,运行时数据区;
-
类加载子系统:可以根据指定的全限定名来载入类或接口;
-
执行引擎:负责执行那些包含在被载入类的方法中的指令;
-
当程序运行时,JVM会吧这些东西都存储到运行时数据区中,以便管理;
-
而运行时数据区又可以分为方法区、堆、虚拟机栈、本地方法栈、程序计数器;
加分回答
-
运行时数据区的理解,在运行时数据区所包含的几块内存空间中,方法区和堆是线程之间共享的内存区域,而虚拟机栈、本地方法栈、程序计数器则是线程私有的区域,就说每个线程都有自己的这个区域;
31、说说JVM的垃圾回收机制
得分点:
新生代收集,老年代收集,混合收集,整堆收集;
标准回答 :
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质上是一套符合大多数程序实际情况的经验法则,而分代收集理论,建立在如下三个分代假说之上即弱分代假说,强分代假说,跨代引用假说。
依据分代假说理论,垃圾回收可以分为如下几类:
- 新生代收集:目标为新生代的垃圾收集;
- 老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为;
- 混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;
- 整堆收集:目标为整个堆和方法区的垃圾收集。
加分回答
- HotSpot虚拟机内置了很多垃圾收集器有CMS、serial Old、Parallel Old。此外,HotSpot还内置了面向整堆的G1收集器,在上述的收集器中,常见的组合方式有:
- Serial + Serial Old,是客户端模式下常用的收集器;
- ParNew + CMS,是服务端模式下常用的收集器;
- Parallel Scavenge + Parallel Old,适用于后台运算而不需要太多交互的分析任务。
32、说说类加载机制
得分点:
加载,验证,准备,解析,初始化;
标准回答 :
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载,验证,准备,解析。初始化,使用,卸载七个阶段,其中验证,准备,解析三个部分统称为连接,而前五个阶段则是类加载的完整过程。
- 在加载阶段JVM需要在内存中生成一个代表这个类的Class对象,作为这个方法区这个类的各种数据的访问入口;
- 验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证;
- 准备阶段是正式为类中定义变量(静态变量)分配到内存并设置类变量初始值的阶段,这些变量所使用的内存都应当在方法区中济宁分配,但必须注意到方法区本身是一个逻辑上的区域;
- 解析阶段是java虚拟机将常量池内的符号替换为直接引用的过程,符号引用以一组符号来描述所引用的目标,直接引用是可以直接指向目标的指针,相对偏移量或者一个能直接定位到目标的句柄;
- 类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,java虚拟机才真正开始执行类中编写的java程序代码,将主导权移交给应用程序,本质上,初始化阶段就是执行类构造器‘()’的过程,‘()’并不是程序员在java代码中直接编写的方法,它是javac编译器的自动生成物。
加分回答
- 关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握,但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”:
- 使用new实例化对象,读写类的静态字段,调用类的静态方法时;
- 使用java.lang.reflect包的方法对类型进行反射调用时;
- 当初始化类时,若发现其父类还没有进行初始化,则先初始化这个父类;
- 虚拟机启动时,需要指定一个要执行的主类,虚拟机会先初始化这个主类;
- 当使用JDK7新假如的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newinvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
- 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
33、请你说说抽象类和接口的区别
得分点:
接口和抽象类的方法,接口与抽象类的常量与变量,单继承多实现;
标准回答 :
接口和抽象类相同点有:
- 接口和抽象类都不能被实例化,他们都位于继承树的顶端,用于被其他类实现和继承;
- 接口和抽象类都可以有抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法,在用法上,接口和抽象类也有如下差异:
- 接口里只能包含抽象方法和默认方法,不能为普通方法提供方法实现;
- 抽象类则可以包含普通方法;
- 接口里只能定义静态常量,不能定义普通成员变量;抽象类里既可以定于普通成员变量,也可以定义静态变量;
- 接口里不包含构造器;抽象类可以包含构造器,但抽象类的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足,总之,接口通常是定义允许多个实现的类型的最佳途径,但当演变的容易性比灵活性和功能更加重要时,应该使用抽象类来定义类型。
加分回答
- 在二者的设计目的上,接口作为系统与外界交互的窗口,体现了一种规范,对于接口的实现者来说,接口规定了实现者必须向外提供哪些服务;对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务;
- 当一个程序中使用接口时,接口是多个模块间的耦合标准;
- 当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准;
- 抽象类则不一样,抽象类作为系统中多个子类的共同父类,它体现的是一种模板式设计;
- 抽象类作为多个子类的父类,它可以被当作系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当作最终产品,必须要有更进一步的完善,这种完善可能有几种不同的方式。
34、请你说说==和equals()的区别
得分点:
==和equals()比较的基本用法;
标准回答 :
==和equals都是java中判断两个变量是否相等的方式,如果判断的是两个基本类型的变量,并且两者都是数值类型(不一定要求数据类型完全相同),只要两个变量的值相等就会返回TRUE。
- 对于两个引用变量只有他们指向同一个引用时,==才会返回true,==不能用于比较类型上没有父子关系的两个对象;
- equals()方法是object类提供的一个实例方法,所以所有的引用变量都能调用equals方法来判断它是否与其他引用变量相等,但使用这个方法来判断两个引用变量的对象是否相等的判断标准与使用==运算符没有区别,它同样要求两个引用变量指向同一个对象才会返回true,但如果这样的话,equals方法就没有了存在的意义,所以如果我们希望自定义判断相等的标准时,可以通过重写equals方法来实现;
- 重写equals()方法时,相等条件是由业务要求决定的,因此equals()方法的实现是由业务要求决定的。
35、说说synchronize的用法及原理
得分点:
作用于三个位置,对象头,锁升级;
标准回答 :
用法synchronized可以作用于在三个不同的位置,对应三种不同的使用方式,这三种不同的使用方式,这三种方式的区别是锁对象的不同,不同的锁对象,与位置不同的锁粒度,所以我们不应该将它加在方法前了事,尽管通常这可以解决问题,而是应该根据要锁定的范围,准确额的选择锁对象,从而准确的确定锁的粒度,降低锁带来的性能开销。
- 作用在静态方法上,则锁升级当前类的Class对象;
- 作用在普通方法上,则锁是当前的实例(this);
- 作用于代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。
- 原理synchronized的底层是采用Java对象头来存储锁信息的,并且还支持锁升级。
- Java对象头包含三部分,分别是Mark Word,Class Metadata Address,Array length。其中Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度,如果对象不是数组类型,则没有Array length信息,synchronized的锁信息包括锁的标志和锁的状态,这些信息都存放在对象头的Mark Word这一部分。
- Java 6为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,所以在Java 6中,锁一共被封为四种状态,级别由低到高依次是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。随着线程竞争情况的升级,锁的状态会从无锁状态逐步升级到重量级锁状态。
- 锁可以升级却不能降级,这种只能升不能降的策略,是为了提高效率。
- synchronized的早期设计并不包含锁升级机制,所以性能较差,那个时候只有无锁和有锁之分,是为了提升性能才引入了偏向锁向于某一线程。
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步块时就不需要做加锁和解锁操作了,只需要简单的测试一下Mark Word里是否存储者自己的线程ID即可。
10.轻量级锁:就是加锁时JVM先在当前线程栈帧中创建用于存储锁记录的空间,并将Mark Word复制到锁记录中,官方称之为Displaced Mark Word,然后线程尝试以CAS方式将Mark Word替换为指向锁记录的指针,如果成功则当前线程获得所,如果失败则表示其他线程竞争锁,此时当前线程就会通过自旋来尝试获取锁。
加分回答
锁升级的过程:
- 开始,没有任何线程访问同步块,此时同步块处于无锁状态;
- 然后,线程1首先访问同步块,它以CAS的方式修改Mark Word,尝试加偏向锁,由于此时没有竞争,所以偏向锁加锁成功,此时Mark Word里存储的是线程1的ID;
- 然后,线程2开始访问线程块,它以CAS的方式修改Mark Word,尝试加偏向锁,由于此时存在竞争,所以偏向锁加锁失败,于是线程2会发起撤销偏向锁的流程(清空线程1的ID),于是同步块从偏向线程1的状态恢复到了可以公平竞争的状态;
- 然后,线程1和线程2共同竞争,他们同时以CAS方式修改Mark Word,尝试加轻量级锁,由于存在竞争,只有一个线程会成功,假设线程1成功了,但线程2不会轻易放弃,它认为线程1很快就能执行完毕,执行权很快就会落到自己头上,于是线程2继续自旋加锁;
- 最后,如果线程1很快执行完,则线程2就会加轻量级锁成功,锁不会晋升到重量级状态,也可能是线程1执行时间较长,那么线程2自旋一定次数后就会放弃自旋,并发起锁膨胀的流程,届时,锁被线程2修改为重量级锁,之后西安城2进入阻塞状态,而线程1重复加锁或者解锁,CAS操作都会失败,此时他会释放锁并唤醒等待的线程,总之,在锁升级的机制下,锁不会一步到位变为重量级锁,而是根据竞争的情况逐步升级的,当竞争小的时候,只需要以较小的代价加锁,直到竞争加剧,才使用重量级锁,从而减少了加锁带来的开销。
36、说说你对AQS的理解
得分点:模板方法,同步队列;
标准回答 :
AQS(AbstractQueuedSynchronizer)是从队列同步器,是用来构建锁的基本框架,Lock实现类都是基于AQS实现的,AQS是基于模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法。
- AQS 内部定义了一个FIFO的队列来实现线程的同步,同步时还定义了同步状态记录锁的信息,AQS的模板方法,将管理同步状态的逻辑提炼出来形成标准流程,这些方法主要包括:独占式获取锁的信息,独占式释放同步信息,共享式获取同步信息,共享式释放同步状态。
- 以独占式获取同步状态为例,它的大致流程是:
- 尝试以独占方式获取同步状态;
- 如果状态获取失败,则将当前线程加入同步队列;
- 自旋处理同步状态,如果当前线程位于队头,则唤醒它并让它出队,否则使其进入阻塞状态,其中有些步骤无法确定在父类确定,则提炼成空方法留待子类实现,例如,第一步的尝试操作,对于公平锁和非公平锁来说就不一样,所以子类在实现时需要按照场景各自实现这个方法;
- AQS的同步队列,是一个双向链表,AQS则持有链表的头尾节点,对于尾节点的设置,是存在多线程竞争的,所以采用CAS的方式进行修改,对于头结点设置,则一定是拿到了同步状态的线程才能处理,所以修改头结点不需要采用CAS的方式;
- AQS的同步状态,是一个int类型的整数,它在表示状态的同时还能表示数量,通常情况下,状态为0时表示无锁,状态大于0时表示锁的重入次数,另外,在读写锁的场景中,这个状态标志既要记录锁又要记录写锁,于是,锁的实现者就将状态表示拆成高低两部分,高位存读锁、低位存写锁。
加分回答
- 同步状态需要在开发环境下修改,所以需要保证其线程安全,由于AQS本身就是锁的实现工具,所以不适合用锁来保证其线程安全,因为如果你用一个锁来定义另一个锁的话,那干脆直接用synchronized算了,实际上,同步状态时被volatile修饰的,该关键字可以保证状态变量的内存可见性,从而解决了线程安全问题。
37、Java那些地方使用了CAS
得分点:
原子类,AQS,并发容器;
标准回答 :
Java提供的API中使用了CAS的地方有很多,比较经典的使用场景有原子类,AQS,并发容器。
- 对于原子类,以AtomicInteger为例,它的内部提供了诸多原子操作的方法,如原子替换整数值,增加指定的值,加1,这些方法的底层便是采用操作系统提供的CAS原子指令来实现的,对于AQS,在向同步队列的尾部追加节点时,它首先会以CAS的方式尝试一次,如果失败则进入自旋状态,并反复以CAS的发那个是进行尝试,此外,在以共享方式释放同步状态时,他也是以CAS方式对同步状态进行修改的,对于并发容易以ConcurrentHashMap为例,它的内部多次使用了CAS操作,在初始化数组时,它会以CAS的方式修改初始化状态,避免多个线程同时进行初始化,在执行put方法初始化头结点时,它会以CAS的方式将初始化好的头结点设置到指定槽首位,避免多个线程同时设置头结点,在数组扩容时,每个线程会以CAS方式修改任务序列号来争抢扩容任务,避免和其他线程产生冲突,在执行get方法是,它会以CAS的方式修改任务序列号来争抢扩容任务,避免其他线程同时对头结点做出修改。
加分回答
- CAS的实现离不开操作系统原子指令的支持,Java中对原子指令封装的方法集中在Unsafe类中, 包括:原子替换引用类型,原子替换int型整数,原子替换long型整数,这些方法都有四个参数:var1、var2、var4、var5,其中var1代表要操作的对象,var2代表要替换的成员变量,var4代表期望的值,var5代表更新的值。
- public final native boolean compareAndSwapObject( Object var1, long var2, Object var4, Object var5);
- public final native boolean compareAndSwapInt( Object var1, long var2, int var4, int var5);
- public final native boolean compareAndSwapLong( Object var1, long var2, long var4, long var6);
38、说说JVM的垃圾回收算法
得分点:
标记清除、标记复制、标记整理;
标准回答 :
常用的垃圾回收算法有:标记清除算法、标记复制算法、标记整理算法;
- 标记清除算法:算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象,他主要有如下两个缺点:
- 第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要回收的,这是必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象的数量增长而降低;
- 第二个是内存空间碎片化的问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当程序在运行过程中需要分配较大对象时无法找到足够的连续的内存而不得不提前触发另一次垃圾收集;
- 标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当着一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。对于大多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点,另外,如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,所以,现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。
- 标记整理算法:针对老年代对象的存亡特征,1974年Edward Luedures提出了另外一种有针对性的“标记-整理”算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一段移动,然后直接清理掉边界以外的内存,如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行,像这样的停顿被最初的虚拟机设计者形象的描述为“Stop The World”。
加分回答
- 目前新生代的垃圾回收采用标记复制算法的比较多,老年代的垃圾回收采用标记整理算法比较多,而标记复制算法浪费一半内存的缺点长期以来被人诟病,所以业界也有人针对该算法给出了改进的方案,IBM公司曾有一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有98%熬不过第一轮收集,因此并不需要按照1:1的比例来划分新生代的内存空间,在1989年,Andrew Appel针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel式回收”。Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,发生垃圾回收搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局,Hotspot虚拟机默认Eden和Survivor的大小比例是8:1:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被浪费的,98%的对象可被回收的仅仅是普通场景下测得的数据,任何人都没有办法百分百保证每次回收都只有不多于10%的对象存活,因此Apple式回收还有一个充当罕见情况的逃生门的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。
39、请你说说Redis数据类型中的zset,它和set有什么区别?底层是怎么实现的?
得分点:
有序无序,底层结构;
标准回答 :
Redis有序集合和集合一样也是string类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个double类型的分数Redis正是通过分数来为集合中的成员进行从小到大的排序,
- 有序集合的成员是唯一的,但分数(score)却可以重复,集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1),集合中最大的成员数为232-1(4294967295),每个集合可存储40亿个成员,zset的底层的存储结构包括ziplist或skiplist,其他时候使用skiplist,当skiplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素及分值,使用dict来保存元素的分值,当skiplist作为zset的底层存储结构的时候,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系;
加分回答
- 实际上单独使用Hashmap或skiplist也可以实现有序集合,Redis使用两种数据结构组合的原因是如果我们单独使用Hashmap,虽然能以O(1)的时间复杂度查找成员非分值,但是因为Hashmap是以无序的方式来保存元素,所以每次进行范围操作的时候都要进行排序,而如果单独使用skiplist,虽然能执行范围操作,但查找操作的复杂度却由O(1)变为O(logN),因此Redis使用了两种数据结构来共同实现有序集合。
40、说说static修饰符的用法
得分点:
static可以修饰什么,static的重要规则;
标准回答 :
Java类中包含了成员变量,方法,构造器,初始化块和内部类(包括接口,枚举)5种成员,static关键字可以修饰除了构造器外的其他4中成员。
- static关键字修饰的成员被称为类成员,类成员属于整个类,不属于单个对象,static关键字有一条非常重要的规则,即类成员不能访问实例成员,因为类成员属于类的,类成员的作用域比实例成员的作用域更大,很容易出现类成员初始化完成时,但实例成员还没有被初始化,这时如果类成员访问实例成员就会引起大量错误。
加分回答
- static修饰的部分会和类同时被加载,被static修饰的成员先于对象存在,因此,当一个类加载完毕,即使没有创建对象也可以去访问被static修饰的部分,静态方法中没有this关键词,因为静态方法是和类同时被加载的,而this是随着对象的创建存在的,静态比对象优先存在,也就是说,静态可以访问静态,但静态不能访问非静态而非静态可以访问静态。
41、说说线程的状态
得分点:
NEW,RUNNABLE,BLOCKED,WTING,TIMED_WTING,TERMINATED;
标准回答 :
JAVA线程在运行时的生命周期中,在任意给定的时刻,只能处于下列6种状态之一:
- NEW:初始状态,线程被创建,但是还没有调用start方法;
- RUNNABLE:可运行状态,线程正在JVM中执行,但是有可能在等待操作系统的调度;
- BLOCKED:阻塞状态,线程正在等待获取监视器锁;
- WTING:等待状态,线程正在等待其他线程的通知或中断;
- TIMED_WTING:超时等待状态,在WTING的基础上增加了超时时间,即超出时间自动返回;
- TERMINATED:终止状态,线程已经执行完毕;
- 线程在创建之后默认为初始状态,在调用start方法之后进入可运行状态,可运行状态不代表线程正在执行,它有可能正在等待操作系统的调度,进入等待状态的线程需要其他线程的通知才能返回到可运行状态,而超时等待状态,相当于在等待状态非的基础上,增加了超时限制,除了其他线程的唤醒,在超时时间到达时也会发返回运行状态,此外,线程在执行同步方法时,在没有获取到锁的情况下,会进入阻塞状态,线程在执行完run方法之后,会进入到终止状态;
加分回答
- Java将操作系统中的就绪和运行两个状态合并为可运行状态(RUNNABLE),线程阻塞于synchronized的监视器锁时会进入阻塞状态,而线程阻塞与Lock锁时会进入的却是等待状态,这是因为Lock接口实现类对于阻塞的实现均使用了LockSupport类中的相关方法;
42、说说你对ThreadLocal的理解
得分点:
作用,实现机制;
标准回答 :
ThreadLocal,即线程变量,它将需要并发访问的资源复制多份,让每个线程拥有一份资源,由于每个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。
- ThreadLocal提供了线程安全的共享机制,在编写多线程代码时,可以吧不安全的变量封装进ThreadLocal,在实现上,Thread类中声明了ThreadLocals变量,用于存放当前线程独占的资源,ThreadLocal类中定义了该变量的类型(ThreadLocalMap),这是一个类似于Map的结构,用于存放键值对,ThreadLocal类中还提供了set和get方法,set方法会初始化ThreadLocalMap并将其绑定到Thread.threadlocals,从而将传入的值绑定到当前线程,在数据存储上,传入的值将作为键值对的value,而key则是ThreadLocal对象本身(this)。get方法没有任何参数,它会以当前的ThreadLocal对象(this)为key,而Thread.threadlocals中获取与当前线程绑定的数据。
加分回答
- 注意,ThreadLocal不能替代同步机制,两者面向的问题领域不同,同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式,而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了,一般情况下,如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制,如果仅仅需要隔离多个线程之间的共享冲突,则可以使用THreadLocal。
43、说说Spring Boot常用的注解
得分点:
Spring Boot常用注解的作用
标准回答 :
- @SpringBootApplication注解:在Spring Boot入口类中,唯一的一个注解就是@SpringBootApplication,它是Spring Boot项目的核心注解,用于开启自动配置,准确来说就是通过该注解内组合的@EnableAutoConfiguration开启了在佛那个配置;
- @EnableAutoConfiguration注解:主要功能是启动Spring应用程序上下文时进行自动配置,它会尝试猜测并配置项目可能需要的Bean,自动配置通常是基于项目的classpath中引入的类个已定义的Bean来实现的,在此过程中,被自动配置的组件来自项目自身和项目依赖的jar包中;
- Import注解:@EnableAutoConfiguration的关键功能是通过@Import注解导入的ImportSelector来完成的,从源代码得知@Import(AutoConfigurationImportSelector.class)是@EnableAutoConfiguration注解的组成部分,也是自动配置功能的核心实现者;
- @Conditional注解:@Conditional注解是由Spring4.0版本引入的性特征,可根据是否吗,满足指定的条件来决定是否进行Bean的实例化及装配,比如,设定当类路径下包含某个jar包的时候才会对注解的类进行实例化操作,总之,就是根据一些特定的条件来控制Bean实例化的行为。
44、说说Bean的生命周期
得分点:
Spring Bean生命周期的四大部分以及详细步骤;
标准回答 :
Bean生命周期大致分为Bean定义,Bean的初始化,Bean的生存期和Bean的销毁4个部分;
- Spring启动,查找并加载需要被Spring管理的Bean,进行Bean的实例化;
- Bean实例化后对键Bean的引入和值注入到Bean的属性中;
- 如果Bean实现了BeanNameAware接口的话,Spring将Bean的id传递给setBeanName()方法;
- 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;
- 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来;
- 如果Bean实现了BeanPostPrecessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法;
- 如果Bean实现了init-method声明了初始化方法,该方法也会被调用;
- 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法;
- 此时,Bean已经准备就绪,可以被应用程序使用了,他们将一直驻留在应用上下文中,直到应用上下文被销毁;
- 如果bean实现了DisposableBean接口 ,Spring将调用它的destory()方法,同样,如果bean使用了destory-method声明销毁方法,该方法也会被调用。
加分回答
- 这个过程是由Spring容器自动管理的,其中有两个环节我么可以进行干预:
- 我们可以自定义初始化方法,并在该方法前增加#@PostConstruct注解,届时Spring容器将在调用SetBeanFactory方法之后调用该方法;
- 我们可以自定义销毁方法,并在该方法前增加@PreDestory注解,届时Spring容器将在自身销毁前,调用这个方法。