秋招面试题(阿里)

目录html

 

1.Mysql表在磁盘中的存储方式java

2.对DB引擎的了解,不一样引擎的对比git

3.实现共同好友功能的sql语句github

4.sql语句的优化https://www.cnblogs.com/parryyang/p/5711537.htmlgolang

5.执行select语句的具体流程,何时追加日志文件redis

6.CAS的底层实现算法

7.项目的JVM调优细节https://zhuanlan.zhihu.com/p/58897189sql

8.TCP是如何处理的,三次握手,四次挥手,四次挥手时,若是发生丢包会发生什么数据库

10.如何顺序执行几个线程vim

11.Mysql数据库为何采用B+树,为了让树变得矮,子节点越多越好吗?

12.Mysql的联合索引和聚簇索引

13.hash算法的原理,hash冲突,怎么从原理上优化hash算法,将hash冲突降到最低,hash值是怎么算的,一致性hash

14.链表与数组的区别

15.红黑树除了解决查询效率的问题还解决了什么问题,怎么将链表转成红黑树的

18.四种引用类型

19.java里面为何要加锁

20.Java里面的各类锁

21.锁的应用场景

22.AQS原理与常见的同步器

23.hashmap扩容

24.JDK中消费者生产者应用场景

25.HashSet和TreeSet的区别

26.枚举的构造方法是私有的仍是共有的,为何?

27.设计模式的原则

28.设计模式

29.三个线程如何实现交替打印ABC


1.Mysql表在磁盘中的存储方式

答:每次建立表时Mysql都会建立一个与表名相同的磁盘文件,用于保护该表的格式文件。扩展名为.frm。此文件与操做系统和存储引擎无关,不管是哪一个存储引擎和操做系统,都会建立的。除此以外,Mysql会根据不一样的存储引擎建立不一样的数据库文件。

InnoDB 存储引擎: 磁盘文件 .idb;(存放表数据和索引)InnoDB引擎会把表的数据和索引存储在它的系统表空间里。

MyISAM存储引擎:  数据文件.MYD , 索引文件 .MYI

2.对DB引擎的了解,不一样引擎的对比

答:MyISAM和InnoDB

(1)MyISAM:是MySQL的默认数据库引擎(5.5版以前)。虽然性能极佳,并且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,并且最大的缺陷就是崩溃后没法安全恢复。

(2)InnoDB:5.5版本以后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。

二者的对比:

  1. 是否支持行级锁 : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
  2. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具备原子性,其执行速度比InnoDB类型更快,可是不提供事务支持。可是InnoDB 支持ACID事务,外部键等高级数据库功能。 具备事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
  3. 是否支持外键 MyISAM不支持,而InnoDB支持。
  4. 是否支持MVCC :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 READ COMMITTEDREPEATABLE READ 两个隔离级别下工做;MVCC能够使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。
  5. 是否支持全文索引:MyIASM支持全文类型索引,而InnoDB不支持全文索引

3.实现共同好友功能的sql语句

答:第一想法就是经过redis来是实现,redis set 的交集

命令:SINTER friend:a friend:b  编码:setOperations.intersect(A_FRIEND_KEY, B_FRIEND_KEY); 

而后这里多是想问sql查询语句

userand 为表名,是朋友关系表,UserOne是它的第一个字段,表明用户的id,UserTwo 是它的第二个字段,表明用户的id,表示两个用户是好友,如今查询用户1和用户2的共同好友:

select UserTwo from userand WHERE UserOne=1 AND UserTwo exists( select UserTwo from userand WHERE UserOne=2)

查询用户1和用户2的共同好友个数:

select COUNT(UserTwo) from userand WHERE UserOne=1 AND UserTwo in( select UserTwo from userand WHERE UserOne=2)

4.sql语句的优化http://www.javashuo.com/article/p-clacsrtm-ba.html

答:

(1)查询性能低下的最基本缘由是访问数据太多,所以要优化数据访问量,查看是否请求了不须要的数据。

解决方法:

1.LIMIT()限制返回数据数量;

2.单表查询或多表关联查询,只返回须要的列;

3.对应老是重复查询的数据,可采用缓存存储,避免屡次查询

4.正确的创建索引(创建在常常查询,不多修改,内容差异较大的列上面)复合索引把选择性最高的列排在前面,采用的是最左匹配原则的

a. ORDER BY + LIMIT组合的索引优化

若是一个SQL语句形如:SELECT [column1],[column2],…. FROM [TABLE] ORDER BY [sort] LIMIT [offset],[LIMIT];

这个SQL语句优化比较简单,在[sort]这个栏位上创建索引便可。

b. WHERE + ORDER BY + LIMIT组合的索引优化

若是一个SQL语句形如:SELECT [column1],[column2],…. FROM [TABLE] WHERE [columnX] = [VALUE] ORDER BY [sort] LIMIT [offset],[LIMIT];

这个语句,若是你仍然采用第一个例子中创建索引的方法,虽然能够用到索引,可是效率不高。更高效的方法是创建一个联合索引(columnX,sort)

c. WHERE+ORDER BY多个栏位+LIMIT

 若是一个SQL语句形如:SELECT * FROM [table] WHERE uid=1 ORDER x,y LIMIT 0,10;

对于这个语句,多是加一个这样的索引:(x,y,uid)。但实际上更好的效果是(uid,x,y)。这是由MySQL处理排序的机制形成的。

5.一些会引发全表查询的状况

  会引发全表查询索引失效 优化的方法
like SELECT id FROM A WHERE name like '%abc%' SELECT id FROM A WHERE name like 'abc%'
!=和<> SELECT id FROM A WHERE ID != 5 SELECT id FROM A WHERE ID>5 OR ID<5
IS NULL 和IS NOT NULL SELECT id FROM A WHERE num IS NULL SELECT id FROM A WHERE num=0
or SELECT id FROM A WHERE num =10 or num = 20 SELECT id FROM A WHERE num = 10 union all SELECT id FROM A WHERE num=20
in 和not in SELECT id FROM A WHERE num in(1,2,3) SELECT id FROM A WHERE num between 1 and 3
  SELECT id FROM A WHERE num in(select num from b ) SELECT num FROM A WHERE num exists(select 1 from B where B.num = A.num)
  SELECT id FROM A WHERE num in(select num from B) SELECT id FROM A LEFT JOIN B ON A.num = B.num

6.要进行批量插入,

INSERT into person(name,age) values('A',14)

INSERT into person(name,age) values('B',14)

INSERT into person(name,age) values('C',14)

优化为:INSERT into person(name,age) values('A',14),('B',14),('C',14),

