目录
4.sql语句的优化https://www.cnblogs.com/parryyang/p/5711537.html
7.项目的JVM调优细节https://zhuanlan.zhihu.com/p/58897189
8.TCP是如何处理的,三次握手,四次挥手,四次挥手时,如果发生丢包会发生什么
11.Mysql数据库为什么采用B+树,为了让树变得矮,子节点越多越好吗?
13.hash算法的原理,hash冲突,怎么从原理上优化hash算法,将hash冲突降到最低,hash值是怎么算的,一致性hash
15.红黑树除了解决查询效率的问题还解决了什么问题,怎么将链表转成红黑树的
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。
两者的对比:
- 是否支持行级锁 : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
- 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 支持ACID事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
- 是否支持外键: MyISAM不支持,而InnoDB支持。
- 是否支持MVCC :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在
READ COMMITTED
和REPEATABLE READ
两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。 - 是否支持全文索引: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语句的优化https://www.cnblogs.com/parryyang/p/5711537.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>());
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)执行同步代码。
(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扩容新的阈值;
因此在扩容的时候,不需要重新计算hash值,而是判断新增bit位对于原hash是0还是1(即判断原来第5位bit是0还是1),如果是1,就不用变化,如果是1,就将原来的值加16
这个设计确实非常的巧妙,既省去了重新计算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条原理就是利用一张日志表来记录已经处理成功的消息的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.进程间的通信方式
https://www.cnblogs.com/LUO77/p/5816326.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