7.不要在where子句中的“=”左边进行函数、算数运算或其余表达式运算,不然系统将可能没法正确使用索引。

如SQL:SELECT id FROM A WHERE num/2 = 100 优化成:SELECT id FROM A WHERE num = 100*2

如SQL:SELECT id FROM A WHERE substring(name,1,3) = 'abc' 优化成:SELECT id FROM A WHERE LIKE 'abc%'

如SQL:SELECT id FROM A WHERE datediff(day,createdate,'2016-11-30')=0 优化成:SELECT id FROM A WHERE createdate>='2016-11-30' and createdate<'2016-12-1'

 如SQL:SELECT id FROM A WHERE year(addate) <2016 优化成:SELECT id FROM A where addate<'2016-01-01'

8.排序的索引问题 

一个查询语句只会用一个索引,所以若是where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。所以数据库默认排序能够符合要求状况下不要使用排序操做;尽可能不要包含多个列的排序,若是须要最好给这些列建立复合索引。

9.尽可能用 union all 替换 union

  union和union all的差别主要是前者须要将两个(或者多个)结果集合并后再进行惟一性过滤操做,这就会涉及到排序,增长大量的cpu运算,加大资源消耗及延迟。因此当咱们能够确认不可能出现重复结果集或者不在意重复结果集的时候,尽可能使用union all而不是union  

10 exist 代替 in

in()适合B表比A表数据小的状况 exists()适合B表比A表数据大的状况

5.执行select语句的具体流程,何时追加日志文件

答:整个语句的执行过程以下:

1)       读取from子句中基本表、视图的数据,执行笛卡尔积操做;

2)       选取知足where子句中给出的条件表达式的元组;

3)       按group子句中指定列的值分组,同时提取知足having子句中组条件表达式的那些组;

4)       按select子句中给出的列名或列表达式求值输出;

5)       order子句对输出的目标表进行排序,按附加说明asc升序排列,或按desc降序排列。

6.CAS的底层实现

答:比较并交换。CAS有3个操做数,内存值V旧的预期值A要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。CAS是经过unsafe类的compareAndSwapInt(this, valueOffset, expect, update方法实现的

Unsafe是CAS的核心类,Java没法直接访问底层操做系统,而是经过本地(native)方法来访问。不过尽管如此,JVM仍是开了一个后门,JDK中有一个类Unsafe它提供了硬件级别的原子操做。valueOffset表示的是变量值在内存中的偏移地址,由于Unsafe就是根据内存偏移地址获取数据的原值的。

volatile修饰的变量具备可见性的。

CAS存在一个很明显的问题,即ABA问题和只能保证一个共享变量的原子操做。
若是变量V初次读取的时候是A,而且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其余线程修改过了吗?若是在这段期间它的值曾经被改为了B,而后又改回A,那CAS操做就会误认为它历来没有被修改过。针对这种状况,java并发包中提供了一个带有标记的原子引用类”AtomicStampedReference”,它能够经过控制变量值的版本来保证CAS的正确性。

CAS 只对单个共享变量有效,当操做涉及跨多个共享变量时 CAS 无效。可是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行 CAS 操做.因此咱们能够使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操做。

7.项目的JVM调优细节https://zhuanlan.zhihu.com/p/58897189

答:对JVM内存的系统级的调优主要的目的是减小GC的频率和Full GC的次数。

(1)Full GC会对整个堆进行整理,包括Young、Tenured和Perm。Full GC由于须要对整个堆进行回收,因此比较慢,所以应该尽量减小Full GC的次数。

Full GC触发缘由

1)老代被写满:调优时尽可能让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要建立过大的对象及数组避免直接在老年代建立对象 。

2)持久代Pemanet Generation空间不足:增大Perm Gen空间,避免太多静态对象 , 控制好新生代和旧生代的比例

3)System.gc()被显示调用:垃圾回收不要手动触发,尽可能依靠JVM自身的机制

(2)JVM性功调优

1)监控GC状态:使用各类JVM工具,查看当前日志,分析当前JVM参数设置,而且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,以为是否进行优化。

2)生成堆的dump文件:经过JMX的MBean生成当前的Heap信息,大小为一个3G(整个堆的大小)的hprof文件,若是没有启动JMX能够经过Java的jmap命令来生成该文件。

3)分析dump文件:打开这个3G的堆信息文件,显然通常的Window系统没有这么大的内存,必须借助高配置的Linux,几种工具打开该文件:

4)分析结果,判断是否须要优化:若是各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,若是GC时间超过1-3秒,或者频繁GC,则必须优化。

5)调整GC类型和内存分配:若是内存分配过大或太小,或者采用的GC收集器比较慢,则应该优先调整这些参数,而且先找1台或几台机器进行beta,而后比较优化过的机器和没有优化的机器的性能对比,并有针对性的作出最后选择。

8.TCP是如何处理的,三次握手,四次挥手,四次挥手时,若是发生丢包会发生什么

9.mybatis是如何实现的

10.如何顺序执行几个线程

答:

(1)使用join()方法,这个方法的做用是,让当前执行线程等待直到调用join方法的线程结束运行或者完成join()内规定的时间才能够继续执行;

(2)使用单线程池,保证只用一个线程能够执行。

11.Mysql数据库为何采用B+树,为了让树变得矮,子节点越多越好吗?

答:(1)Mysql数据库的数据是存储在磁盘当中的,所以读取数据一定要访问磁盘。当大规模数据存储在磁盘当中,须要查询的时候定位是很是耗时的。B+树,能提升磁盘读取定位的效率

1)B类树可以尽量多的在结点上存储相关信息,保证层数尽量少,这样,更方便的进行读取,磁盘I/O操做也能减小;

2)B类树是平衡树,这样每一个节点到根节点的高度都同样,保证了查询的稳定性;

3)采用B+树是由于B+树比B-树存在更多的优势:

  • 更低的磁盘读取代价:B+树只有在叶子节点才存储数据,非叶子节点存储的索引指针,这样可以尽量多的在叶子节点上存储更新指针,这样相对B-树就更矮,磁盘读写的代价就更小;
  • 查询效率更稳定:因为B+树的数据都存储在叶子节点,不管查询那条数据,路径长度都是相同的,效率相同;
  • B+树叶子节点的数据用链表相连,是一个有序的链表,查询进行排序查询,遍历效率更高;

(2)不是子节点越多越好,好比果让数据节点只有三层的话,每一个非叶子节点上存储的指针就越多,这样不方便定位的。

12.Mysql的联合索引和聚簇索引

答:Mysql创建索引是为了提升查询性能,索引有不少种,包括聚簇索引、非聚簇索引、联合索引、覆盖索引等等,这些索引是创建在B+树上面的。

(1)聚簇索引(也叫汇集索引、主键索引):一个表只能有一个聚簇索引,并且创建在主键上面的,非叶子节点上只包含索引的列,叶子节点上包含所有数据,由于索引是有序的,在查询上效率是很高的经过oder by和groupby操做便可;叶子节点存放的是一行的一整行数据。

(2)非聚簇索引(也叫二级索引、辅助索引):一个表能够拥有多个非聚簇索引,非聚簇索引的叶子节点存放的不是一行全部数据,而是存储的是索引的字段值(主键列的值),因此在利用非聚簇索引作查询时一般会涉及回表操做,即根据非聚簇索引查询到的主键列,再进行聚簇索引的查询,才能查询到想要的数据;

(3)联合索引:一个索引包含多个字段,联合索引遵循最左前缀规则;

(4)覆盖索引:覆盖索引和联合索引有些类似,是指一个索引覆盖全部要查询的字段值,不需回表。所以覆盖索引的叶子节点须要包含全部索引列的数据值。它是针对特定的查询语句的。

13.hash算法的原理,hash冲突,怎么从原理上优化hash算法,将hash冲突降到最低,hash值是怎么算的,一致性hash

答:hash即散列表,是为了快速存取数据设计的,典型的”空间换时间“的作法,根据key-value的方式进行访问数据的经过key值讲数据映射到表中相应的位置。这个映射函数叫散列函数,存放数据的数组叫散列表。

(1)使用一个下表范围比较大的数组来存储元素。设计一个hash函数,使得每一个元素经过hash函数,获得一个相应存储位置。可是不能确保每一个元素与函数值一一对应的。会存在:根据同一hash函数计算出hash值,若是不相同,那么输入的值必定不一样,可是,若是hash值相同,输入的值也不必定相同,由于会存在hash碰撞问题。

(2)影响hash冲突的因素有:

  • 散列函数是否均匀:
  • 处理冲突的方法:开放地址法,链地址法,再hash法等等。
  • 散列表的填装因子:填装因子=填装数/散列表长度;填装因子越大,表示填入越多,发生冲突越大。

(3)下降冲的方法:根据引发冲突的因素来优化:计算has函数要尽量使元素均匀的分布;适当的进行扩容。提供一些处理冲突的方法。

(4)hash值得计算方式:原属hash& (length-1),length为数组得长度,一般为2的N次方,是为了可以上数据更均匀。

(5)

14.链表与数组的区别

答:

(1)链表:

  • 每一个节点在内存中能够存在任何地方,不要求连续,每一个节点存放着本身的数据和指向下一个节点的指针的顺序存贮结构;
  • 适用于对数据的增长和删除操做;
  • 对于查询并不适用,由于须要从头节点逐一进行查询;
  • 不用定义大小,能够任意扩容

(2)数组:

  • 数据在内存中存放在连续的空间中
  • 不适用于除了在数组最后的增长和删除,由于空间须要连续的,若是中间添加和删除,后面的数据位置都要作相应的改变
  • 适合随机的查询;
  • 须要定于大小,不利于扩展,当数组大小不够时,只能建立新的。

15.红黑树除了解决查询效率的问题还解决了什么问题,怎么将链表转成红黑树的

答:

(1)红黑树引入红黑节点的概念,使树保持一种非彻底平衡树,查询效率使稳定的;除此以外,红黑树的维护代价相对平衡二叉树来讲小不少,保证节点插入、删除旋转树次数更少,效率更高。

(2)链表转红黑树(在hashMap中)https://blog.csdn.net/chenssy/article/details/73749297

链表转换为红黑树过程就是一个红黑树增长节点的过程。在put过程当中,若是发现链表结构中的元素超过了默认值8,则会把链表转换为红黑树:

  • treeifyBin主要的功能就是把链表全部的节点Node转换为TreeNode节点;
  • 构建完成以后调用setTabAt()构建红黑树。
  • 在构建红黑树,向树中加节点时,经过balanceInsertion方法保证红黑树的特性的。

16.动态代理问题

17.二叉树的平衡算法

18.四种引用类型

(1)强引用:一般指的是Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器就不会回收引用的对象。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具备强引用的对象来解决内存不足的问题。若是一个强引用对象是全局的变量时,就须要在不用这个对象时赋值为null。

(2)软引用:用来描述一些还有用但非必需的对象。若是一个对象只具备软引用,则内存空间足够,垃圾回收器就不会回收它;若是内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就能够被程序使用。所以,这一点能够很好地用来解决OOM的问题,而且这个特性很适合用来实现缓存:好比网页缓存、图片缓存等。

String str=new String("abc"); // 强引用                                                                    SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用

软引用能够和一个引用队列(ReferenceQueue)联合使用,若是软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

例如:假若有一个应用须要读取大量的本地图片,若是每次读取图片都从硬盘读取,则会严重影响性能,可是若是所有加载到内存当中,又有可能形成内存溢出,此时使用软引用能够解决这个问题,用一个HashMap来保存图片的路径 和 相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

(3)弱引用:弱引用也是用来描述非必要对象的。弱引用与软引用的区别在于:只具备弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程当中,一旦发现了只具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。不过,因为垃圾回收器是一个优先级很低的线程,所以不必定会很快发现那些只具备弱引用的对象。

WeakReference<String> abcWeakRef = new WeakReference<String>(str); str=null;

若是这个对象是偶尔的使用,而且但愿在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。  

弱引用能够和一个引用队列(ReferenceQueue)联合使用,若是弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 

(4)虚引用:与其余几种引用都不一样,虚引用并不会决定对象的生命周期。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收器回收。

PhantomReference<User> phantomReference=new PhantomReference<User>(new User(),new ReferenceQueue<User>());

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

19.java里面为何要加锁

答:保证并发线程中的可见性和原子性和有序性

20.Java里面的各类锁

答:其实若是按照名称来讲,锁的分类为:公平锁/非公平锁、可重入锁/不可重入锁、独享锁/共享锁、互斥锁/读写锁、乐观锁/悲观锁、分段锁、偏向锁/轻量级锁/重量级锁、自旋锁:

(1)公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁。

Java中的ReentrantLock,能够经过构造器指定该锁是否公平,默认是非公平的,非公平锁的优点在于吞吐量比公平锁大。而Java中的synchronized,是一种非公平锁,它并不像ReentrantLock同样,经过AQS来实现线程调度,因此没有方法让它变成公平锁。

(2)可重入锁/不可重入锁

当一个线程得到当前实例的锁lock,而且进入了方法A,该线程在方法A没有释放该锁的时候,是否能够再次进入使用该锁的方法B?不可重入锁:在方法A释放锁以前,不能够再次进入方法B;可重入锁:在方法A释放该锁以前能够再次进入方法B。synchronized和ReentrantLock都是可重入锁。

(3)独享锁/共享锁

独享锁是指该锁一次只能被一个线程持有,而共享锁是指该锁一次能够被多个线程持有。synchronized毫无疑问是独享锁Lock类的实现ReentrantLock也是独享锁,而Lock类的另外一个实现ReadWriteLock,它的读锁是共享的(可让多个线程同时持有,提升读的效率),而它的写锁是独享的。ReadWriteLock的读写,写读,写写的过程是互斥的。
(4)互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

互斥锁在Java中的具体实现就是ReentrantLock,包括synchronized。
读写锁在Java中的具体实现就是ReadWriteLock。

(5)乐观锁/悲观锁

悲观锁认为对于同一个数据的并发操做,必定是会发生修改,哪怕没有修改,也会认为修改。所以对于同一个数据的并发操做,悲观锁采起加锁的形式。悲观的认为,不加锁的并发操做必定会出问题。
乐观锁则认为对于同一个数据的并发操做,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断从新的方式更新数据。乐观的认为,不加锁的并发操做是没有事情的。
(6)分段锁:从锁的设计来分的,细化锁的粒度,而不是一有操做就锁住整个对象。例如在ConcurrentHashMap中。

(7)偏向锁/轻量级锁/重量级锁:这三种锁是从锁的状态来划分的,并且是针对synchronized。

在Java 5经过引入锁升级的机制来实现高效Synchronized,这三种锁的状态是经过对象监视器在对象头中的字段来代表的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,下降获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,提升性能。
重量级锁是指当锁为轻量级锁的时候,另外一个线程虽然是自旋,但自旋不会一直持续下去,当自旋必定次数的时候,尚未获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能下降。重量级锁使除了拥有锁的线程之外的线程都阻塞,防止CPU空转。

(8)自旋锁:自旋锁是指尝试获取锁的线程不会当即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减小线程上下文切换的消耗,缺点是循环会消耗CPU。

PS:synchronized 支持不公平锁、可重入锁,但JDK 1.6之后引入了偏向锁、自旋锁和轻量级锁和重量级锁等

synchronized 原理:

对象的Mark World (对象头文件)会存储着对象的状态,主要是四种状态,无锁状态,偏向状态、轻量级状态和重量级状态

偏向锁获取过程:(01)

(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

(2)若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤(5),不然进入步骤(3)。

(3)若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行(5);若是竞争失败,执行(4)。

(4)若是CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码。

(5)执行同步代码。

轻量级锁的加锁过程:(00)

(1)在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。

(2)拷贝对象头中的Mark Word复制到锁记录中。

(3)拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤(4),不然执行步骤(5)。

(4)若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。

(5)若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了避免让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的加锁过程:(10)

每一个对象都有一个monitor监视器,调用monitorenter就是尝试获取这个对象,成功获取到了就将值+1,离开就将值减1。若是是线程重入,在将值+1,说明monitor对象是支持可重入的。

注意,若是synchronize在方法上,那就没有上面两个指令,取而代之的是有一个ACC_SYNCHRONIZED修饰,表示方法加锁了。它会在常量池中增长这个一个标识符,获取它的monitor,因此本质上是同样的。

synchronized 锁处理过程(我的通俗理解):给你们讲一个故事吧,在一个村庄,有口井,村里的每户人家都须要到这口井里面打水,但这口井,只能有一我的同时打水,这样有存在不少人来打水的并发线程问题啦。井就是锁,人就是线程,那么打水就是每一个线程要执行的任务呀。这个村长大人为了解决并发问题,提出了一套制度,就叫synchronized 准则吧。

准则的内容是:当一我的A去打水,若是这是,井口没人打水,他就能够直接打水了。但,若是此时,有人正在打水,那这我的就不能直接去打水,A只能站在打水等待的队伍的最后面。然而,因为synchronized不公平锁因此,若是A不是一个文平村民的话,他能够有一个办法,那就是来到井口旁边,若是此时,打水的人正好打完水,即释放了锁,而等待打水队伍的第一我的尚未反应过来,这样,A就能够趁机“插队”,抢占了井口,即,获取了锁,本身打水(这样感受对等待队伍中的人很不公平,可是没办法,synchronized 就是支持这样不公平的操做呀)。synchronized 是支持可重入锁,具体体如今,假如A在打水,此时,A的妻子来了,也要打水,那么A妻就不用去排队,就能够直接来井口旁边打水,这由于他们是一家人,就把他们视为一个对象了,一个对象只要没打完水,就能够一直占用井口。由于这个准则,其余的人在等待的队伍中也不能离开去作别的事情,就一直等着(线程阻塞)。

这样下来,若是在打水高峰期的话,分多人都在打水,就可能存在效率很低的问题了(由于你们只能排队,不能离开)。这样,细心的村长大人,就决定把这个准则改一改,就出现了增强版的synchronized 。新的准则支持偏向锁,就是,假如,村长里面人们都不喜欢在早上去打水,村长就把早上只有一我的打水的状态称为偏向状态,但因为A的家就住在井口旁,他喜欢早起一我的去打水,这样天天早上就只有A本身去打水(至关于单线程工做)。那么就须要那么A天天早上就在井口旁边的公告栏上面写下本身的名字,告诉别人,如今这个井被A占用了,当A打完以一次再去打第二次的时候,只须要看看公告栏上面是否仍是本身的名字,若是时,就直接打水,不须要获取锁和释放锁。可是,因为早上只有A打水,B就感受村里的井在早上只属于A,因此很嫉妒,B就决定他也要早上来打水。一旦有第二我的来打水,由于此时为偏向状态,通告栏上面写着A的名字,不会主动释放锁。因此B只能查看A是否已经打完其须要的全部水了,若是此时A没有打完水,那么就存在竞争打水的状况了,村长规定,一旦有多人打水,就能够把偏向状态改成轻量级状态,即,B就站在井口旁,一直问(处于自旋状态)A是否打彻底部的水了,可是是有时间要求的,若是在必定时间内,A打完全部水了。那么这口井就是空闲的了。就又从新回到只有一我的来打水的偏行状态了,那么B就能够把通告栏上的名字改为本身的名字,来实现这口井只能B来使用。但若是B一直问了必定时间,A仍是没有打完水,那么就将轻量级状态转为重量级状态,即,B再也不询问,而是来到旁边静静的等待A打完水,就有回到了在开始synchronized 的尊则了。简单的来讲就是,当只有一我的来打水,处于偏向状态,当处于偏向状态时,有同时第二我的也来打水,就变成了轻量级状态,第二我的在井口自旋询问超过必定时间了就变成了重量级状态了。这就是自旋锁、偏向锁、轻量级锁、重量级锁

 

ReentrantLock与synchronized 的区别

① 二者都是可重入锁

二者都是可重入锁。“可重入锁”概念是:本身能够再次获取本身的内部锁。好比一个线程得到了某个对象的锁,此时这个对象锁尚未释放,当其再次想要获取这个对象的锁的时候仍是能够获取的,若是不可锁重入的话,就会形成死锁。同一个线程每次获取锁,锁的计数器都自增1,因此要等到锁的计数器降低为0时才能释放锁。

② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面咱们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了不少优化,可是这些优化都是在虚拟机层面实现的,并无直接暴露给咱们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,须要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),因此咱们能够经过查看它的源代码,来看它是如何实现的。

③ ReentrantLock 比 synchronized 增长了一些高级功能

相比synchronized,ReentrantLock增长了一些高级功能。主要来讲主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁能够绑定多个条件)

  • ReentrantLock提供了一种可以中断等待锁的线程的机制,经过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程能够选择放弃等待,改成处理其余事情。
  • ReentrantLock能够指定是公平锁仍是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先得到锁。 ReentrantLock默认状况是非公平的,能够经过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是不是公平的。
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合能够实现等待/通知机制,ReentrantLock类固然也能够实现,可是须要借助于Condition接口与newCondition() 方法。Condition是JDK1.5以后才有的,它具备很好的灵活性,好比能够实现多路通知功能也就是在一个Lock对象中能够建立多个Condition实例(即对象监视器),线程对象能够注册在指定的Condition中,从而能够有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例能够实现“选择性通知” ,这个功能很是重要,并且是Condition接口默认提供的。而synchronized关键字就至关于整个Lock对象中只有一个Condition实例,全部的线程都注册在它一个身上。若是执行notifyAll()方法的话就会通知全部处于等待状态的线程这样会形成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的全部等待线程。

因此,咱们写同步的时候,优先考虑synchronized,若是有特殊须要,再进一步优化。ReentrantLock和Atomic若是用的很差,不只不能提升性能,还可能带来灾难。

ReentrantLock获取锁的方式:

  • lock(), 若是获取了锁当即返回,若是别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁;
  • tryLock(), 若是获取了锁当即返回true,若是别的线程正持有锁,当即返回false
  • tryLock(long timeout,TimeUnit unit), 若是获取了锁定当即返回true,若是别的线程正持有锁,会等待参数给定的时间,在等待的过程当中,若是获取了锁定,就返回true,若是等待超时,返回false;
  • lockInterruptibly:若是获取了锁定当即返回,若是没有获取锁定,当前线程处于休眠状态,直到获取锁定,或者当前线程被别的线程中断

(2)非阻塞同步:都是乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

乐观锁的实现方式:乐观锁通常会使用版本号机制或CAS算法实现。

21.锁的应用场景

答:

(1)偏向锁:

优势:加锁和解锁不须要额外的开销,和执行非同步方法相比仅存在纳秒级的差距。

缺点:若是线程间存在锁竞争,会带来额外的锁撤销的消耗

适用场景:适用于只有一个线程访问同步块场景

(2)轻量级锁:

优势:竞争的线程不会阻塞,提升了程序的响应速度。

缺点:若是始终得不到锁竞争的线程,使用自旋会消耗CPU

适用场景:追求响应时间,同步块执行速度很是快

(3)重量级锁:

优势:线程竞争不使用自旋,不会消耗CPU

缺点:线程阻塞,响应时间缓慢

适用场景:追求吞吐量。同步块执行速度较长

22.AQS原理与常见的同步器

答:AQS是AbstractQueuedSynchronizer的缩写

(1)AQS是JUCL下的一个类,它是用于构建锁和同步器的一个框架,使用AQS可以简单且高效的构造出应用普遍的大量同步器。如:ReentrantLock,Semaphore等等。

原理:若是被请求的共享资源为空闲状态,则将当前请求资源的线程设置为有效的工做线程,而且将资源设置为锁定状态,但若是被请求共享资源为锁定占用状态,那么就须要一套线程阻塞等待以及被唤醒时锁分配的机制。这个机制是AQS用CLH队列锁实现的,即将暂时获取不到锁的线程加到队列中去。

(2)AQS定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 。

(3)同步器就是资源共享方式

  • Semaphore(信号量)-能够控制多个线程同时访问:Semaphore主要用于控制当前活动线程数目,就如同停车场系统通常,而Semaphore则至关于看守的人,用于控制总共容许停车的停车位的个数,而对于每辆车来讲就如同一个线程,线程须要经过acquire()方法获取许可,而release()释放许可。若是许可数达到最大活动数,那么调用acquire()以后,便进入等待队列,等待已得到许可的线程释放许可,从而使得多线程可以合理的运行。
  • CountDownLatch (倒计时器)-容许一个或多个线程一直等待,直到其余线程的操做执行完后再执行。CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,以后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。应用场景:

①某一线程在开始运行前等待n个线程执行完毕。将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程须要等待多个组件加载完毕,以后再继续执行。

②实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。相似于赛跑,将多个线程放到起点,等待发令枪响,而后同时开跑。作法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 :new CountDownLatch(1) ,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

③死锁检测:一个很是方便的使用场景是,你能够使用n个线程访问共享资源,在每次测试阶段的线程数目是不一样的,并尝试产生死锁。

  • CyclicBarrier(循环栅栏)CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要作的事情是,让一组线程到达一个屏障(也能够叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,全部被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每一个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,而后当前线程被阻塞。

CyclicBarrier和CountDownLatch 区别:

  • ReentrantReadWriteLock(读写锁)-是一种特殊的自旋锁。在读写锁的世界里,访问共享资源的线程被划分为两类:一类是只对共享资源进行访问而不更改,暂且认为他是读者;一类是改变共享资源,即写操做,是写者。这个锁的核心要求是在没有任何线程得到读锁和写锁的状况才能得到写锁。

23.hashmap扩容

http://yikun.github.io/2015/04/01/Java-HashMap工做原理及实现/

答:Hashmap的扩容须要知足两个条件:当前数据存储的数量(即size())大小必须大于等于阈值;当前加入的数据是否发生了hash冲突。resize的过程:

  • 建立一个原来2倍的新数组;
  • 首先将原来数组转移到新的数组中。转移方法是:经过计算hash值,将hash值改变的加入到新的数组中,未发生改变的直接复制到原来数组相对于的位置上;
  • 设置hashmap扩容后为新的数组引用
  • 设置hashmap扩容新的阈值;

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

所以在扩容的时候,不须要从新计算hash值,而是判断新增bit位对于原hash是0仍是1(即判断原来第5位bit是0仍是1),若是是1,就不用变化,若是是1,就将原来的值加16

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

 这个设计确实很是的巧妙,既省去了从新计算hash值的时间,并且同时,因为新增的1bit是0仍是1能够认为是随机的,所以resize的过程,均匀的把以前的冲突的节点分散到新的bucket了。

24.JDK中消费者生产者应用场景

答:有三种实现方式

(1)synchronized和wait、notifyAll实现

多个线程put和take的时候,使用notify容易出现死锁:好比如今有1个take线程和1个put线程由于wait方法被要求进入等待队列,若是一个put线程正占据着锁,它用完以后notify,唤醒的若是是另外一个put线程,而这个put线程拿到锁后,发现put条件不知足,进入wait方法。由于此时没有线程在锁池中,因此锁就空了。等待队列中的take和put线程都没有人来唤醒它们,进入了死锁状态。当使用notifyAll的时候,全部等待池的线程都进入锁池,这样即便put线程由于wait方法进入了等待池,其余的线程还能够竞争锁,使得等待池有可能被唤醒。固然notifyAll并不能解决全部的死锁问题,只是比notify更不容易出错

(2)Lock锁和Condition实现

使用Lock和Condition的await() / signal()方法,Condition接口的await()和signal()就是其中用来作同步的两种方法,它们的功能基本上和Object的wait()/ nofity()相同,彻底能够取代它们,可是它们和新引入的锁定机制Lock直接挂钩,具备更大的灵活性。经过在Lock对象上调用newCondition()方法,将条件变量和一个锁对象进行绑定,进而控制并发程序访问竞争资源的安全。

(3)BlockingQueue实现

BlockingQueue就像是一个封装好的StockRoom对象,直接使用它的put和take方法就行。

PS:为何使用消息队列? 消息队列有什么优势和缺点? Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别,以及适合哪些场景?

https://studygolang.com/topics/8246#reply0

(1)为何使用消息队列(其实就是问问你消息队列都有哪些使用场景,而后你项目里具体是什么场景,说说你在这个场景里用消息队列是什么?)其实场景有不少,可是比较核心的有 3 个:解耦、异步、削峰

  • 解耦

场景:在这个场景中,A 系统跟其它各类乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,不少系统都须要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统若是挂了该咋办?要不要重发,要不要把消息存起来?

解决办法:若是使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪一个系统须要数据本身去 MQ 里面消费。若是新系统须要数据,直接从 MQ 里消费便可;若是某个系统不须要这条数据了,就取消对 MQ 消息的消费便可。这样下来,A 系统压根儿不须要去考虑要给谁发送数据,不须要维护这个代码,也不须要考虑人家是否调用成功、失败超时等状况。

案例:

订单系统和配送系统

 

  • 异步

场景:再来看一个场景,A 系统接收一个请求,须要在本身本地写库,还须要在 BCD 三个系统写库,本身本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感受搞个什么东西,慢死了慢死了。用户经过浏览器发起请求,等待个 1s,这几乎是不可接受的。(秒杀系统就是经过这个来实现的)

解决办法:若是使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感受上就是点个按钮,8ms 之后就直接返回了,真快!

  • 削峰

场景:天天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 ~ 13:00 ,每秒并发请求数量忽然会暴增到 5k+ 条。可是系统是直接基于 MySQL的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。通常的 MySQL,扛到每秒 2k 个请求就差很少了,若是每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,致使系统崩溃,用户也就无法再使用系统了。可是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操做,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。(外卖订餐系统就是这样)

解决办法:若是使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,由于 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过本身每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就致使在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。这个短暂的高峰期积压是 ok 的,由于高峰期过了以后,每秒钟就 50 个请求进 MQ,可是 A 系统依然会按照每秒 2k 个请求的速度在处理。因此说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。

(2)优缺点:

  • 优势上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。
  • 缺点也有不少的:

系统可用性下降: 系统引入的外部依赖越多,越容易挂掉(原本你就是 A 系统调用 BCD 三个系统的接口就行了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?全部要引入高可用的消息队列

系统复杂度提升:硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的状况?怎么保证消息传递的顺序性?

系统一致性问题:一致性问题 A 系统处理完了直接返回成功了,人都觉得你这个请求就成功了;可是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了

(3)区别

特性

ActiveMQ

RabbitMQ

RocketMQ

Kafka

单机吞吐量

万级,吞吐量比RocketMQ和Kafka要低了一个数量级

万级,吞吐量比RocketMQ和Kafka要低了一个数量级

10万级,RocketMQ也是能够支撑高吞吐的一种MQ

10万级别,这是kafka最大的优势,就是吞吐量高。

 

通常配合大数据类的系统来进行实时数据计算、日志采集等场景

topic数量对吞吐量的影响

 

 

topic能够达到几百,几千个的级别,吞吐量会有较小幅度的降低

 

这是RocketMQ的一大优点,在同等机器下,能够支撑大量的topic

topic从几十个到几百个的时候,吞吐量会大幅度降低

 

因此在同等机器下,kafka尽可能保证topic数量不要过多。若是要支撑大规模topic,须要增长更多的机器资源

时效性

ms级

微秒级,这是rabbitmq的一大特色,延迟是最低的

ms级

延迟在ms级之内

可用性

高,基于主从架构实现高可用性

高,基于主从架构实现高可用性

很是高,分布式架构

很是高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会致使不可用

消息可靠性

有较低的几率丢失数据

 

通过参数优化配置,能够作到0丢失

通过参数优化配置,消息能够作到0丢失

功能支持

MQ领域的功能极其完备

基于erlang开发,因此并发能力很强,性能极其好,延时很低

MQ功能较为完善,仍是分布式的,扩展性好

功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准

优劣势总结

很是成熟,功能强大,在业内大量的公司以及项目中都有应用

 

偶尔会有较低几率丢失消息

 

并且如今社区以及国内应用都愈来愈少,官方社区如今对ActiveMQ 5.x维护愈来愈少,几个月才发布一个版本

 

并且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用

 

erlang语言开发,性能极其好,延时很低;

 

吞吐量到万级,MQ功能比较完备

 

并且开源提供的管理界面很是棒,用起来很好用

 

社区相对比较活跃,几乎每月都发布几个版本分

 

在国内一些互联网公司近几年用rabbitmq也比较多一些

 

可是问题也是显而易见的,RabbitMQ确实吞吐量会低一些,这是由于他作的实现机制比较重。

 

并且erlang开发,国内有几个公司有实力作erlang源码级别的研究和定制?若是说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。

 

并且rabbitmq集群动态扩展会很麻烦,不过这个我以为还好。其实主要是erlang语言自己带来的问题。很难读源码,很难定制和掌控。

接口简单易用,并且毕竟在阿里大规模应用过,有阿里品牌保障

 

日处理消息上百亿之多,能够作到大规模吞吐,性能也很是好,分布式扩展也很方便,社区维护还能够,可靠性和可用性都是ok的,还能够支撑大规模的topic数量,支持复杂MQ业务场景

 

并且一个很大的优点在于,阿里出品都是java系的,咱们能够本身阅读源码,定制本身公司的MQ,能够掌控

 

社区活跃度相对较为通常,不过也还能够,文档相对来讲简单一些,而后接口这块不是按照标准JMS规范走的有些系统要迁移须要修改大量代码

 

还有就是阿里出台的技术,你得作好这个技术万一被抛弃,社区黄掉的风险,那若是大家公司有技术实力我以为用RocketMQ挺好的

kafka的特色其实很明显,就是仅仅提供较少的核心功能,可是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,并且分布式能够任意扩展

 

同时kafka最好是支撑较少的topic数量便可,保证其超高吞吐量

 

并且kafka惟一的一点劣势是有可能消息重复消费,那么对数据准确性会形成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响能够忽略

 

这个特性自然适合大数据实时计算以及日志收集

综上所述,各类对比以后,总结:

通常的业务系统要引入MQ,最先你们都用ActiveMQ,可是如今确实你们用的很少了,没通过大规模吞吐量场景的验证,社区也不是很活跃,因此你们仍是算了吧,不推荐用这个了;

后来你们开始用RabbitMQ,可是确实erlang语言阻止了大量的java工程师去深刻研究和掌控他,对公司而言,几乎处于不可控的状态,可是确实是开源的,比较稳定的支持,活跃度也高;

不过如今确实愈来愈多的公司,会去用RocketMQ,确实很不错,可是我提醒一下本身想好社区万一忽然黄掉的风险,对本身公司技术实力有绝对自信的,我推荐用RocketMQ,不然回去老老实实用RabbitMQ吧,人是活跃开源社区,绝对不会黄

因此中小型公司,技术实力较为通常,技术挑战不是特别高,用RabbitMQ是不错的选择;大型公司,基础架构研发实力较强,用RocketMQ是很好的选择。

若是是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,况且几乎是全世界这个领域的事实性规范。

(4)MQ存在各类隐患问题的解决方案:RabbitMQ为例

  • MQ挂掉了怎么办

采用高可用的性的镜像集群,MQ有三种搭建方式:单机的(通常不会用)、普通集群的(不能高可用)和镜像集群(高可用)

普通集群:在多台机器上启动多个 RabbitMQ 实例,每一个机器启动一个,每台机器上面的RabbitMQ 实例存储的只是quequ的元数据,只有一台机器上面存储的是元数据和实际数据,实际上若是链接到了另一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这样就多出大量的数据通讯,而其,若是太宕机了,数据就丢失了,即便作过持久化,也须要所有加载完才能再用,不能实现高可用性。

镜像集群:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不同的是,在镜像集群模式下,你建立的 queue,不管元数据仍是 queue 里的消息都会存在于多个实例上,就是说,每一个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的所有数据的意思。而后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。
 

样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 均可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息须要同步到全部机器上,致使网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就没有扩展性可言了,若是某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的全部数据,并无办法线性扩展你的 queue。你想,若是这个 queue 的数据量很大,大到这个机器上的容量没法容纳了,此时该怎么办呢?

Kafka的高可用:采用主从结构分散放在多个机器上的,每一个机器就放一部分数据。当主机宕机了,从机器会自动顶上。

写数据的时候,生产者就写 leader,而后 leader 将数据落地写本地磁盘,接着其余 follower 本身主动从 leader 来 pull 数据。一旦全部 follower 同步好数据了,就会发送 ack 给 leader,leader 收到全部 follower 的 ack 以后,就会返回写成功的消息给生产者。(固然,这只是其中一种模式,还能够适当调整这个行为)
消费的时候,只会从 leader 去读,可是只有当一个消息已经被全部 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

  • 消息丢失

数据丢失的状况:

(1)生产者弄丢了数据采用confirm 机制:每次写的消息都会分配一个惟一的 id,而后若是写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack 消息,告诉你说这个消息 ok 了。若是 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,你能够重试。并且你能够结合这个机制本身在内存里维护每一个消息 id 的状态,若是超过必定时间还没接收到这个消息的回调,那么你能够重发

(2)MQ弄丢了数据:开启 RabbitMQ 的持久化,就是消息写入以后会持久化到磁盘,哪怕是 RabbitMQ 本身挂了,恢复以后会自动读取以前存储的数据,通常数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,本身就挂了,可能致使少许数据丢失,可是这个几率较小。

(3)消费者弄丢了数据:RabbitMQ 若是丢失了数据,主要是由于你消费的时候,刚消费到,还没处理,结果进程挂了,好比重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。这个时候得用 RabbitMQ 提供的 ack 机制,简单来讲,就是你必须关闭 RabbitMQ 的自动 ack,能够经过一个 api 来调用就行,而后每次你本身代码里确保处理完的时候,再在程序里 ack 一把。这样的话,若是你还没处理完,不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。

  • 如何保证消息的顺序性:

一个 queue,多个 consumer。好比,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操做,把 data2 存入数据库,而后是 data1/data3。这不明显乱了。

解决:拆分多个 queue,每一个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 可是对应一个 consumer,而后这个 consumer 内部用内存队列作排队,而后分发给底层不一样的 worker 来处理。

  • 重复消费问题

形成消息重复的根本缘由是:网络不可达。只要经过网络交换数据,就没法避免这个问题。因此解决这个问题的办法就是绕过这个问题。那么问题就变成了:若是消费端收到两条同样的消息,应该怎样处理?

  1. 消费端处理消息的业务逻辑保持幂等性
  2. 保证每条消息都有惟一编号且保证消息处理成功与去重表的日志同时出现

第1条很好理解,只要保持幂等性,无论来多少条重复消息,最后处理的结果都同样。第2条原理就是利用一张日志表来记录已经处理成功的消息的ID,若是新到的消息ID已经在日志表中,那么就再也不处理这条消息。

第1条解决方案,很明显应该在消费端实现,不属于消息系统要实现的功能。第2条能够消息系统实现,也能够业务端实现。正常状况下出现重复消息的几率其实很小,若是由消息系统来实现的话,确定会对消息系统的吞吐量和高可用有影响,因此最好仍是由业务端本身处理消息重复的问题,这也是RocketMQ不解决消息重复的问题的缘由。

RocketMQ 不保证消息不重复,若是你的业务须要保证严格的不重复消息,须要你本身在业务端去重

  • 如何解决消息队列的延时以及过时失效问题?消息队列满了之后该怎么处理?有几百万消息持续积压几小时呢?

消息触发机制

(1)死循环方式读取队列(秒杀系统)

(2)定时任务:

(3)守护进程:

 

 

25.HashSet和TreeSet的区别

答:

(1)底层数据结构:HashSet的底层数据结构是基于哈希表存储的,根据hashCode()、equals()来区分重复数据;HashSet底层是基于红黑树,经过Comparable 来去除重复元素的;

(2)数据有序性:HashSet存储的数据是没有顺序的,与写入HashSet的顺序更不相同,HashSet因为基于红黑树存储,一定是有序的;

(3)存储数据的类型:HashSet存储的数据不能有相同的,可是能够有null,TreeSet不能相同也不能有null;

(4)添加删除的复杂度:HashSet添加删除的复杂度是O(1),HashSet是O(log(n));

26.枚举的构造方法是私有的仍是共有的,为何?

答:是私有的构造方法,是由于枚举类型是单例模式。即枚举类型会由JVM在加载的时候,实例化枚举对象,你在枚举类中定义了多少个就会实例化多少个,JVM为了保证每个枚举类元素的惟一实例,是不会容许外部进行new的,因此会把构造函数设计成private,防止用户生成实例,破坏惟一性

27.设计模式的原则

答:

(1)单一原则:简单的说就是一个类只完成一个任务,不要存在多个任务在一个类中,会使任务复杂,容易出错‘

(2)开闭原则:简单的说就是扩展支持开放,修改支持关闭,即,尽量在原来的基础上扩展而不是修改原来的功能;

(3)里氏代换原则:简单的说就子类能够扩展父类的功能,但不能改变父类原有的功能,即尽可能不要重写父类的方法。能够新的方法来完成新的功能;

(4)接口隔离原则:简单的说就是只继承须要的接口,不要继承不须要的;

(5)依赖倒转原则:简单的说就是要依赖接口,不要依赖细节;

(6)迪米特原则:简单的说就是要一个类尽量少的用其余的类,要作到解耦;

28.设计模式

答:

(1)建立型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

(2)结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

(3)行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

29.三个线程如何实现交替打印ABC

答:https://www.jianshu.com/p/f79fa5aafb44

(1)Synchronized

使用同步块和wait、notify的方法控制三个线程的执行次序。具体方法以下:从大的方向上来说,该问题为三线程间的同步唤醒操做,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必需要肯定唤醒、等待的顺序,因此每个线程必须同时持有两个对象锁,才能进行打印操做。一个对象锁是prev,就是前一个线程所对应的对象锁,其主要做用是保证当前线程必定是在前一个线程操做完成后(即前一个线程释放了其对应的对象锁)才开始执行还有一个锁就是自身对象锁。主要的思想就是,为了控制执行的顺序,必需要先持有prev锁(也就前一个线程要释放其自身对象锁),而后当前线程再申请本身对象锁,二者兼备时打印。以后首先调用self.notifyAll()唤醒下一个等待线程(注意notify不会当即释放对象锁,只有等到同步块代码执行完毕后才会释放),再调用prev.wait()当即释放prev对象锁,当前线程进入休眠,等待其余线程的notify操做再次唤醒
(2)Lock

经过ReentrantLock咱们能够很方便的进行显式的锁操做,即获取锁和释放锁,对于同一个对象锁而言,统一时刻只可能有一个线程拿到了这个锁,此时其余线程经过lock.lock()来获取对象锁时都会被阻塞,直到这个线程经过lock.unlock()操做释放这个锁后,其余线程才能拿到这个锁。

(3)Condition

与ReentrantLock搭配的通行方式是Condition,以下:
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
condition.await();//this.wait();
condition.signal();//this.notify();
condition.signalAll();//this.notifyAll();
Condition是被绑定到Lock上的,必须使用lock.newCondition()才能建立一个Condition。从上面的代码能够看出,Synchronized能实现的通讯方式,Condition均可以实现,功能相似的代码写在同一行中。这样解题思路就和第一种方法基本一致,只是采用的方法不一样。

29.进程间的通讯方式

http://www.javashuo.com/article/p-msgyixni-bs.html

(1)管道pipe:是一种半双工的通讯方式,数据只能单向流动,并且只能在具备亲缘关系的进程间使用。进程的亲缘关系一般是指父子进程关系。须要双方通讯时,须要创建起两个管道

(2)有名管道FIFO:也是半双工的通讯方式,可是它容许无亲缘关系进程间的通讯。

(3)消息队列:是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

(5)共享存储:是映射一段能被其余进程所访问的内存,这段共享内存由一个进程建立,但多个进程均可以访问。共享内存是最快的 IPC 方式,它是针对其余进程间通讯方式运行效率低而专门设计的。它每每与其余通讯机制,如信号两,配合使用,来实现进程间的同步和通讯。例如java的内存模型。

(6)信号量Semaphore:信号量是一个计数器,能够用来控制多个进程对共享资源的访问。它常做为一种锁机制,防止某进程正在访问共享资源时,其余进程也访问该资源。所以,主要做为进程间以及同一进程内不一样线程之间的同步手段。

(7)套接字Socket:套解口也是一种进程间通讯机制,与其余通讯机制不一样的是,它可用于不一样及其间的进程通讯。

(8)信号 ( sinal ) : 信号是一种比较复杂的通讯方式,用于通知接收进程某个事件已经发生。

30.Linux经常使用命令集合

(1)目录管理

ls、cd、pwd、mkdir、rmdir、tree

(2)文件管理:

touch、stat、file、rm、cp、mv、nano、vi、vim

(3)日期管理:

date、clock、hwclock、cal、ntpdate

(4)查看文本:

cat、tac、more、less、head、tail

31.