我们登上并非我们所选择的舞台,演出并非我们所选择的剧本。 --爱比克泰德
数据库牛客八股文
https://www.nowcoder.com/discuss/635638
InnoDB:事务型数据库首选,支持事务ACID,支持行锁定和外键,用在需要高性能的大型数据库站点上。
MyISAM:不支持事务,较高的插入查询速度,在Web,数据仓储上最常用。
MEMORY:将表中的数据存到内存中。
https://blog.csdn.net/luoyang_java/article/details/92781164
https://blog.csdn.net/timonium/article/details/116792184
Innodb存储千万级别的数据,层数也是三层左右,大概只进行3次IO操作
这棵B+树的存放总行记录数=根节点指针数单个叶子记录的行数。 这里先计算叶子节点, B+树中的单个叶子节点的大小为16K,假设每一条目为1K, 那么 记录数即为16(16k/1K=16), 然后计算非叶子节点能够存放多少个指针, 假设主键ID为bigint类型, 那么 长度为8字节,而指针大小在InnoDB中是设置为6个字节,这样加起来一共是14个字节。 那么 通过页大小/(主键ID大小+指针大小),即16384/14=1170个指针, 所以 一颗高度为2的B+树能存放161170=18720条这样的记录。 根据这个原理就可以算出 一颗高度为3的B+树可以存放1611701170=21902400条记录。 所以 在InnoDB中B+树高度一般为2-3层,它就能满足千万级的数据存储
https://www.cnblogs.com/novalist/p/6410964.html
索引是一种高效获取数据的数据结构。
索引能够提高查询的效率,就像书的目录一样。用索引的目的也是为了提高数据库查询的效率,解决查询的问题。
使用索引后,磁盘块以树状结构保存,查询数据时大大降低磁盘块的访问数量。
Mysql存储的索引结构是B+树,用它的原因是因为它是平衡树,复杂度O(lgn)如果用二叉树的话,特殊情况会退化成链表
说到B+索引,就不得不提二叉查找树,平衡二叉树,B树这三种数据结构,B+树就是从这三个演化来的。
二叉查找树:左子节点的键值小于当前节点的键值,右子节点的键值大于当前节点的键值。在极端的情况下,可能会变的不平衡,树的高度太高转换成了链表,查询效率不稳定。所以我们引入平衡二叉树。
平衡二叉树:会通过调整来保证平衡性。查找效率更稳定,速度也更快了。平衡二叉树每个节点只能存储一个键值和数据。但是我们不想这样一条一条的从磁盘里读数据,如果一次读取磁盘就能获取很多数据,那么查找数据的时间会大幅降低。不然可以想象平衡二叉树的高度也会变得很高。所以我们要寻找单一节点可以存储多个键值和数据的平衡树,也就是B树了。
B树:每个节点可以存储多个键值和数据,并且每个节点拥有更多子节点,很大程度上降低了树的高度,读取磁盘的次数页会变少。
B+树:是对B树的进一步优化,和B树相比,B+树非叶子节点上不存储数据,仅存储键值,,这样每个节点可以存储更多的键值,也就对应更多子节点,树会进一步变得又矮又胖,所有数据都存储在叶子节点,而且数据是按照顺序排序的,很大程度上提高了范围查找效率,而且因为每次查询都是查询到叶子节点,使得查询变得稳定。
存储引擎:先写入undolog日志中,之后写入redolog 写入binlog 提交事务 redolog prepare-commit阶段
其实说索引是什么的时候就要说一下,优点无非就是提高系统的性能,提高查询的速度。缺点就是创建索引和维护索引需要耗费时间。索引需要占物理空间。对数据增删改的时候,索引也需要维护。
https://blog.csdn.net/u012260238/article/details/106327734
https://blog.csdn.net/winy_lm/article/details/49718193
主键索引:一张数据表有只能有一个主键,并且主键不能为null,不能重复。
唯一索引:唯一索引的属性列不能出现重复的数据,但是允许数据为NULL,一张表允许创建多个唯一索引。 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。也就是说,唯一索引可以保证数据记录的唯一性。
1、在经常需要搜索的列上创建索引,加快搜索速度
2、在经常用在连接的列上创建索引,这些列主要是一些外键,可以加快连接速度。
3、在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的。
4、在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间。
5、在经常需要WHERE子句中的列上面创建索引,加快条件的判断速度。
https://blog.csdn.net/DILIGENT203/article/details/101468475
https://www.cnblogs.com/tufujie/p/9413852.html
一旦 select 语句前加了 explain 关键字,那么 mysql server 在优化器完成执行计划生成后就会立即返回,不会调用引擎进行实际的执行。 通过执行 explain 命令可以看到标的读取顺序、读取操作的类型、使用的索引、扫描的行数等信息。
对表访问方式,表示MySQL在表中找到所需行的方式,又称“访问类型”。
常用的类型有: **ALL、index、range、 ref、eq_ref、const、system、**NULL(从左到右,性能从差到好)
ALL:Full Table Scan, MySQL将遍历全表以找到匹配的行
index: Full Index Scan,index与ALL区别为index类型只遍历索引树
range:只检索给定范围的行,使用一个索引来选择行
ref: 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
eq_ref: 类似ref,区别就在使用的索引是唯一索引,对于每个索引键值,表中只有一条记录匹配,简单来说,就是多表连接中使用primary key或者 unique key作为关联条件
const、system: 当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量,system是const类型的特例,当查询的表只有一行的情况下,使用system
NULL: MySQL在优化过程中分解语句,执行时甚至不用访问表或索引,例如从一个索引列里选取最小值可以通过单独索引查找完成。
这些情况不走索引:
1、 查询条件在索引列上使用函数操作,或者运算的情况 2、 查询条件字符串和数字之间的隐式转换 3、特殊修饰符 %%, Or 将不走索引 4、 索引优化器选择最优的索引
1、当索引字段不可以为null 时,只有使用is not null 返回的结果集中只包含索引字段时,才使用索引
2、当索引字段可以为空时,使用 is null 不影响覆盖索引,但是使用 is not null 只有完全返回索引字段时才会使用索引
什么时候索引会失效?
https://www.cnblogs.com/cheng21553516/p/11450765.html
https://blog.csdn.net/bless2015/article/details/84134361
https://blog.csdn.net/Day_and_Night_2017/article/details/117368728
https://blog.csdn.net/qq_35571554/article/details/82800463
文章很好
https://blog.csdn.net/qq_35571554/article/details/82800463
嵌套的子查询是很慢的。
大表左关联小表很慢,小表左关联大表,很快
1、数据库中设置Sql慢查询,开启慢查询,设置多少秒算慢查询
set global slow_query_log=on;打开慢查询日志
show status like ‘slow_queries’; 它会显示慢查询sql的数目,具体的sql就在上面的Log file日志中可以看到。
2、分析慢查询日志,explain 模拟优化器执行sql查询语句,分析sql慢查询语句,type看是All要注意,看是什么原因导致索引失效,最好高于range。最好system。key==null也要注意。
索引失效,看是否是like匹配第一个字符为%,导致索引失效,%不在第一个位置会起作用。
还有索引的最左匹配原则。
查询条件放到子查询中,子查询只查主键ID,然后使用子查询中确定的主键关联查询其他的属性字段;
select * from test a inner join (select id from test where val=4 limit 300000,5) b on a.id=b.id;
https://zhuanlan.zhihu.com/p/163658548
先查询出主键id值
select id,title from collect where id>=(select id from collect order by id limit 90000,1) limit 10;
原理:先查询出90000条数据对应的主键id的值,然后直接通过该id的值直接查询该id后面的数据。
“关延迟联” 如果这个表非常大,那么这个查询可以改写成如下的方式:
Select news.id, news.description from news inner join (select id from news order by title limit 50000,5) as myNew using(id);
这里的“关延迟联”将大大提升查询的效率,它让MySQL扫描尽可能少的页面,获取需要的记录后再根据关联列回原表查询需要的所有列。这个技术也可以用在优化关联查询中的limit。
https://blog.csdn.net/qq_35571554/article/details/82800463
in是先查子查询表,然后和主表做笛卡儿积,如果内表比较小,查询速度较快
exists先查主表,挨着和子表对比
in 和 exists的区别: 如果子查询得出的结果集记录较少,主查询中的表较大且又有索引时应该用in, 反之如果外层的主查询记录较少,子查询中的表大,又有索引时使用exists。其实我们区分in和exists主要是造成了驱动顺序的改变(这是性能变化的关键),如果是exists,那么以外层表为驱动表,先被访问,如果是IN,那么先执行子查询,所以我们会以驱动表的快速返回为目标,那么就会考虑到索引及结果集的关系了 ,另外IN时不对NULL进行处理。
in 是把外表和内表作hash 连接,而exists是对外表作loop循环,每次loop循环再对内表进行查询。一直以来认为exists比in效率高的说法是不准确的。
not in 和not exists
如果查询语句使用了not in 那么内外表都进行全表扫描,没有用到索引;而not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。
http://www.liuzk.com/410.html(关于B+树的讲解,很全面,包括聚集索引和非聚集索引)
主键索引也就是聚集索引。 辅助索引就是非聚集索引 Innodb引擎默认在主键上建立聚集索引。辅助索引,叶子节点只保存了行的键值和指向对应行的书签,一般指向的是聚集索引。
以主键作为B+树索引的键值而构建的B+树索引,我们称之为聚簇索引。
聚簇索引是将索引和整条记录存放在一起,找到索引就找到了记录
优点:可以把相关的数据保存在一起,减少磁盘IO操作,提高性能;数据访问更快,比非聚簇索引快。
缺点:如果数据放在内存中,就没优势了,插入速度严重依赖于插入顺序,按照主键的插入顺序加载到InnoDB表中速度最快,如果不是按照主键顺序加载数据。;更新聚集索引代价高;如果行比较稀疏,或者页分裂导致数据存储不连续的时候。
以主键以外列值作为键值构建的B+树索引,我们称之为非聚集索引。
两者的区别是非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚簇索引中进行查找,这个再根据聚簇索引查找数据的过程,我们称为回表。
数据即索引,索引即数据。
覆盖索引:
https://www.cnblogs.com/happyflyingpig/p/7662881.html
select 的数据列只用从索引中就能获得,不必从数据表中读取。换句话说查询列要被所使用的索引覆盖。
EXPLAIN 使用explain 放在sql的前面
如果type低于range的话,就需要优化sql了
https://blog.csdn.net/weixin_45310179/article/details/99591496
https://blog.csdn.net/qq_40792869/article/details/88991852 key如果为null代表没有使用任何索引
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
联合索引符合最左匹配原则,按照索引建的顺序,一个查询可以只使用索引中的一部分,但只能是最左侧部分。
Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。
https://blog.csdn.net/weixin_30828697/article/details/112560096
最左优先,以最左边为起点任何连续的索引都能匹配上。同时遇到范围查询就会停止匹配。
//不使用索引
explain select * from user2 where password = '1';
//使用索引
explain select * from user2 where username = '1' and password = '1';
//使用索引-即使是乱序
explain select * from user2 where password = '1' and username = '1';
失效情况:
- 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
- =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
为什么使用联合索引?
减少开销。
覆盖索引
效率高
https://www.cnblogs.com/butterfly100/p/9034281.html
分区和分表。减少数据库的负担,提高效率。
分表:一个大表按照一定规则分解成多张具有独立存储空间的实体表,称为子表,读写的时候,按照事先定义好的规则得到子表名,再操作。
当数据量超过1000W或100G以后以后(磁盘页计算)
垂直(纵向)切分和水平(横向)切分
垂直切分按照列进行
优点:业务清晰,业务层面解耦
缺点:依然没存在数据量过大问题,需要水平切分,分布式事务处理复杂,部分表无法join
水平切分
根据数值范围:优点:单表大小可控。缺点:性能不高,因为热点数据处理不好
根据数值取模模hash取模划分:分片比较均匀,扩容可能比较麻烦
分区:也是按规则分解表,不同在于分表将大表分解为若干个独立的实体表,而分区是将数据划分在多个位置存放,分区后,表面上还是一张表,但是数据分散到多个位置了。
首先MyISAM引擎是不支持事务的,InnoDB引擎才支持事务。
事务是单个逻辑单元执行的一系列操作。
事务是一组数据操作/执行单元,最小的工作单元,不可再分,就是这一组操作要么都成功,要么都失败。
说到事务就不得不提一下事务的四个特性了?
事务有四个特性:ACID
ACID原则
redo 物理,undo逻辑
1、 原子性(Atomicity) 要么都执行,要么都不执行,事务的所有操作要么全部提交成功,要么全部失败回滚。回滚可以用回滚日志(Undo log)来实现。回滚时反向执行这些操作。
2、一致性(Consistency) 事务前后的数据完整性要保证一致
数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。
由其他三大特征保证,程序代码业务上的一致性
3、隔离性(Isolation) 一个事务的执行,不能被其他事务干扰,一个事务内的操作对并发的其他事务是隔离的。
一个事务所做的修改在最终提交以前,对其它事务是不可见的。
MVCC来保证
4、持久性(Durability)事务提交 事务一旦提交则不可逆,被持久化到数据库中/事务一旦被提交,它对数据库中数据的改变是永久性的。redolog日志保证
redo log 用于数据的灾后重新提交不同
redo log 是以“块”为单位进行存储的,称之为“redo log block”,每个块的大小是 512 字节。 以块为单位存储的原因是他和磁盘扇区的大小是相同的,从而保证在异常情况发生时不会出现部分写入成功产生的脏数据
mysql修改数据的时候会在redolog中记录一份日志数据,就算数据没有保存成功,只要日志保存成功了,数据仍然不会丢失
说到事务的特性,就不得不说一下事务的隔离级别了?
1. ISOLATION_READ_UNCOMMITTED(读取未提交内容):会造成脏读,读取到未提交的数据
2. ISOLATION_READ_COMMITTED(读取提交内容):会遇到不可重复读,意味着我们在同一个事务中执行完全相同的select语句时,可能看到不一样的结果。
3. ISOLATION_REPEATABLE_READ(可重复读) :Mysql默认的事务隔离级别,这种隔离级别解决了脏读,不可重复读,但可能会出现幻读。原因是什么快照读啊,当前读的,行锁只能锁住行,即使把所有记录都上锁也阻止不了新插入的记录,解决的办法就是加入在两行记录间的空隙加上锁,这个锁称为间隙锁。
4. ISOLATION_SERIALIZABLE(可串行化):最高级别的隔离级别,防止了脏读,不可重复读,避免了幻读。
4种隔离级别解决的脏读、不可重复读、幻读、又是什么呢?
脏读: 一个事务正在对数据进行更新操作,但是更新还未提交,另一个事务这时也来操作这组数据,并且读取到了前一个事务还未提交的数据,而如果前一个事务因为操作失败进行了回滚,后一个事务读取到的就是错误的数据,造成了脏读。
不可重复读: 一个事务多次读取同一数据,在该事务还未结束时,另一个事务也对该数据进行了操作,而且在第一次事务两次读取之间,第二个事务对数据进行了更新,那么第一个事务前后两次读取到的数据就是不同的,这样就造成了不可重复读。
幻读: 第一个数据正在查询符合某一条件的数据,这时,另一个事务又插入了一条符合条件的数据,第一个事务在第二次查询符合同一条件的数据时,发现多了一条前一次查询时没有的数据,仿佛幻觉一样,这就是幻象读。
不可重复读指的是,在一个事务开启过程中,当前事务读取到了另一事务提交的修改。 幻读则指的是,在一个事务开启过程中,读取到另一个事务提交导致的数据条目的新增或删除。
当前读:
select...lock in share mode (共享读锁) select...for update update , delete , insert
当前读, 读取的是最新版本, 并且对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
快照读
单纯的select操作,不包括上述 select ... lock in share mode, select ... for update。
快照读的实现方式:undolog和多版本并发控制MVCC
https://blog.csdn.net/DILIGENT203/article/details/100751755
对于正常的 select 查询 innodb 实际上进行的是快照读,即通过判断读取到的行的 DB_TRX_ID 与 DB_ROLL_PTR 字段指向的 undo log 回溯到事务开启前或当前事务最后一次更新的数据版本,从而在这样的场景下避免了可重复读与幻读的问题。
insert 和 update 操作虽然是进行当前读,但 insert 与 update 操作后,该行的最新修改事务 ID 为当前事务 ID,因此读到的值仍然是当前事务所修改的数据,不会产生不可重复读的问题。 但如果当前事务更新到了其他事务新插入并提交了的数据,这就会造成该行数据的 DB_TRX_ID 被更新为当前事务 ID,此后即便进行快照读,依然会查出该行数据,产生幻读
MVCC(Multi-Version Concurrency Control,MVCC) 多版本并发控制,是MySQL的InnoDB引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无须使用MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用MVCC无法实现。
在MVCC中事务的修改操作(Delete insert update )会为数据行新增一个版本快照。
脏读和不可重复读最根本的原因是事务读取到其他事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC规定只能读取已经提交的快照。当然,一个事务可以读取自身未提交的快照,这不算是脏读。
MVCC 多版本并发控制。为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
- MVCC多版本并发控制是MySQL中基于乐观锁理论实现隔离级别的方式,用于读已提交和可重复读取隔离级别的实现。在MySQL中,会在表中每一条数据后面添加两个字段:最近修改该行数据的事务ID,指向该行(undolog表中)回滚段的指针。Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
https://www.cnblogs.com/wupeixuan/p/11734501.html这个作者的MySQL专栏写的不错
https://blog.csdn.net/leledsj/article/details/103955918
当有一条记录需要更新的时候,InnoDB引擎会先把记录写到redo log 里面,并更新内存。InnoDB会在适当的时候,将这个操作更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
redo 日志会把事务在执行过程中对数据库所做的修改都记录下来,在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。
redo 日志并不是直接写入磁盘的,而是先写入到缓冲区,我们把这个缓冲区叫做redo 日志缓冲区
redo log是循环写的 redo log 不是记录数据页更新之后的状态,而是记录这个页做了什么改动
缓冲区和磁盘之间的数据如何同步?
MySQL的配置文件提供了innodb_flush_log_at_trx_commit 参数,这个可以用来控制缓冲区和磁盘之间的数据如何同步。
MySQL两份日志:
redo log 和 binlog 区别:
1、redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
2、redo log 是物理日志,记录的是在某个数据页上做了什么修改;binlog 是逻辑日志,记录的是这个语句的原始逻辑。
3、redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。追加写是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
4、redo log和binlog的产生方式不同。redo log是在物理存储引擎层产生,而binlog是在MySQL数据库的Server层产生的,并且binlog不仅针对InnoDB存储引擎,MySQL数据库中的任何存储引擎对数据库的更改都会产生binlog。 5、redo log和binlog的记录形式不同。MySQL Server层产生的binlog记录的是一种逻辑日志,即通过SQL语句的方式来记录数据库的修改;而InnoDB层产生的redo log是一种物理格式日志,其记录的是对于磁盘中每一个数据页的修改。 6、redo log和binlog记录的时间点不同。binlog只是在事务提交完成后进行一次写入,而redo log则是在事务进行中不断地被写入,redo log并不是随着事务提交的顺序进行写入的,这也就是说在redo log 中针对一个事务会有多个不连续的记录日志。
1、undolog日志是用来回滚的,是逻辑日志
很好的问题
https://blog.csdn.net/huangjw_806/article/details/100927097
binlog和redolog都是在事务提交阶段记录的
那是先写binlog还是先写redolog?
写入的顺序对于数据库系统的持久性和主从复制会有影响吗?
那又是如何保证一致性的?
如果先写入redolog再写如binlog
假如在一个数为redo log 时候发生了crash ,主库根据redo log完成事务的重做,主库中有这个事务,但是这个事务并没有发生binlog,那此事务的数据修改不会同步到从库上,这样产生了主从不一致的错误
如果是先写binlog再写redo log
假设在binlog的时候crash崩溃了,redo log还没有持久化,或者说还没记录玩,没有记录commit log 在数据恢复后,从库根据binlog去恢复此数据的修改,但是由于事务没有完整提交redo log 主库恢复后会进行回滚事务,这样也导致了主从不一致的错误。
如何解决?
MySQL内部两阶段提交XA-2PC (two phase commit, 两阶段提交
prepare和commit阶段
第一阶段: InnoDB Prepare阶段。此时SQL已经成功执行,并生成事务ID(xid)信息及redo和undo的内存日志。此阶段InnoDB会写事务的redo log,但要注意的是,此时redo log只是记录了事务的所有操作日志,并没有记录提交(commit)日志,因此事务此时的状态为Prepare。此阶段对binlog不会有任何操作。 第二阶段:commit 阶段,这个阶段又分成两个步骤。第一步写binlog(先调用write()将binlog内存日志数据写入文件系统缓存,再调用fsync()将binlog文件系统缓存日志数据永久写入磁盘);第二步完成事务的提交(commit),此时在redo log中记录此事务的提交日志(增加commit 标签)。
可以看出,此过程中是先写redo log再写binlog的。但需要注意的是,在第一阶段并没有记录完整的redo log(不包含事务的commit标签),而是在第二阶段记录完binlog后再写入redo log的commit 标签。还要注意的是,在这个过程中是以第二阶段中binlog的写入与否作为事务是否成功提交的标志。
通过上述MySQL内部XA的两阶段提交就可以解决binlog和redo log的一致性问题。数据库在上述任何阶段crash,主从库都不会产生不一致的错误。
此时的崩溃恢复过程如下:
如果数据库在记录此事务的binlog之前和过程中发生crash。数据库在恢复后认为此事务并没有成功提交,则会回滚此事务的操作。与此同时,因为在binlog中也没有此事务的记录,所以从库也不会有此事务的数据修改。 如果数据库在记录此事务的binlog之后发生crash。此时,即使是redo log中还没有记录此事务的commit 标签,数据库在恢复后也会认为此事务提交成功(因为在上述两阶段过程中,binlog写入成功就认为事务成功提交了)。它会扫描最后一个binlog文件,并提取其中的事务ID(xid),InnoDB会将那些状态为Prepare的事务(redo log没有记录commit 标签)的xid和Binlog中提取的xid做比较,如果在Binlog中存在,则提交该事务,否则回滚该事务。这也就是说,binlog中记录的事务,在恢复时都会被认为是已提交事务,会在redo log中重新写入commit标志,并完成此事务的重做(主库中有此事务的数据修改)。与此同时,因为在binlog中已经有了此事务的记录,所有从库也会有此事务的数据修改。
- 先写redo log 直接提交,然后写 binlog,假设写完redo log 后,机器挂了,binlog日志没有被写入,那么机器重启后,这台机器会通过redo log恢复数据,但是这个时候bingog并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
- 先写binlog,然后写redo log,假设写完了binlog,机器异常重启了,由于没有redo log,本机是无法恢复这一条记录的,但是binlog又有记录,那么和上面同样的道理,就会产生数据不一致的情况。
如果采用redo log 两阶段提交的方式就不一样了,写完binglog后,然后再提交redo log就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设redo log 处于预提交状态,binglog也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于mysql的处理机制了,mysql的处理过程如下:
- 判断redo log 是否完整,如果判断是完整的,就立即提交。
- 如果redo log 只是预提交但不是commit状态,这个时候就会去判断binlog是否完整,如果完整就提交 redo log, 不完整就回滚事务。
这样就解决了数据一致性的问题。
https://zhuanlan.zhihu.com/p/183753774
XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(TM: Transaction Manager)和(局部)资源管理器(RM: Resource Manager)之间的接口。XA为了实现分布式事务,将事务的提交分成了两个阶段:也就是2PC (tow phase commit),XA协议就是通过将事务的提交分为两个阶段来实现分布式事务。
prepare 阶段:第一阶段,事务管理器向所有涉及到的数据库服务器发出prepare"准备提交"请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成"可以提交",然后把结果返回给事务管理器.
commit 阶段:事务管理器收到回应后进入第二阶段,如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会把"可以提交"的事务回撤。如果第一阶段中所有数据库都提交成功,那么事务管理器向数据库服务器发出"确认提交"请求,数据库服务器把事务的"可以提交"状态改为"提交完成"状态,然后返回应答。
主键是一种约束,而唯一索引是一种索引,索引是什么?上面也说了,索引是一种高效获取数据的数据结构,所以从根本上主键和唯一索引本质上是不同的。
唯一索引列允许空值,主键列不允许空值。一个表最多创建一个主键,但可以创建多个唯一索引。形象的比喻一下:主键相当于一本书的页码,索引相当于书的目录。
https://blog.csdn.net/qq_27327261/article/details/108724765
自增主键:
优点:
数据库自动编号,速度快,而且是增量增长,按顺序存放,对于检索非常有利; 数字型,占用空间小,易排序,在程序中传递也方便; 如果通过非系统增加记录时,可以不用指定该字段,不用担心主键重复问题。
缺点:因为自动增长,在手动要插入指定ID的记录时会显得麻烦,尤其是当系统与其它系统集成时,需要数据导入时,很难保证原系统的ID不发生主键冲突(前提是老系统也是数字型的)。特别是在新系统上线时,新旧系统并行存在,并且是异库异构的数据库的情况下,需要双向同步时,自增主键将是你的噩梦; 在系统集成或割接时,如果新旧系统主键不同是数字型就会导致修改主键数据类型,这也会导致其它有外键关联的表的修改,后果同样很严重; 若系统也是数字型的,在导入时,为了区分新老数据,可能想在老数据主键前统一加一个字符标识(例如“o”,old)来表示这是老数据,那么自动增长的数字型又面临一个挑战。
速度快,顺序存放,检索有利,存储占用空间小,但是手动插入指定ID的记录时会显得麻烦,并且系统集成的时候,会发生主键冲突,问题多
UUID
优点:出现数据拆分、合并存储的时候,能达到全局的唯一性
缺点:影响插入速度, 并且造成硬盘使用率低 uuid之间比较大小相对数字慢不少, 影响查询速度。 uuid占空间大, 如果你建的索引越多, 影响越严重
合并存储的时候能保证全局唯一性,但是uuid影响插入速度,影响查询速度,占用的空间大
B+树是多路查询树
B+树的内部节点并没有指向关键字具体信息的指针,内部节点比B树更小,容纳关键字数量也越多。B+树中间节点不保存数据,磁盘页可以容纳更多的节点元素,树更矮更胖。B+树每次查询都必须查询到叶子节点,所以B+树更稳定,但是并不会慢。范围查找来说,B+树只需要遍历叶子节点链表即可。B树需要重复的中序遍历。
悲观锁:很悲观。每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。
乐观锁:很乐观。每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据库中的数据时需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。(版本号version区别)
悲观锁:比较适合写入操作比较频繁的场景,
乐观锁:比较适合读取操作比较频繁的场景,
https://www.cnblogs.com/treasury/p/13251867.html
char的长度是不可变的,varchar是可变的,char存取速度快。但是付出了空间代价,varchar是空间效率为首位,char取数据的时候,要用trim去掉多余的空格
https://blog.csdn.net/DILIGENT203/article/details/100995440
按照锁定范围来分
-
全局锁 – 锁定整个 mysql 的全局执行 全部备份
-
表级锁 – 锁定单个表
-
行级锁 – 锁定单条或多条行记录
1、记录锁(行锁):锁定某行记录锁就是对某行进行加锁,防止该行被其他操作修改或删除。
2、间隙锁gap lock:锁定某个区间,间隙锁存在的目的是为了防止在事务执行过程中,另一个事务对间隙的插入,能够有效避免幻读的发生。在读已提交与读未提交隔离级别下,Innodb 会自动禁用间隙锁。
3、临键锁next-key lock:锁定左开右闭的一段区间,简单的来说,临键锁就是记录锁 + 间隙锁,也可以理解为特殊的间隙锁,他的区间是前开后闭的。
1、表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
2、行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
3、页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
按照工作原理分:
1、共享锁,读锁,
持有同一个共享锁的多个进程可以同时进入保护空间,这就是共享锁命名的来源,因为他们可以共享被锁定的资源,他通常在读取数据前加锁,以实现多个对数据的读取进程可以相互并发执行不被阻塞,因此也常被称为“读锁”。
2、排他锁,写锁,
排它锁与共享锁不同,一旦加了排它锁,其他任何加锁请求都会被阻塞,排它锁通常用于写数据前加锁,以便让各个写操作之间保持互斥,因此也被成为“写锁”。
共享/排它锁的使用场景
共享锁
- 确保某个事务查到最新的数据;
- 这个事务不需要对数据进行修改、删除等操作;
- 也不允许其它事务对数据进行修改、删除等操作;
- 其它事务也能确保查到最新的数据。
排它锁
- 确保某个事务查到最新的数据;
- 并且只有该事务能对数据进行修改、删除等操作。
https://blog.csdn.net/DILIGENT203/article/details/101119614
死锁:并发系统中不同线程出现对竞争资源的循环依赖并阻塞相互等待就会发生死锁。两个事务会分别阻塞等待另一个事务占用的排他锁,从而陷入死锁。
设置超时 设置锁等待超时是最为简单粗暴的办法,innodb 提供了加锁阻塞超时时间的设置:innodb_lock_wait_timeout。 默认值是 50,即一个加锁请求在等待 50 秒后会自动返回加锁失败。 但这样存在几个问题: 该配置项的单位是秒数,因此他的最小粒度是 1 秒,对于有些系统,1 秒的超时显然太长,而另一些系统中,1 秒的超时又显得太短,难以区分是正常的锁等待还是发生了死锁,从而可能造成误伤。
主动死锁检测 innodb 提供了主动死锁检测机制,innodb 在锁冲突发生时,会扫描持有该锁或在竞争该锁的事务,判断他们之间是否有可能产生死锁,一旦发现当前事务的等待会产生死锁,那么就会立即返回错误。 可以通过 innodb_deadlock_detect 设置为 on 或 off 来开启或关闭主动死锁检测机制,默认是开启状态。 看上去主动死锁检测 + 业务重试可以解决所有的死锁问题了,但是这同样存在一定的问题。 由于整个主动死锁检测过程需要循环遍历所有持有或等待锁的事务两两间的持有锁情况,所以这个过程的时间复杂度是 O(n^2),在高并发的场景下,例如有 1000 个并发的线程同时更新一行,虽然他们之间并不会产生死锁,但主动死锁检测却要进行 100 万次对比,最终造成 CPU 利用率的飙高。
拆分字段实现单条记录并发度的下降 上述主动死锁检测引起性能问题的原因主要是单条记录加锁的并发度过高,但通常,我们不能靠降低系统的并发度来避免问题的发生,但我们可以通过横向或纵向拆分数据库中的字段来实现对并发加锁的优化。 例如,对于单纯用于递增记录的字段,我们可以拆分成多个字段,每次随机选取某个字段进行递增的记录。 这样虽然可以有效降低单个字段上的并发度,但依赖于实际的业务,如果业务场景同时存在增减操作,那么拆分成多个字段必须要考虑是否会将某个字段减到负数等问题,在很大程度上提升了业务逻辑的复杂度。
###############################################
- 预先检测到死锁的循环依赖,并立即返回一个错误。
- 当查询的时间达到锁等待超时的设定后放弃锁请求。
死锁的四个必要条件:
**互斥条件:**进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 **请求保持:**当进程因请求资源而阻塞时,对已获得的资源保持不放。 **不可剥夺条件:**进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 **循环等待:**在发生死锁时,必然存在一个进程--资源的环形链。
索引叶子节点的next指针加锁
https://github.com/mio4/learn-java/blob/master/Note/interview.md
间隙锁【行锁的问题】
- 定义
- 当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但不存在的记录,叫作“间隙(GAP)”。
- InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。(Next-Key锁)
- 因为在Query执行过程中通过范围查找的话,会锁定整个范围内的所有索引键值,即使这个索引不存在。**间隙锁有一个比较致命的弱点,就是当锁定一个范围键值后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定值范围内的任何数据。**在某些场景下这个可能会对性能造成很大的危害。
- 就是会锁定一个范围。
- 例子
- 首先关闭MySQL的自动提交
- 然后在A会话中更新了某列的数据(必须要使用索引)比如从a到b
- 在B会话中插入该列的数据c(a < c < b),会造成阻塞(也就是另外一个会话加锁的提现)
了解一下吧
https://www.cnblogs.com/liuqingzheng/p/11080501.html
https://blog.csdn.net/SnailMann/article/details/94724197
MVCC 多版本并发控制。为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
- MVCC多版本并发控制是MySQL中基于乐观锁理论实现隔离级别的方式,用于读已提交和可重复读取隔离级别的实现。在MySQL中,会在表中每一条数据后面添加两个字段:最近修改该行数据的事务ID,指向该行(undolog表中)回滚段的指针。Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表。已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
- MVCC,**Multi-Version Concurrency Control,多版本并发控制。**MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。(乐观锁实现的一种机制)如果有人从数据库中读数据的同时,有另外的人写入数据,有可能读数据的人会看到『半写』或者不一致的数据。有很多种方法来解决这个问题,叫做并发控制方法。最简单的方法,通过加锁,让所有的读者等待写者工作完成,但是这样效率会很差。MVCC 使用了一种不同的手段,每个连接到数据库的读者,在某个瞬间看到的是数据库的一个快照,写者写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的读者来说是不可见的。**当一个 MVCC 数据库需要更一个一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。**这种方式允许读者读取在他读之前已经存在的数据,即使这些在读的过程中半路被别人修改、删除了,也对先前正在读的用户没有影响。这种多版本的方式避免了填充删除操作在内存和磁盘存储结构造成的空洞的开销,但是需要系统周期性整理(sweep through)以真实删除老的、过时的数据。对于面向文档的数据库(Document-oriented database,也即半结构化数据库)来说,这种方式允许系统将整个文档写到磁盘的一块连续区域上,当需要更新的时候,直接重写一个版本,而不是对文档的某些比特位、分片切除,或者维护一个链式的、非连续的数据库结构。
https://blog.csdn.net/zhangzhikai1/article/details/110522181
第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。 第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。 第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。
主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。
- binlog 线程 :负责将主服务器上的数据更改写入二进制日志(Binary log)中。
- I/O 线程 :负责从主服务器上读取二进制日志,并写入从服务器的中继日志(Relay log)。
- SQL 线程 :负责读取中继日志,解析出主服务器已经执行的数据更改并在从服务器中重放(Replay)。
https://blog.csdn.net/mrtwofly/article/details/53939400
1、数学函数 ABS()返回值 BIN()返回二进制 MOD(x,y) 取余
2、聚合函数(常用于GROUP BY 从句的SELECT查询中)
AVG()返回指定列的平均值
COUNT()返回指定列中非NULL值的个数
MIN()返回指定列的最小值
MAX()返回指定列的最大值
SUM()返回指定列的所有值之和
3、字符串函数
ASCLL() 返回字符ASCLL码值
CONCAT 连接成字符串
LENGTH 字符数
TRIM 去除字符串首部和尾部的所有空格
4、日期和时间函数
curdate()返回当前的日期
curtime()返回当前的时间
https://www.cnblogs.com/tusheng/articles/9389672.html
物理层:建立、维护、断开物理连接,电气特性规定
数据链路层:物理地址寻址,数据的成帧,流量控制、数据的检错,重发等
网络层:IP,ICMP(控制报文协议) 网络地址翻译成对应的物理地址 IP寻址 路由选择 ARP
传输层:TCP,UDP 对报文进行分组(发送时)、组装(接收时) 提供传输协议的选择
会话层:确定数据是否需要网络传递
表示层:数据提供表示
应用层:HTTP、TFTP、FTP、DNS 为用户提供服务,给用户一个操作界面 DHCP 动态主机配置协议
为操作系统或网络应用程序提供访问网络服务的接口 ,通过应用进程间的交互完成特定网络应用。应用层定义的是应用进程间通信和交互的规则。(HTTP,FTP,SMTP,RPC)
UDP:用户数据报协议,是无连接的,尽最大可能交付,没有拥塞控制,面向报文,支持一对一 一对多,多对一,和多对多的交互通信。
TCP:传输控制协议,是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流,每一条TCP连接只能是点对点(一对一)
- TCP面向连接,UDP是无连接的,即发送数据之前不需要建立连接。
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流,UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
- 每一条TCP连接只能是点到点的,UDP支持一对一,一对多,多对一和多对多的交互通信。
- TCP首部开销20字节,UDP的首部开销小,只有8个字节。
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
源端口
目的端口
序号
确认号
检验和
ACK确认
SYN同步
FIN终止
窗口
源端口、目的端口、长度、检验和
客户端发送FIN连接释放报文之后,服务器收到这个报文进入CLOSE-WAIT状态,这个状态是为了让服务器端发送还未发送完毕的数据,传送完毕后,服务器会发送FIN连接释放报文。
防止已失效的请求报文突然又传送到了服务端,因而产生错误。
一个连接请求并没有丢失,而是长时间滞留了,延误发送过去后,如果不采用三次握手,服务端收到后开启连接,但是客户端并没有发出连接请求,服务端只是对一个过期的请求进行了开启连接,这时候就导致了server的资源造成了很多浪费。
如果四次挥手中最后一个ACK丢了,那么服务端会再次发送一个FIN。如果没有TIME_WAIT的话,那么客户端直接关闭,有可能重新与别的服务器建立连接,此时用的还是原来的端口和IP,那么之前的服务端重新发送的FIN又发给了客户端,此时客户端就懵逼了,我才刚建立连接,怎么就要分手?,这个其实是TIME_WAIT的作用。
LAST-ACK 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后当客户端撤销相应的TCB后,才进入CLOSED状态。这么做两个理由:
等待2MSL的的两个理由
确保最后一个确认报文能够到达,如果B没收到A发送来的确认报文,那么就会重新发送连接释放请求报文,A等待一段时间就是处理这种情况的发生。
还有一个原因是让本连接持续时间内所产生的所有报文都从网络中消失,使下一次新的连接不会出现在旧的连接请求报文。
time-wait过多怎么办?
timewait快速回收和重用。快速回收:通过修改参数启用快速回收,此时timewait只有一个rto的时间。重用有两个条件,1.新连接的初始序列号比TW老连接的末序列号大。2.如果使用了时间戳,那么新到来的连接的时间戳比老连接的时间戳大。并且同一个ip和端口号的才能重用。
https://blog.csdn.net/zs18753479279/article/details/115588381
DNS:获取域名对应的ip。
TCP协议:建立tcp连接
IP协议:建立TCP协议时,需要发送数据,发送数据在网络层使用ip协议
OPSF协议:开放最短路径优先,是一种内部网关协议,路由选择使用OPSF协议:
ARP:地址解析协议,将ip地址转化为MAC地址
HTTP协议:使用HTTP协议访问网页。
总体分为以下几个过程:
1、DNS解析
2、TCP连接
3、发送HTTP请求
4、服务器处理请求并返回HTTP报文
5、浏览器渲染页面
6、连接结束
1 先查询web缓存器,如果有的话则直接显示 2 通过DNS域名解析服务解析IP地址,先从浏览器缓存中查询,如果没有则查询本地DNS服务器的缓存 3 通过TCP的三次握手建立连接,建立连接后,向服务器发送HTTP请求 4 服务器收到浏览器的请求后,进行处理并发送响应报文 5 浏览器收到服务器的响应报文后,如果可以,进行缓存 6 浏览器渲染页面并呈现给用户 7 四次挥手断开连接
ARP:地址解析协议,由IP地址得到MAC地址
HTTP是超文本传输协议,信息传输的时候是明文的,不安全的。HTTPS是具有安全性是ssl加密传输协议,比http安全。http的端口是80,https的端口是443。
https采用非对称加密算法,需要两个密钥,公开密钥和私有密钥。
A B发送消息,要把各自的公钥告诉对方,发送的时候用对方的公钥加密,对方解密的时候,用自己的私钥解密。
一个HTTPS请求实际上包含了两次HTTP传输,可以细分为8步。 1.客户端向服务器发起HTTPS请求,连接到服务器的443端口 2.服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。 3.服务器将自己的公钥发送给客户端。 4.客户端收到服务器端的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性,关于客户端如何验证数字证书的合法性,下文会进行说明。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,即客户端密钥,这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,至此,HTTPS中的第一次HTTP请求结束。 5.客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。 6.服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。 7.然后服务器将加密后的密文发送给客户端。 8.客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束,整个HTTPS传输完成。
https://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html
攻击者盗用了你的身份,以你的名义发送恶意请求
1、登陆受信任的网站A 并在本地生成Cookie
2、在不登出A的情况下,访问危险网站B
csrf防御
1、在客户端页面增加伪随机数
2、在表单里添加Hash值,以认证这确实是用户发送的请求,服务端进行验证
3、验证码,每次用户提交表单,都需要填写一个图片上的随机字符串
https://www.cnblogs.com/baizhanshi/p/8482612.html
TCP是传输层,而http是应用层, http是要基于TCP连接基础上的,简单的说,TCP就是单纯建立连接,不涉及任何我们需要请求的实际数据,简单的传输。http是用来收发数据,即实际应用上来的。
在前面客户端和应用服务器建立TCP连接之后,就需要用http协议来传送数据了,HTTP协议简单来说,还是请求,确认,连接。
总体就是C发送一个HTTP请求给S,S收到了这个http请求,然后返回给Chttp响应,然后C的中间件或者说浏览器把这些数据渲染成为了网页,展示在用户面前。
TCP是底层通讯协议,定义的是数据传输和连接方式的规范 HTTP是应用层协议,定义的是传输数据的内容的规范 HTTP协议中的数据是利用TCP协议传输的,所以支持HTTP也就一定支持TCP
HTTP支持的是www服务 而TCP/IP是协议 它是Internet国际互联网络的基础。TCP/IP是网络中使用的基本的通信协议。 TCP/IP实际上是一组协议,它包括上百个各种功能的协议,如:远程登录、文件传输和电子邮件等,而TCP协议和IP协议是保证数据完整传输的两个基本的重要协议。通常说TCP/IP是Internet协议族,而不单单是TCP和IP。
请求报文结构:
- 第一行是包含了请求方法,URL、协议版本;
- 接下来多行都是请求首部Header,每个首部都有一个首部名称,以及对应的值。(编码格式,语言,host,缓存这些)
- 一个空行用来分隔首部和内容主体Body
- 最后是请求的内容主体
响应报文结构:
- 第一行包含协议版本、状态码以及秒杀,200 OK表示请求成功了
- 接下来多行也就是首部内容
- 一个空行分隔首部和内容主体
- 最后是响应的内容主体
https://www.cnblogs.com/baizhanshi/p/8482612.html
Http是应用层协议,更靠近用户端,TCP是传输层协议,而Socket是从传输层上抽象出来的一个抽象层,本质是接口。
https://blog.csdn.net/min996358312/article/details/68969519
1、TCP连接与HTTP连接的区别?
HTTP是基于TCP的
2、TCP连接与Socket连接的区别?
Socket也是基于TCP的
3、HTTP连接与Socket连接的区别?
HTTP是短连接,Socket是长连接,HTTP连接服务端无法主动发送信息,Socket连接双方可以随时向另一方喊话。
用HTTP的情况:双方不需要时刻保持连接在线,比如客户端资源的获取,文件上传等。
用Socket的情况:大部分即时通讯应用(QQ、微信)聊天室
https://www.cnblogs.com/heluan/p/8620312.html
http1.0和http1.1的主要区别如下: 1、缓存处理:1.1添加更多的缓存控制策略(如:Entity tag,If-Match) 2、网络连接的优化:1.1支持断点续传 3、错误状态码的增多:1.1新增了24个错误状态响应码,丰富的错误码更加明确各个状态 4、Host头处理:支持Host头域,不在以IP为请求方标志 5、长连接:减少了建立和关闭连接的消耗和延迟。
http1.1和http2.0的主要区别: 1、新的传输格式:2.0使用二进制格式,1.0依然使用基于文本格式 2、多路复用:连接共享,不同的request可以使用同一个连接传输(最后根据每个request上的id号组合成正常的请求) 3、header压缩:由于1.X中header带有大量的信息,并且得重复传输,2.0使用encoder来减少需要传输的hearder大小 4、服务端推送:同google的SPDUY(1.0的一种升级)一样
https://www.cnblogs.com/skynet/archive/2010/12/11/1903347.html
HTTP协议中客户端发送一个小请求,服务器响应以所期望的信息(例如一个html文件或一副gif图像)。服务器通常在发送回所请求的数据之后就关闭连接。这样客户端读数据时会返回EOF(-1),就知道数据已经接收完全了
keep-alive模式
再使用长连接的时候,是怎么判断数据以及发送完的?
1、使用消息首部字段Conent-Length
2、当没有Conent-Length的时候使用Transfer-Encoding
https://blog.csdn.net/qq_41431406/article/details/97926927
如果网络出现拥塞,分组会丢失,此时发送方继续重传,导致网络拥塞程度更高,因此在出现拥塞的时候,我们要控制发送方的速率,和流量控制很像,但是出发点不同,流量控制是为了让接收方来得及接收,而拥塞控制是为了降低整个网络的拥塞程度
TCP通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复
慢开始:一开始向网络注入的报文段少
拥塞避免:并非指完全能够避免拥塞,而是指在拥塞避免阶段将拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞
快重传:使发送方尽快进行重传,而不是等超时重传计数器超时再重传
快恢复:发送方一旦收到3个重复确认,就知道现在只是丢失了个别的报文段。于是不启动慢开始算法,而是执行快恢复算法。
https://zhuanlan.zhihu.com/p/133307545
TCP有确认应答机制,这样的缺点是,数据包的往返时间如果很长的话,通信的效率就会越低,为解决这个问题,TCP引入了窗口这个概念,即使在往返时间较长的情况下,也不会降低网络通信的效率。有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发生数据的最大值。可以通过累计确认和累计应答。
流量控制是避免发送方的数据填满接收方的缓存,但是并不知道网络中发生了什么。
当网络出现拥堵时,如果继续发送大量数据包,会导致数据包时延,丢失,TCP会重传,进一步导致网络负担更重,导致更大的延迟,恶性循环。于是就有了拥塞控制。
拥塞控制是避免发送方的数据填满整个网络。
有个拥塞窗口cwnd的概念,它会根据网络的拥塞程度动态变化。
只要网络中没有出现拥塞,cwnd就会变大
如果网络中出现了拥塞,cwnd就会减小
只要发送方发生了超时重传,就会认为出现了拥塞 ,拥塞控制主要是4个算法:
1、慢启动:一开始一点点提高发送数据包的数量 当发送方每收到一个ACK,拥塞窗口cwnd的大小就会加1 但是慢启动算法发包的个数是指数性的增长,有一个慢启动门限ssthresh状态变量。
当cwnd<ssthresh 时 使用慢启动算法。
当cwnd>ssthresh 时 就会使用拥塞避免算法。
2、拥塞避免算法:每当收到一个ACK时,cwnd增加1/cwnd。拥塞窗口变成线性增长。
3、拥塞发生,快重传,接受方发现丢了一个中间包的时候,发送三次前一个包的ACK,于是发送端就会快速的重传,不必等待超时再重传。cwnd=cwnd/2; ssthresh=cwnd ,进入快恢复算法。
4、还能收到3个重复ACK,说明网络也不那么糟糕,拥塞窗口cwnd=ssthresh+3,重传丢失的数据包,恢复之前的状态,即再次进入拥塞避免状态。
如果说ARP协议是用来将IP地址转换为MAC地址,那么DNS协议则是用来将域名转换为IP地址(也可以将IP地址转换为相应的域名地址)。
域名解析(根据访问的域名,找到对应的ip地址)------>TCP三次握手------>建立TCP连接后发起http请求------>服务器响应http请求并传输数据------>浏览器解析并渲染呈现给用户------>TCP4次挥手。
这里可能要着重说一下TCP三次握手和四次挥手
- 浏览器根据域名解析IP地址(DNS),并查DNS缓存
- 浏览器与WEB服务器建立一个TCP连接
- 浏览器给WEB服务器发送一个HTTP请求(GET/POST):一个HTTP请求报文由请求行(request line)、请求头部(headers)、空行(blank line)和请求数据(request body)4个部分组成。
- 服务端响应HTTP响应报文,报文由状态行(status line)、相应头部(headers)、空行(blank line)和响应数据(response body)4个部分组成。
- 浏览器解析渲染
1 先查询web缓存器,如果有的话则直接显示 2 通过DNS域名解析服务解析IP地址,先从浏览器缓存中查询,如果没有则查询本地DNS服务器的缓存 3 通过TCP的三次握手建立连接,建立连接后,向服务器发送HTTP请求 4 服务器收到浏览器的请求后,进行处理并发送响应报文 5 浏览器收到服务器的响应报文后,如果可以,进行缓存 6 浏览器渲染页面并呈现给用户 7 四次挥手断开连接
这个问题是问长连接和短连接的。
关于长连接和短连接可以看这篇文章
简单的说,短连接就是客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。长连接就是客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接,它有个过期的时间。
http1.0中默认是短连接,1.1之后默认是长连接。
FTP:文件传输协议(File Transfer Protocol,FTP)是用于在网络上进行文件传输的一套标准协议,它工作在 OSI 模型的第七层, TCP 模型的第四层, 即应用层, 使用 TCP 传输而不是 UDP, 客户在和服务器建立连接前要经过一个“三次握手”的过程, 保证客户与服务器之间的连接是可靠的, 而且是面向连接, 为数据传输提供可靠保证。
get一般用于获取资源,而post用于传输实体主体。
get的参数出现在url上,而post的参数存储在实体主体中。
get方法是幂等的,执行多次请求,服务器的状态也是一样的。
post不是幂等的
- Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。
- Get传送的数据量较小,这主要是因为受URL长度限制;Post传送的数据量较大,一般被默认为不受限制。
- Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。
- Get执行效率却比Post方法好。Get是form提交的默认方法。
- GET产生一个TCP数据包;POST产生两个TCP数据包。(非必然,客户端可灵活决定)
Cookie和session都是一种会话机制。
存储位置不同,Cookie保存在客户端,Session保存在服务器,cookie保存在本地,相对不安全,session相对安全。存储容量不同,对于session并没有上限,cookie保存的数据<=4KB ,一个站点最多保存20个Cookie。
Cookie:
服务器发送到用户浏览器并保存在本地的一小块数据,会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。
Session:
session是存在server段内存进程中的。在客户端存储的时候,。一般是用sessionID,当客户端发送请求的时候,会带上这个sessionID,服务器接收之后会依据Session id找到对应的session。
session的实现方式:1、使用Cookie来保存2、使用URL附加信息的方式。3、隐藏域
https://blog.csdn.net/ai_shuyingzhixia/article/details/80778183
https://blog.csdn.net/weixin_40648117/article/details/78844100
表单隐藏域
url重写
这个问题就要讨论get请求和post请求的区别了。getpost请求之前都是TCP三次握手连接。
首先post请求更安全,不会在作为url的一部分,也不会被缓存。
post发送的数据更大一点,因为get有url的限制,post请求会比get请求慢,
为什么慢,因为post包含了更多的请求头,而且post在接收真正数据之前会先将请求头发送给服务器进行确认,然后才真正发送数据。
TCP使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那就重传这个报文段。
一个报文段从发送再到接收到确认所经过的时间称为往返时间RTT
https://mp.weixin.qq.com/s/doxVJZ1G6187B4AOXb0JlA
UDP是基于报文发送的,有指示长度,因此可以将不同的数据报文分开。
TCP是基于字节流的,在传输的过程中会出现沾包、拆包
正常收到两个数据包,没有发生拆包沾包现象
只收到一个数据包,但一个中包含了两个数据包,即发生了沾包
收到两个数据包,但是一个要么多出来,要么不完整,就是发生了拆包和沾包
为什么会发生?
要发送数据大于TCP缓冲区剩余空间大小,会发生拆包
要发送数据小于TCP缓冲区剩余空间大小,会发生沾包
- 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包
怎么解决?
由于TCP本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。
**消息定长:**发送端将每个数据包封装成固定长度
**设置消息边界:**服务端从网络流中按消息边界分离出消息内容
**将消息分为消息头和消息体:**在消息头中包含消息总长度
- 更复杂的应用层协议比如 Netty 中实现的一些协议都对粘包、拆包做了很好的处理。
2开头的代表 成功状态码 请求成功。200 OK 请求被正常处理 204 请求成功但是没有任何资源返回。
3开头表示重定向状态码需要执行某些特殊的处理以正确处理请求
301 Moved Permanently:资源的uri已更新,你也更新下你的书签引用吧。永久性重定向
302 Found:资源的URI已临时定位到其他位置了,姑且算你已经知道了这个情况了。临时性重定向
4开头代表客户端错误状态码 404 not found 服务器上没有请求的资源
403:Forbidden:不允许访问那个资源。(权限、未授权IP)请求被拒绝
5开头 代表服务端错误状态码 500 内部资源出错
500 :服务器发生错误
502:CG1应用程序超时
504:网关超时
session
cookie
token
select、poll、epoll都是IO多路复用的具体实现,select出现的最早,之后是poll,再是epoll。
select允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成IO操作
poll的功能与select类似,也是等待一组描述符中的一个成为就绪状态。
select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。
- select 会修改描述符,而 poll 不会;
- select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;
- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
- 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。
select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。只需要将描述符从进程缓存区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符,epoll仅适用于LinuxOS。epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。对多线程编程更友好。
客户端发送一个请求到服务器,服务器匹配servlet,这都和请求转发一样,servlet处理完之后调用sendRedirect()这个方法,设置HTTP相应报头中的Status为302、设置HTTP响应报头中的Location值为指定的URL,客户端接收这个响应,响应行告诉客户端你必须要再发送一个请求,去访问Location里的URL,客户端发送一个新的请求,去请求指定资源
JAVA虚拟机,引入Java语言虚拟机后,Java语言可以在不同平台上运行,不需要重新编译。只需生成字节码文件,就可以在JVM中运行。
1、程序计数器:
当前线程所执行的字节码的行号指示器,程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复。为了多个线程切换后,能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,是线程私有的内存,这个内存区域不会发生OutOfMemoryError.
2、Java虚拟机栈:
Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接、方法出口等信息 局部变量表中存储的是基本数据类型,对象的引用和returnAddress类型
线程私有,基本数据类型和对象的引用,申请的栈空间超过最大的规定容量就会发生OutOfMemoryError.
3、本地方法栈:
与虚拟机栈类似
调用本地方法。会发生OutOfMemoryError.
4、Java堆:
最大的一块内存区域,被所有线程共享,存放对象实例和数组,垃圾收集器管理的内存区域。会发生OutOfMemoryError.内存布局8:1:1
5、方法区:
各个线程共享的内存区域,存储已被虚拟机加载的类型的信息,常量,静态变量,即时编译器编译后的代码缓存。会发生OutOfMemoryError.
在JDK1.8采用元空间来实现
6、运行时常量池:
方法区的一部分,存放字面量和符号引用。会发生OutOfMemoryError.
回答技巧,要说明各个部分是干嘛的,会不会发生OutOfMemoryError,是线程私有还是线程共享的?只有程序计数器不会发生OutOfMemoryError.只有方法区和堆是被所有线程共享的。
http://www.cyc2018.xyz/Java/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.html#minor-gc-%E5%92%8C-full-gc
Minor GC 和Full GC
Minor GC:回收新生代,因为新生代对象存活时间很短,因此Minor GC会频繁执行,执行的速度一般也会比较快。
Full GC:回收老年代和新生代,老年代对象存活时间长,因此Full GC很少执行,执行速度会比Minor GC慢很多。
内存分配策略:
1、对象优先在Eden分配
2、大对象直接进入老年代
3、长期存活的对象进入老年代。
4、动态对象年龄判定
5、空间分配担保
对于Minor GC,其触发条件非常简单,当Eden空间满时,就将触发一次Minor GC。而Full GC则相对复杂。有以下条件。
1、调用System.gc():只是建议虚拟机执行Full GC。但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2、老年代空间不足:老年代空间不足的场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的Full GC,应当尽量不要创建过大的对象数组。除此之外还可以通过-Xmn虚拟机参数调大新生代的大小,让对象尽量在新生代多存活一段时间。
3、空间分配担保失败:使用复制算法的Minor GC需要老年代的内存空间做担保,如果担保失败会执行一次Full GC
这一切都来始于分代收集理论。
标记-复制算法
标志-清除算法
标志-整理算法
判断对象是否可以回收,引用计数法,可达性分析算法
Garbage First:它是一款面向服务器端应用的垃圾回收器,发布的初衷是为了替代掉CMS垃圾回收器。它的垃圾回收机制是面向整个堆,并将其划分为各个大小相等的Region,采用的算法是标记复制算法。它会维护一个优先级列表,根据我们设置的停顿时间来选择回收收益最大的Region进行垃圾回收,将存活的对象复制到空的Region中,通过设置停顿时间可以达到在吞吐量和响应速度上的协调,它还有一个Humongous区域,只要对象大小超过Region的一半,便直接放在这个区域中,它的执行过程为以下四个步骤(三停顿一并发)
1、初始标记:标记GC Roots直接关联的对象,需要Stop the world
2、并发标记:从GC Roots遍历能引用到的所有对象
3、最终标记:处理并发标记后的修正操作,需要Stop the world
4、筛选回收:对各个Rigion的回收价值进行排序,根据用户期望的停顿时间按计划回收,并将被回收的Region中存活的对象复制到空的 Region中,再清理掉旧的Region,需要Stop the world;
在虚拟机栈中引用的对象 在方法区中静态属性引用的对象 在方法区中常量引用的对象 在本地方法栈中引用的对象 Java虚拟机内部的引用(基本数据类型对应的Class对象,一些常驻的异常对象:NullPointException,OutOfMemoryError,还有系统类加载器) 被同步锁持有的对象 反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等(面试我从没有说过这一条,再往下问我我不知道该怎么解释)
老年代垃圾收集器
这个题我感觉就分别说一下CMS垃圾回收器和C1回收器就好了。
上面说过G1了,现在说下CMS:
CMS(Concurrent Mark Sweep 并行标记扫描:其实从名字就知道它用的标记-清除算法实现的):获得最短回收停顿为目标,更加关注服务器的响应速度,希望给用户更好的交互体验,采用的是标记清除算法,执行过程分为如下四步(两停顿两并发),会产生空间碎片,无法解决“浮动垃圾”。执行步骤:
1、初始标记:标记GC Roots直接关联的对象,需要Stop the world
2、并发标记:从GC Roots遍历能引用到的所有对象
3、重新标记:对并发标记阶段的标记进行修正,需要Stop the world
4、并发清除:与用户线程一起运行,执行垃圾回收。
ps:这个问题可有点难搞
**Serial:**面向年轻代的,单线程的的垃圾回收器,采用的是标记复制算法,在进行垃圾回收的时候,必须执行Stop the world
**ParNew:**实际上是Serial的多线程版本,同样是标记复制算法,也需要在垃圾回收的时候Stop the world
**Parallel Scavenge:**面向年轻代,也是多线程的,关注的是达到一个可控制的吞吐量,采用的是标记复制算法,也需要在垃圾回收的时候Stop the world
**Serial Old:**Serial的老年代版本,采用的是标记整理算法,执行垃圾回收需要Stop the world
**Parallel Old:**是Parallel Scavenge的老年代版本,支持多线程并行收集,采用标记整理算法,同样也是关注吞吐量
**CMS:**获取最短回收停顿为目标,更加关注服务器的响应速度,希望给用户更好交互体验,采用的是标记清除算法,执行过程分为如下四步(两停顿两并发),会产生空间碎片,无法解决“浮动垃圾” 1 初始标记:标记GC Roots直接关联的对象,需要Stop the world 2 并发标记:从GC Roots遍历能引用到的所有对象 3 重新标记:对并发标记阶段的标记进行修正,需要Stop the world 4 并发清除:与用户线程一起运行,执行垃圾回收
**Garbage First:**一个浪漫的名字,它是一款面向服务器端应用的垃圾回收器,发布的初衷是为了替代掉CMS垃圾回收器,它的垃圾回收机制是面向整个堆,并将其划分为各个大小相等的Region,采用的算法是标记复制算法,它会维护一个优先级列表,根据我们设置的停顿时间来选择回收收益最大的Region进行垃圾回收,将存活的对象复制到空的Region中,通过设置停顿时间可以达到在吞吐量和响应速度上的协调,它还有一个Humongous区域,只要对象大小超过Region的一半,便直接放在这个区域中,它的执行过程为以下四个步骤(三停顿一并发) 1 初始标记:标记GC Roots直接关联的对象,需要Stop the world 2 并发标记:从GC Roots遍历能引用到的所有对象 (前连个阶段和CMS基本一致???) 3 最终标记:处理并发标记后的修正操作,需要Stop the world 4 筛选回收:对各个Rigion的回收价值进行排序,根据用户期望的停顿时间按计划回收,并将被回收的Region中存活的对象复制到空的Region中,再清理掉旧的Region,需要Stop the world
Shenandoha和ZGC这个不大问。
加载---链接(验证、准备、解析)----初始化
加载:生成一个代表类的Class对象,作为方法区中该类各种数据的访问入口。
验证:确保包含的信息符号当前虚拟机的要求,不会危害虚拟机自身的安全。
准备:为类变量分配内存,设置初始值
解析:将常量池的符号引用替换为直接引用的过程。
初始化:执行Java程序代码。
https://blog.csdn.net/codeyanbao/article/details/82875064
类加载器的类别:
BootstrapClassLoader:启动类加载器
ExtClassLoader:扩展类加载器
AppClassLoader:应用程序类加载器
CustomClassLoader:用户自定义类加载器
加载一个类的时候,先从AppClassLoader加载器开始,检查是否加载过,没有加载过就向上拿到父类加载器,调用父类的loadClass方法,父类去加载,重复这个过程,直到没有父类加载器了,也就是从App->Ext->Bootstrap,到Bootstrap加载器后就开始检查自己是否能够加载,如果可以加载,就直接加载,如果不能加载就向下交给子类,一种到最底层,如果没有任何一个加载器可以加载,就抛出ClassNotFoundException。
这种机制的好处是为了安全,防止了危险代码的植入。保证核心的class不会被篡改。
如何打破双亲委派机制呢?
自己写一个类加载器,重写loadclass方法,重写findclass方法
8:1:1 1个Eden区和2个Survivor区(from 和to)使用eden和from
https://blog.csdn.net/luzhensmart/article/details/81369091
https://www.cnblogs.com/yishanchuan/p/13406144.html
https://www.cnblogs.com/chenyangyao/p/5296807.html
1、类加载
2、分配内存
3、初始化
4、设置对象头
5、执行init方法
1、JVM遇到new关键字时,会先去常量池中查看是否有该类的符号引用,如果没有,说明类还没有被加载,先执行类的加载(加载-连接(验证-准备-解析)-初始化)
2、加载完成之后,为对象在堆中分配内存
3、然后进行初始化内存,将分配的内存初始化为零值,不包括对象头
4、设置对象头
5、执行init方法,对属性赋值,执行构造方法
内存快照分析工具,MAT,Jprofiler
分析Dump内存文件,快速定位内存泄漏
获得堆中的数据,获得大的对象
//出现这种异常就保存dump文件
-Xms 1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError生成分析dump文件,定位内存溢出和内存泄漏 oom dump
-Xms 设置初始化内存分配大小 默认1/63
-Xmx 设置最大分配内存 默认1/4
-XX:+PrintGCDetails 打印GC垃圾回收信息
https://www.cnblogs.com/swisszhang/p/9892992.html
父类的静态代码块 子类的静态代码块 父类的普通代码块 父类的构造方法 子类的普通代码块 子类的构造方法
1、被static修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要new出一个类来
2、被static修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要new出一个类来
被static修饰的变量、被static修饰的方法统一属于类的静态资源,是类实例之间共享的
静态资源属于类,但是是独立于类存在的。从JVM的类加载机制的角度讲,静态资源是类初始化的时候加载的,而非静态资源是类new的时候加载的。
静态方法不能引用非静态资源。因为new的时候才产生的东西,类初始化后就存在的静态资源根本不认识。
静态方法当然可以引用静态资源了
非静态方法也可以引用静态资源
static静态块,静态块里面的代码只执行一次,且只在初始化类的时候执行
静态资源的加载顺序是严格按照静态资源的定义顺序来加载的
静态代码块对于定义在它之后的静态变量,可以赋值,但是不能访问
https://www.cnblogs.com/dolphin0520/p/3736238.html
https://blog.csdn.net/qq_42651904/article/details/87708198
https://www.zhihu.com/question/31345592
(第二篇博客总结了finally里面使用return的一些情况,值得一看)
final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)
当final修饰一个类时,表明这个类不能被继承,也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的所有成员方法都会被隐式地指定为final方法。
在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。
其中所有的方法都不能被重写(这里需要注意的是不能被重写,但是可以被重载,这里很多人会弄混),所以不能同时用abstract和final修饰类(abstract修饰的类是抽象类,抽象类是用于被子类继承的,和final起相反的作用)
当final修饰方法的时候,第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。
因此当想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。类的private方法会隐式的被指定为final方法
当final修饰变量时,对于基本变量,其数值一旦在初始化之后便不能更改,如果是引用类型的变量,则在其初始化之后便不能让再让其指向另一个对象。
被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。
static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变。
Finalize: Finalize是object类中的一个方法,子类可以重写finalize()方法实现对资源的回收。垃圾回收只负责回收内存,并不负责资源的回收,资源回收要由程序员完成,Java虚拟机在垃圾回收之前会先调用垃圾对象的finalize方法用于使对象释放资源(如关闭连接、关闭文件),之后才进行垃圾回收,这个方法一般不会显示的调用,在垃圾回收时垃圾回收器会主动调用。
跨平台性,write once run anywhere
垃圾回收机制
**封装:**将一个对象的属性私有化,行为公开化,同时提供对外的接口来访问对象,数据被保护在抽象数据类型的内部,尽可能的隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节。优点是减少了耦合。
继承: 继承是使用已存在的类作为基础创建新的子类,这个新的子类可以增加新的数据和方法,也可以使用父类的功能,但是不能选择性的继承,通过继承很方便的实现了代码的复用。
**多态:**多态分为编译时多态和运行时多态,编译时指的是重载,运行时指的是对象引用所指向的具体类型在运行期间才确定。一个变量到底指向哪个实例类,该引用变量发出的方法调用是由哪个类实现的,必须程序运行时才能确定,实现多态的方式是继承和接口。
重写发生在父类与子类之间,方法名相同,参数列表相同,返回值可以“变小”,抛出的异常可以“变小”,访问修饰符权限不能变小,发生在运行期 重载是在一个类中,方法名相同,参数列表不同(参数顺序不同也行),返回值和访问修饰符可以不同,发生在编译期
首先: Exception 和Error都是继承于Throwable 类,在Java中只有Throwable类型的实例才能被程序抛出(throw)或者捕获(catch),它是异常处理机制的基本类型
其次:
Exception 和Error它是体现java平台针对不同异常情况的分类。
Exception 是程序正常运行过程中可以预料到的意外情况,应该被捕获并进行处理。
Error正常情况下不大可能发生的情况,绝大部分Error都会导致程序状态的不正常,且不可恢复,既然非正常情况,所以我们不便也不需要进行处理,例如OutOFMemoryError之类的都是Error的子类。
再次: Exception 分为检查型异常和非检查型异常。检查型异常必须在源码处进行捕获处理,这是编译检查的一部分。除了RuntimeException以外全部都是检查型异常。
非检查型异常就是所谓的RuntimeException、类似NullPointerException和ArrayIndexOfBoundException就是我们的非检查型异常,通常可以编码避免的逻辑错误,具体可以根据需要进行捕获,编译时不检查,如果抛出非检查型异常就是代码逻辑问题,需要解决
public:可以被所有其他类所访问
protected:自身、子类及同一个包中类可以访问
default:同一包中的类可以访问,声明时没有加修饰符,认为是default。
private:只能被自己访问和修改
元注解 作用: 注解其他注解
四个标准的meta-annotation类型:
@Target
用于描述注解的使用范围(即: 被描述的注解可以用在什么地方)
@Retention
表示需要在什么级别保存该注释信息,用于描述注解的生命周期
(SOURCE < CLASS < RUNTIME) @Document
说明该注解将被包含在javadoc中
@Inherited
说明子类可以继承父类中的该注解
byte -128(-2^7)
127(2^7-1)
short -32768(-2^15)
32767(2^15-1)
int -2,147,483,648(-2^31)
2^31-1
long -2^63
2^63-1
double
float
boolean
char
1、接口的方法修饰符默认都是public,所有方法在接口中不能有实现(jdk7及以前,jdk8默认方法实现)
2、接口中除了 static、final变量,不能有其他变量,而抽象类中则不一定。
3、一个类可以实现多个接口,但只能继承一个抽象类。接口可以通过extends拓展多个接口
4、接口方法默认修饰符public 抽象方法可以public protected、default
5、抽象是对类的抽象,是一种模板设计;接口是对行为进行抽象,是一种行为规范
面向接口编程文章
https://www.cnblogs.com/leoo2sk/archive/2008/04/10/1146447.html
接口和抽象类,
接口实现多态,抽象类实现代码复用
抽象类是定义一些共性的东西,凡是这个类别的都会有的属性和方法 比如人就应该是一个抽象类,人都有身高属性,获得身高的方法也是一致的,如果定义为接口,那么男人和女人都要重写一遍获得身高的方法, 这个不论是从代码利用,还是OO的理解上来说,都应该是抽象类 还有你说的吃饭做为接口并不合适,因为吃饭是一个共性的动作,是人就会吃饭,这个吃的方法应该是作为抽象方法,因为每个人吃饭的方法可能不同,有人吞着吃,有人咬着吃等等,所以就要继承人这个类的子类来具体实现。
而接口是定义一些特性的规则,也就是比较特殊的东西,比如有的人会跳舞,有的人会游泳 那么就应该定义一个跳舞的接口,一个游泳的接口 如果男人实现了跳舞的接口,那么这个男人就会跳舞了 但是并不是每个男人,或者每个人都会跳舞
言简意赅: 行为的抽象,是规则,接口, (是特殊的个性) 事物的抽象,是共性,是抽象类(是一般的共性)
https://www.runoob.com/java/java-inheritance.html
Java继承(extends):就是子类继承父类的特征和行为,使得子类实例具有父类的具有相同的属性和行为。子类可以对父类进行扩展,可以用自己的方式去实现父类的方法。Java的继承是单继承,不支持多继承。
implements关键字,可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,同时可以继承多个接口
传基本类型传递过来的是一个值,在栈开辟一个空间,创建一个局部变量,不会对原有的值造成影响。
传引用类型的时候,传递过来的值是一个堆内存的地址。栈空间开辟一个空间创建对象,指向的是堆内存的地址,对它进行操作,实际上是对堆内存空间存放的原始数据操作,原始数据会发生变化
https://www.cnblogs.com/ysocean/p/8482979.html
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但部分组引用的对象。因此,原始对象及其副本引用同一个对象。
深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型还是引用类型,都复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
Object类提供的clone是只能实现浅拷贝的。
那么如何实现深拷贝呢?
1、让每个引用类型属性内部都重写clone()方法,既然引用类型不能实现深拷贝,那么我们将每个引用类型拆分为基本类型,分别进行浅拷贝。
2、利用序列化,每个需要序列化的类都实现Serializable接口。
==:1、基本数据类型,比较的是值,2、引用类型,比较的是内存地址是否相等
equals 如果没有重写,比较的是内存地址,重写的话,一般是比较对象字段的内容,就是按照重写的规则,比如String类就是重写了,String的equals是比较值是否相等。
HashCode相等,equals不一定相同,但是如果equals相同,hashcode一定相同。so easy
Java内存模型。
Java内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
https://blog.csdn.net/Appleyk/article/details/77879073
我们平时编写的文件都是.java文件,经过jdk里面的javac编译器编译后就变成了一堆.class文件,然后类加载器会将class文件加载到JVM虚拟机的内存中,此时会自动创建一个Class对象(由始至终只有一个)去存储这个类的相关信息(构造器,成员变量,方法等等)。我们可以利用这个Class对象去动态的创建对象和动态的调用对象的方法。
这部分我并没有放在Java语言基础中,是为了和一些Java的语法区分开,专门来讨论Java的集合。
List Set
ArrayList底层是数组,适合快速匹配,不适合频繁的增删,允许add null ,默认容量大小为10,会自动扩容,其中size() isEmpty() get() add()复杂度都是O(1) 它是线程不安全的,要实现线程安全,使用Collentions.synchronizedList(),或者Vector。synchronizedList和Vector的区别:add 的时候,两个的扩容方式不一样。SynchronizedList和Vector最主要的区别: 1.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。 2.使用SynchronizedList的时候,进行遍历时要手动进行同步处理。 3.SynchronizedList可以指定锁定的对象。
调用无参构造方法的时候,JDK1.8默认为空数组,数字大小10是第一次调用add方法时候,扩容的数组大小
add()方法
先确定数组大小是否足够,如果创建ArrayList的时候指定了大小,那么则以指定的大小创建一个数组,否则默认大小为10;容量足够大的情况,直接赋值,如果容量不够大,进行扩容方法grow(),扩容大小为原来大小的1.5倍。如果扩容后大小还不够的话,就将数组大小直接设置为我们需要的大小,扩容最大值是Integer.MAX_VALUE,之后会调用Arrays.copyOf()将原数组中的数组复制过来,。其中Arrays.copyOf()底层调用的是System.arrayCopy()。
remove() 方法
该方法被删除位置后的元素向前复制,底层调用的也是System.arrayCopy()方法,复制完成后,将数组元素的最后一个设置为null,解决了重复元素的问题。
语法糖(遍历)
迭代器和增强for循环一样,过程中会判断modCount的值是否符合循环过程中的期望。如果不符合的话。则会抛出并发修改异常,比如在增强for循环中进行删除操作。
底层数据结构是双向链表,每一个节点为Node,有pre和next属性。
适合增删,不适合快速匹配。提供从头添加,从尾添加,从头删除,从尾删除。
底层基于CAS + synchronized实现,所有操作都是线程安全的,允许多个线程同时进行put、remove等操作
底层数据结构:数组、链表和红黑树的基础上还添加了一个转移节点,在扩容时应用
table数组被volatile修饰
其中有一个比较重要的字段,sizeCtl = -1 时代表table正在初始化 table未初始化时,代表需要初始化的大小 table初始化完成,表示table的容量,默认为0.75table大小
put过程 key和value都是不能为空的,否则会产生空指针异常,之后会进入自旋(for循环自旋),如果当前数组为空,那么进行初始化操作,初始化完成后,计算出数组的位置,如果该位置没有值,采用CAS操作进行添加;如果当前位置是转移节点,那么会调用helptransfer方法协助扩容;如果当前位置有值,那么用synchronized加锁,锁住该位置,如果是链表的话,采用的是尾插发,如果是红黑树,则采用红黑树新增的方法,新增完成后需要判断是否需要扩容,大于sizeCtl的话,那么执行扩容操作
初始化过程 在进行初始化操作的时候,会将sizeCtl利用CAS操作设置为-1,CAS成功之后,还会判断数组是否完成初始化,有一个双重检测的过程 过程:进入自旋,如果sizeCtl < 0, 线程礼让(Thread.yield())等待初始化;否则CAS操作将sizeCtl设置为-1,再次检测是否完成了初始化,若没有则执行初始化操作
在JDK1.7采用的是Segment分段锁,默认并发度为16
在jdk1.7的时候,使用了分段锁。将数据分为多个 “段” segment,每个段使用单独的ReentrantLock分段锁。jdk1.8之后放弃了ReentrantLock(Ri en chun t Lock),重新使用了synchronized。
主要的原因:
加入多个分段锁浪费内存空间
生产环境中,map put时竞争同一个锁的概率非常小,分段会造成长时间等待。
数组+链表+红黑树
允许put空值,如果key为null,那么hash值为0.
底层数据结构:数组 + 链表 + 红黑树
允许put null 值,HashMap在调用hash算法时,如果key为null,那么hash值为0,这一点区别于HashTable和ConcurrentHashmap (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
loadFactor:负载因子默认为0.75,是均衡了时间和空间损耗计算出来的,较高的值会减少空间的开销,扩容减小,数组大小增加速度变慢,但是增加了查找的成本,hash冲突增加,链表变长
如果有很多需要储存到HashMap中的数据,要在一开始把它的容量设置为足够大,防止出现不断扩容
通过Collections.synchronizedMap()来实现线程安全或者使用ConcurrentHashmap
需要记住的字段如下
DEFAULT_INITIAL_CAPICITY = 1 << 4; 默认大小为16
MAXIMUM_CAPACITY = 1 << 30; 最大容量
DEFAULT_LOAD_FACTOR = 0.75f; 默认负载因子
TREEIFY_THRESHOLD = 8; UNTREEIFY_THRESHOLD = 6; 树化和退化为链表的阈值
MIN_TREEIFY_CAPACITY = 64; 链表转化为红黑树时需要的数组大小
threshold 表示扩容的阈值,大小为 数组大小*负载因子
put过程 首先会判断数组有没有进行初始化,没有的话,先执行初始化操作,resize()方法 (n - 1) & hash用来定位到数组中具体的位置,如果数组中的该位置为空,直接在该位置添加值 如果数组当前位置有值的话,如果是链表,采用的是尾插发,并且当链表长度大于等于8时,会进行树化操作;如果是红黑树的话,则会调用红黑树的插入值的方法;添加完成后,会判断size是否大于threshold,是否需要扩容,若扩容的话,数组大小为之前的2倍大小,扩容完成后,将原数组上的节点移动到新数组上。 一篇我觉得写得不错的博客儿:HashMap扩容时的rehash方法中(e.hash & oldCap) == 0算法推导
扩容
初始容量×负载因子 16*0.75=阈值。
一旦当前容量超过该阈值,就执行扩容。
当链表中的元素个数超过默认设定8个,数组大小超过64的时候,会将链表转化成红黑树。
使用一个容量更大的数组来代替已有的容量小的数组,扩容的大小是原来的2倍。transfer方法将原有Entry数组的元素拷贝到新的Entry数组里。jdk1.7采用的是头插法,1.8使用尾插法,并且不需要再向1.7那样重新计算hash,只需要看原来的hash值新增的bit是0还是1,是0的话索引不变,是1的话索引变成(原索引+oldCap)
为什么树化操作的阈值是8? 链表的查询时间复杂度为O(n),红黑树的查询时间复杂度为O(logn),在数据量不多的时候,使用链表比较快,只有当数据量比较大的时候,才会转化为红黑树,但是红黑树占用的空间大小是链表的2倍,考虑到时间和空间上的损耗,所以要设置边界值(其实链表长度为8的概率很低,在HashMap注释中写了,出现的概率不择千万分之一,红黑树只是为了在极端情况下来保证性能)
为什么还要有一个阈值是6? 避免频繁的进行树退化为链表的操作,因为退化也是有开销的,当我们移除一个红黑树上的值的时候,如果只有阈值8的话,那么它会直接退化,我们若再添加一个值,它有可能又需要变为红黑树了,添加阈值6相当于添加了一个缓冲
hash算法 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16),右移16位的操作使得hash值更加分散
为什么数组大小始终为2的n次幂? 因为在确定某个值在数组位置的下标时,采用的是(数组大小 - 1)位与上hash值,而数组大小减一之后,用2进制表示最后几位都是1,这样每位在位与运算之后,不是0就是1,如果我们hash值是均匀分布的话,那么我们得到的数组下标也是均匀分布的,而如果我们的数组容量不是2的n次幂,那么就没有这个特性了
数组大小为什么默认是16? 16是一个经验值,2,4,8有些小,会频繁的扩容,32有些大,这样就多占用了空间
为什么JDK1.8采用了尾插法? JDK1.7时采用的是头插法,它在扩容后rehash,会使得链表的顺序颠倒,引用关系发生了改变,那么在多线程的情况下,会出现链表成环而死循环的问题,而尾插法就不会有这样的问题,rehash后链表顺序不变,引用关系也不会发生改变,也就不会发生链表成环的问题
HashMap遍历方法:
1、foreach 遍历entryset
2、For-Each迭代keys和values
3、使用Iterator迭代
解决hash冲突的方法
https://www.cnblogs.com/lyfstorm/p/11044468.html
1、开放地址法:一旦发生了冲突,就去寻找下一个空的散列地址
冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字
2、再哈希法
rehash法,不同的hash函数
3、链地址法:
每个哈希表节点都有一个next指针,发生冲突通过next指针将节点连接起来。
4、建立公共溢出区
HashMap 红黑树排序的方式
如果key有实现comparable接口,那么就用compare方法,否则就按照类的名字排序
1、结点是红色或黑色
2、根节点是黑色
3、所有叶子节点是黑
4、每个红色结点的两个子结点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色结点)
5、从任一节点到每个叶子节点的所有简单路径上包含相同数量的黑色节点
HashMap和Hashtable的区别 实现方式不同:Hashtable:继承了Dictionary类,而HashMap继承的是AbstractMap类 初始容量不同:HashMap的初始容量为16,Hashtable为11,负载因子都是0.75 扩容机制不同:HashMap是翻2倍,Hashtable是翻两倍+1
https://tech.meituan.com/2016/06/24/java-hashmap.html
初始容量×负载因子 16*0.75=阈值。
一旦当前容量超过该阈值,就执行扩容。
当链表中的元素个数超过默认设定8个,数组大小超过64的时候,会将链表转化成红黑树。
使用一个容量更大的数组来代替已有的容量小的数组,扩容的大小是原来的2倍。transfer方法将原有Entry数组的元素拷贝到新的Entry数组里。jdk1.7采用的是头插法,1.8使用尾插法,并且不需要再向1.7那样重新计算hash,只需要看原来的hash值新增的bit是0还是1,是0的话索引不变,是1的话索引变成(原索引+oldCap)
文章还讲了HashTable和ConcurrentHashMap对比。
在了解关于HashMap的面试题之前,要先把HashMap的底层搞懂,源码、参数多读。
put(key,value):
先判断哈希桶数组是否初始化,如果没有初始化就先初始化哈希桶数组resize(),接下来会通过index=hash&(n-1)来计算对应的哈希桶数组的下标,然后判断对应下标同种是否存在节点(是否产生hash冲突),如果不存该节点,也就是没有hash冲突,就直接创建新的节点放入桶中,放入桶中后判断是否需要扩容,然后最后结束;如果存在冲突,就需要判断链表结构或者树结构中是否存在相同key的节点(equals()),如果存在相同的节点,将旧节点中的value覆盖后结束;如果不存在相同key的节点,就创建新的节点插入链表/树结构尾部,然后判断是否需要扩充,最后结束。
resize机制
HashMap的扩容机制就是重新申请一个容量是当前的2倍的桶数组,然后将原先的记录逐个重新映射到新的桶里面,然后将原先的桶逐个置为null使得引用失效。后面会讲到,HashMap之所以线程不安全,就是resize这里出的问题。
不安全的情况:
数据丢失
数据重复
多线程下put会出现数据不一致的问题。
HashMap get()的时候可能会因为resize()造成死循环的问题(jdk1.8中使用尾插法已经解决)
HashMap在1.7采用头插法会产生死循环,主要就是扩容后rehash链表的顺序会颠倒,1.8之后采用尾插法,rehash后链表的顺序不变,引用关系也不会发生改变。
HashSet是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的hashmap。封装了一个hashmap对象来存储所有的集合元素,所有放在hashset中的集合元素实际上由hashmap的key来保存。而hashset中的hashmap的value存储了一个PRESENT的静态object对象。
https://blog.csdn.net/u010647035/article/details/86375981
CAS+synchronized实现所有操作都是线程安全的,允许多个线程同时进行put、remove 底层数据结构:数组+链表+红黑树的基础上还增加了一个转移节点。
对于put操作,如果Key对应的数组元素为null,则通过CAS操作将其设置为当前值,如果key对应的数组元素不为null,则对该元素使用synchronized关键字申请锁,然后进行操作,如果该put操作使得当前链表长度超过一定阈值,则将该链表转换为树,从而提高寻址效率
对于读操作,数组是用volatile关键字修饰的,不用担心数组可见性问题。
spring中的bean初始化后存放在concurrenthashmap,线程安全
牛客
底层基于CAS + synchronized实现,所有操作都是线程安全的,允许多个线程同时进行put、remove等操作
底层数据结构:数组、链表和红黑树的基础上还添加了一个转移节点,在扩容时应用
table数组被volatile修饰
其中有一个比较重要的字段,sizeCtl = -1 时代表table正在初始化 table未初始化时,代表需要初始化的大小 table初始化完成,表示table的容量,默认为0.75table大小
put过程 key和value都是不能为空的,否则会产生空指针异常,之后会进入自旋(for循环自旋),如果当前数组为空,那么进行初始化操作,初始化完成后,计算出数组的位置,如果该位置没有值,采用CAS操作进行添加;如果当前位置是转移节点,那么会调用helptransfer方法协助扩容;如果当前位置有值,那么用synchronized加锁,锁住该位置,如果是链表的话,采用的是尾插发,如果是红黑树,则采用红黑树新增的方法,新增完成后需要判断是否需要扩容,大于sizeCtl的话,那么执行扩容操作
初始化过程 在进行初始化操作的时候,会将sizeCtl利用CAS操作设置为-1,CAS成功之后,还会判断数组是否完成初始化,有一个双重检测的过程 过程:进入自旋,如果sizeCtl < 0, 线程礼让(Thread.yield())等待初始化;否则CAS操作将sizeCtl设置为-1,再次检测是否完成了初始化,若没有则执行初始化操作
在JDK1.7采用的是Segment分段锁,默认并发度为16
https://my.oschina.net/pingpangkuangmo/blog/817973
https://zhuanlan.zhihu.com/p/116748080
https://www.wanaright.com/2018/09/30/java10-concurrenthashmap-no-segment-lock/
区别就是一个是线程安全的,一个是非线程安全的,然后面试官肯定就会问,那ConcurrentHashMap的如何保证线程安全的?
在jdk1.7的时候,使用了分段锁。将数据分为多个 “段” segment,每个段使用单独的ReentrantLock分段锁。jdk1.8之后放弃了ReentrantLock(Ri en chun t Lock),重新使用了synchronized。
主要的原因:
加入多个分段锁浪费内存空间
生产环境中,map put时竞争同一个锁的概率非常小,分段会造成长时间等待。
提高GC的效率
并发情况下rehash resize死循环
字典树,单词查找树,是hash树的变种,典型应用是用于统计,排序和保存大量的字符串,经常被搜索引擎系统用于文本词频统计。
用于处理一些不相交集合的合并及查询问题。
进程的组成部分,PCB(进程控制块) 程序段,数据段
1、动态性
2、并发性
3、独立性
4、异步性
https://blog.csdn.net/daaikuaichuan/article/details/82951084
进程是资源分配的基本单位,线程是CPU调度的基本单位。
进程控制块PCB描述进程的基本信息和运行状态,所谓的撤销进程都是指对PCB的操作。
进程包含线程,一条线程只能在一个进程中 进程与进程之间是独立的,而线程与线程之间资源可以是共享的
- 进程是操作系统资源分配的最小单位,线程是CPU任务调度的最小单位。一个进程可以包含多个线程,所以进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。
- 不同进程间数据很难共享,同一进程下不同线程间数据很易共享。
- 每个进程都有独立的代码和数据空间,进程要比线程消耗更多的计算机资源。线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉。
- 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
协程:比线程更轻量级的存在,协程不是被操作系统内核所管理,完全是由程序所控制(也就是在用户态执行)这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
协程的优势:
极高的执行效率:子程序切换不是线程切换,而是由程序自身控制,没有线程切换的开销
不需要多线程的锁机制
进程同步与进程通信很容易混淆:
进程同步:控制多个进程按一定顺序执行。
进程通信:进程间传输信息。
管道、消息队列、共享内存、信号量、Socket(套接字).
网络中进程通信,通过ip地址、协议、端口标识网络进程
这个文章写的很好,有很多基础的计算机网络知识。
https://www.cnblogs.com/yanggb/p/11179008.html
- 管道:速度慢,容量有限,只有父子进程能通讯。
- FIFO:任何进程间都能通讯,但速度慢。
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
- 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。
- 信号量(共享内存的进阶):不能传递复杂消息,只能用来同步。共享内存最大的问题就是多进程竞争内存的问题,也就是线程安全的问题。解决这个问题的方法就是信号量。
进程通讯方式
管道
僵尸线程
进程状态
信号量如何实现
JVM就是用得共享内存的方式
1、最佳置换算法,只具有理论意义的算法,,用来评价其他页面置换算法。置换策略是将当前页面中在未来最长时间内不会被访问的页置换出去。
2、先进先出FIFO置换算法:简单粗暴的一种置换算法,没有考虑页面访问频率信息。每次淘汰最早调入的页面。
3、最近最久未使用算法LRU:算法赋予每个页面一个访问字段,用来记录上次页面被访问到现在所经历的时间t,每次置换的时候把t值最大的页面置换出去(实现方面可以采用寄存器或者栈的方式实现)。
最少使用
4、时钟算法clock(也被称为是最近未使用算法NRU):页面设置一个访问位,并将页面链接为一个环形队列,页面被访问的时候访问位设置为1。页面置换的时候,如果当前指针所指页面访问为为0,那么置换,否则将其置为0,循环直到遇到一个访问为位0的页面。
5、改进型Clock算法:在Clock算法的基础上添加一个修改位,替换时根究访问位和修改位综合判断。优先替换访问位和修改位都是0的页面,其次是访问位为0修改位为1的页面。
6、LFU最少使用算法LFU:设置寄存器记录页面被访问次数,每次置换的时候置换当前访问次数最少的。
批处理系统:没有太多用户操作。调度算法的目标是保证吞吐量和周转时间。
1、FCFS 先来先服务:非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,不利于短作业。
2、SJF 短作业优先:非抢占式,运行时间最短顺序调度,长作业可能会饿死。
3、SRTN 最短剩余时间优先,短作业优先的抢占式版本,按照剩余运行时间的顺序进行调度。
交互系统:有大量的用户交互操作,在该系统中调度算法的目标是快速进行响应。
1、时间片轮转:按FCFS排成队列,依次分配时间片,时间片用完后,发出中断信息,程序送到队尾,继续分配CPU给队首的进程。
2、优先级调度:为每个进程分配一个优先级,按优先级调度。
3、多级反馈队列:设置多个队列,每个队列的时间片大小都不同。可以看成是时间片轮转调度和优先级调度算法的结合。
https://blog.csdn.net/21cnbao/article/details/108860584
https://blog.csdn.net/feixuedongji/article/details/79287891
当程序执行了系统调用或中断进入内核态时,进程切换线程就称为进程上下文,包含了一个进程所具有的全部信息,一般包括:进程控制块PCB、有关程序段和相应的数据集。
进程上下文切换两大步骤:1、地址空间切换和2、处理器状态切换(硬件上下文切换),前者保证了进程回到用户空间之后能访问到自己的指令和数据。后者保证了进程内核栈和执行流的切换。
进程控制块:PCB是操作系统管理控制进程运行所有的信息集合,主要包括进程描述信息,进程控制信息,资源分配清单和处理机相关信息等,是进程实体的一部分,进程存在的唯一标志。
最佳启动线程数=[任务执行时间/(任务执行时间-IO等待时间)]*CPU内核数超过这个数量
尽可能榨取cpu的计算能力
- 同步和异步关注的是消息通信机制,所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
- 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
p153
逻辑上实现对内存容量的扩充,并非是从物理上扩大内存容量。运行一个游戏,很大,不可能全部加载到内存中,程序运行时存在局部性现象(时间、空间) 没必要全部加载进内存中,只加载少数页面、段即可,程序运行过程中,如果它要访问的页面、段已经调入内存,就可以继续执行下去,如果不在的的话,就发出缺页中断,OS请求调页将他们调入内存中。
页面置换算法:
最佳置换算法(无法实现)、先入先出算法、LRU最近最久未使用算法。Clock置换算法(LRU改进算法)
虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多可用内存。
分页系统地址映射。
分页置换算法:
1、LRU:最近最久未使用
2、FIFO:先进先出 缺页率高
产生死锁中的竞争的资源指的是不可剥夺资源:例如打印机
多个线程互相抱着对方需要的资源,然后形成僵持。多个线程因为抢夺资源形成的僵局。
死锁的四个必要条件:
**互斥条件:**进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 **请求保持:**当进程因请求资源而阻塞时,对已获得的资源保持不放。 **不可剥夺条件:**进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 **循环等待:**在发生死锁时,必然存在一个进程--资源的环形链。
怎么解决死锁呢?
预防死锁——破坏死锁的四个必要条件
- 破坏“请求和保持”条件:让进程在申请资源时,一次性申请所有需要用到的资源,不要一次一次来申请,当申请的资源有一些没空,那就让线程等待。不过这个方法比较浪费资源,进程可能经常处于饥饿状态。还有一种方法是,要求进程在申请资源前,要释放自己拥有的资源。
- 破坏“不可抢占”条件:允许进程进行抢占,方法一:如果去抢资源,被拒绝,就释放自己的资源。方法二:操作系统允许抢,只要你优先级大,可以抢到。
- 破坏“循环等待”条件:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序提出(指定获取锁的顺序,顺序加锁)。
避免死锁——银行家算法(安全状态的资源分配)
检测死锁
解除死锁——剥夺资源、撤销进程
排除死锁问题 日志,堆栈信息
1、使用jps -l
定位进程号
2、使用jstack 进程号
找到死锁问题
在计算机系统中运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或者无意的破坏,为计算机设置了两个状态。
系统态(管态、核心态):操作系统在系统态运行,运行操作系统程序
用户态(目态):应用程序只能在用户态运行,运行用户程序。
在用户态运行的非特权指令不能对系统中硬件和软件直接访问,而在系统态的特权指令对内存空间的访问基本不受限制。
用户态切换到内核态的唯一途径------>中断/异常/陷入
内核态切换到用户态的途径------->设置程序状态字
https://blog.csdn.net/m0_38109046/article/details/89449305
BIO同步阻塞IO
NIO同步非阻塞IO
AIO异步非阻塞IO
传统BIO如果处理多个请求,就必须使用多线程
IO流是阻塞的,NIO流是不阻塞的。
Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
Java IO的各种流是阻塞的。这意味着,当一个线程调用 read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。
在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
3)Channel (通道)
NIO 通过Channel(通道) 进行读写。
通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
4)Selectors(选择器)
NIO有选择器,而IO没有。
选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
NIO 包含下面几个核心的组件:
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
同步与异步
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞
- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
判断等待------>业务------->通知
public class A {
public static void main(String[] args) {
Data data=new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//判断等待 业务 通知
class Data{
private int number=0;
//+1
public synchronized void increment() throws InterruptedException {
if(number!=0){
//等待
this.wait();
}
//业务
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
this.notifyAll();
}
//-1;
public synchronized void decrement() throws InterruptedException {
if(number==0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程 -1 完毕了
this.notifyAll();
}
}
注意:防止虚假唤醒问题,这里需要将if换成while
public class N个线程循环打印1_100 {
private int num;
private static final Object LOCK=new Object();
private int maxnum=10;
private void print(int targetNum){
while (true){
synchronized (LOCK){
while (num%3!=targetNum){
if(num>=maxnum){
break;
}
try {
LOCK.wait();
}catch (Exception e){
e.printStackTrace();
}
}
if(num>=maxnum){
break;
}
num++;
System.out.println(Thread.currentThread().getName()+": "+num);
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
N个线程循环打印1_100 test=new N个线程循环打印1_100();
new Thread(()->test.print(0),"thread1").start();
new Thread(()->test.print(1),"thread2").start();
new Thread(()->test.print(2),"thread3").start();
}
}
JUC Condition 交替打印ABC
public class 三个线程轮流打印ABC {
private int num;
// private static final Lock lock=new ReentrantLock();
private static final Object lock=new Object();
private void printABC(int targetNum){
for (int i = 0; i < 10; i++) {
synchronized (lock){
while (num%3!=targetNum){
try {
lock.wait();
}catch (Exception e){
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName());
lock.notifyAll();
}
}
}
public static void main(String[] args) {
三个线程轮流打印ABC test=new 三个线程轮流打印ABC();
new Thread(()->test.printABC(0),"A").start();
new Thread(()->test.printABC(1),"B").start();
new Thread(()->test.printABC(2),"C").start();
}
}
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A执行完调用B,B执行完调用C,C执行完调用A
*
*/
public class C {
public static void main(String[] args) {
Data3 data3=new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data3.printC();
}
},"C").start();
}
}
class Data3{//资源类
private Lock lock=new ReentrantLock();
private Condition condition1=lock.newCondition();
private Condition condition2=lock.newCondition();
private Condition condition3=lock.newCondition();
private int number = 1; //1A 2B 3C
public void printA(){
lock.lock();
try {
//业务,判断->执行 通知
while (number!=1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAAAA");
//唤醒,唤醒指定的人,B
number=2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
//业务,判断->执行 通知
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBB");
//唤醒,唤醒指定的人,C
number=3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
//业务,判断->执行 通知
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>CCCCCCCC");
//唤醒,唤醒指定的人,C
number=1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
五大基本数据类型:
String(字符串) List(列表) Set(集合) Hash(哈希) ZSet(有序集合)
三种特殊数据类型
geospatioal(地理位置) Hyperloglog(基数 不重复的元素,网址uv ) Bitmaps(位存储 0 1 两个状态)
https://blog.csdn.net/qq_26742855/article/details/105793947
String:
SDS简单动态字符串 编码int raw embstr
常数获得长度,二进制安全,兼容C字符串函数,杜绝缓冲区溢出。
减少修改字符串长度时所需重新分配内存次数
List:
编码ziplist linkedlist
ziplist压缩列表
linkedlist双端链表
Hash:
编码 ziplist hashtable
Set:
编码:intset hashtable
intset:整数集合,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,使用整数集合。
集合对象
ZSet:
编码:ziplist skiplist(跳表)
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头结点、表尾结点、长度)而zskiplistNode则用于表示跳跃表结点
https://zhuanlan.zhihu.com/p/200815425?ivk_sa=1024320u
跳表是基于链表的升级,使一个有序链表获得了高效增删改查,并始终维持有序的能力。
在原始链表的基础上,增加了一个索引链表,多层次的索引链表。
跳表是典型的用空间换时间
跳表在功能和性能上与红黑树相似,但代码实现远远比红黑树简单。
跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。平均查找和插入时间复杂度都是O(logn) ,空间复杂度 O(n*MaxLevel))快速查询是通过维护一个多层次的链表,且每一层链表中的元素是前一层链表元素的子集。一开始,算法在最稀疏的层次进行搜索,直至需要查找的元素在该层两个相邻元素中间,这时,算法将跳转到下一个层次,重复刚才的搜索,直到找到需要查找的元素为止。
https://blog.csdn.net/qq_34412579/article/details/101731935
按照范围区间查找元素,这个操作,红黑树没有跳表效率高。
跳跃表是有序集合的底层实现之一
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头结点、表尾结点、长度)而zskiplistNode则用于表示跳跃表结点
布隆过滤器:https://www.cnblogs.com/ysocean/p/12594982.html
缓存穿透:内存数据库和持久层数据库都没有,都查不到,解决办法:布隆过滤器 缓存空对象
缓存击穿:一个key非常热点,失效的瞬间,大量并发请求到持久层数据库,解决办法:设置热点数据永不过期,加互斥锁
缓存雪崩:某个时间段,缓存集中过期失效,或者Redis宕机,比如商品抢购同一时间集中放入缓存,解决办法:数据过期时间随机分布起来,设置不同的过期时间,让失效的时间点均匀。设置redis高可用,一个服务宕掉,其他还可以继续工作,也就是搭建集群。限流降级,某个key只允许一个线程查询数据和写缓存,其他线程等待。
这个答的时候,就把Redis的一些优点答一下。
redis是内存数据库,它的速度非常的快,而且还支持持久化的操作。在数据量特别大,也就是高并发的情况下,如果都去访问底层数据库的话,对数据库的压力特别的大。
另外Mysql有自己的内存引擎Memory,这个得知道,但是它不支持行锁只支持表锁,另外持久化也是问题。
NoSql的优点:解耦、数据之间没有关系,很好扩展,大数据量,高性能。数据类型多样,不需要事先设计数据库。
缺点:缓存和数据库一致性的问题,如果对数据完整性要求特别高,不适合用。缓存穿透,缓存击穿,缓存雪崩。
如果说问你了解redis吗?然后让你讲一下。
那就要从nosql和sql的区别,比较优缺点,redis五大数据类型、三种特殊的数据类型,事务、redis持久化(rdb、aop)发布订阅、主从复制(哨兵模式)redis会遇到的问题(缓存穿透、缓存击穿、和缓存雪崩)这些说一说了。如果记得住的话,redis.conf的配置文件常用的配置说一下就更好了。
https://www.cnblogs.com/ysocean/p/12422635.html
定时删除:设置key的过期时间的同时,创建一个定时器,在过期时间到来的时候,定时器立马对其进行删除操作。
优点:对内存最友好,保证内存中的key一旦到期就能立即从内存中删除。
缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分CPU时间,对服务器的响应时间和吞吐量造成影响。
惰性删除:设置key过期后,当需要key时,检查其是否过期,如果过期,就删除掉,反之返回该key。
优点:对CPU友好,我们只会在使用该键时才会进行定期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键会一直存在内存中,如果数据库中很多这种使用不到的过期 键,内存不释放的话,会造成内存泄漏。
定期删除:每隔一段时间,对一些key进行检查,删除里面过期的key
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响,定期删除也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。如果执行太频繁,变的和定时删除策略一样,对CPU不友好,如果执行太少,又和惰性删 除一样,过期键占用的内存不会及时释放。
另外最重要的是,在获取某个键时,如果某个键过期时间已经到了,但是没有执行定期删除,那么就会返回这个键的值,这个业务是不能忍受的错误。
Redis的过期删除策略是:惰性删除和定期删除两种策略配合使用
惰性删除:Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。
定期删除:由redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。
在redis.conf中,可以通过maxmemory <bytes>
来设定最大内存
当现有内存大于 maxmemory 时,便会触发redis主动淘汰内存方式,通过设置 maxmemory-policy ,有如下几种淘汰方式:
1)volatile-lru 利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used ) 。
2)allkeys-lru 利用LRU算法移除任何key (和上一个相比,删除的key包括设置过期时间和不设置过期时间的)。通常使用该方式。
3)volatile-random 移除设置过过期时间的随机key 。
4)allkeys-random 无差别的随机移除。
5)volatile-ttl 移除即将过期的key(minor TTL)
6)noeviction 不移除任何key,只是返回一个写错误 ,默认选项,一般不会选用。
Redis过期删除策略是采用惰性删除和定期删除这两种方式组合进行的,惰性删除能够保证过期的数据我们在获取时一定获取不到,而定期删除设置合适的频率,则可以保证无效的数据及时得到释放,而不会一直占用内存数据。
但是我们说Redis是部署在物理机上的,内存不可能无限扩充的,当内存达到我们设定的界限后,便自动触发Redis内存淘汰策略,而具体的策略方式要根据实际业务情况进行选取。
https://www.cnblogs.com/westboy/p/8696607.html
https://blog.csdn.net/qq_41489540/article/details/113744700
先更新数据库再删除缓存 读的时候先读缓存,缓存没有去读数据库,然后把数据更新到缓存。更新的时候先更新数据库,然后数据库更新成功之后再删除缓存。
先删除缓存再更新数据库会造成的问题? 比如两个并发,一个更新一个查询,更新删除缓存但数据库还没改,查询将旧的数据存到缓存中,导致缓存脏数据。
为什么是删除缓存不是更新缓存?因为缓存的数据可能不是来自一张表,是多张表的聚合,如果更新一个字段还要去关联很多的数据,操作非常耗时。
在设计方面要考虑内存数据库的性能,要高性能,高可用性,有容错能力,高可靠性,事务这些,安全性,可扩展,可维护性,同时还有易用性。
1、延时双删策略。先删除缓存再写数据库
在写库前后都进行redis.del()操作,并且设定合理的超时时间。
(1)先删除缓存
(2)再写数据库
(3)休眠500毫秒
(4)再次删除缓存
2、异步更新缓存
基于订阅binlog的同步机制
https://zhuanlan.zhihu.com/p/296484467
NIO
同步非阻塞IO
基于多路复用的高性能I/O模型
冒泡、快速,归并,堆。
public static void main(String[] args) {
int[] arr=new int[]{342,4,2,34,45,63,3,1,2,54,6,0};
System.out.println(Arrays.toString(arr));
maopaoSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void maopaoSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1])
{
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
public static void main(String[] args) {
int[] arr=new int[]{342,4,2,34,45,63,3,1,2,54,6,0};
System.out.println(Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int l,int r){
if(l>=r) return;
int left=l,right=r;
int pri=arr[left];
while (left<right)
{
while (left<right&&arr[right]>=pri)
{
right--;
}
if(left<right)
{
arr[left]=arr[right];
}
while (left<right&&arr[left]<=pri)
{
left++;
}
if(left<right)
{
arr[right]=arr[left];
}
if(left>=right)
{
arr[left]=pri;
}
}
quickSort(arr,l,right-1);
quickSort(arr,right+1,r);
}
//合并arr数组中下标为left到middle,和下标middle+1到right的两个部分
public static void Merge(int arr[],int left,int middle,int right){
int[] temp=new int[right-left+1];
for(int i=left;i<=right;i++){
temp[i-left]=arr[i];
}
int i=left,j=middle+1;
for (int k = left; k <=right ; k++) {
if(i>middle&&j<=right)
{
arr[k]=temp[j-left];j++;
}
else if(j>right&&i<=middle)
{
arr[k]=temp[i-left];i++;
}
else if(temp[i-left]>temp[j-left])
{
arr[k]=temp[j-left];j++;
}
else if(temp[i-left]<=temp[j-left])
{
arr[k]=temp[i-left];i++;
}
}
}
//对arr数组中下标为left的元素到下标为right的元素进行排序
public static void MergeSort(int arr[],int left,int right){
if(left>=right) return;
int middle=(right+left)/2;
MergeSort(arr,left,middle);
MergeSort(arr,middle+1,right);
Merge(arr,left,middle,right);
}
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
https://www.cnblogs.com/xiaokang01/p/12562127.html
方法一:构建堆。(找1000个最大的数,构建最小堆)找1000个最小的数构建最大堆
方法二:分治法
方法三:Hash去重
归并排序:将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序
归并排序是一种稳定、非原地的排序算法,时间复杂度O(nlogn),空间复杂度O(n).
快速排序是一种非稳定、原地排序算法,时间复杂度O(nlogn),空间复杂度O(1),极端情况下时间复杂度会退化到O(n^2)。
https://blog.csdn.net/Victor2code/article/details/107443547
首先将1GB的大文件分解为5个200MB的小文件,这里的小文件就是可以直接读进内存一次性排序。
然后直接对这5个小文件读进内存进行排序。
最后再利用归并的思路,通过5个游标分别在5个文件中逐行从左往右移动,将每次最小的一行读到新文件中,达到排序的效果
基本思想:将待排序的序列构建成大顶堆,整个序列的最大值就算堆顶的根节点,将其与末尾元素进行交换,此时末尾是最大值,然后将剩余n-1个元素重新构建成一个堆,如此反复。
步骤,构造初始堆。从左到右,从下到上
堆顶元素和末尾元素交换
重新调整结构,满足堆
堆排序是选择排序
O(nlgn)
二叉树: 最好O(lgn) 最差 O(n)
二叉搜索树:最好O(lgn) 最差O(n) 所有数据全部在一端
平衡二叉树:O(lgn)
红黑树:O(lgn)
稳定性:能否保证原始数据的相对次序,人家本来就是有序的,如果在排序过程中还进行了交换,就是非稳定的。
冒泡: 时间:O(n*n) 空间O(1) 稳定
选择排序: 时间:O(n*n) 不稳定
快速:时间O(nlogn) 最坏O(n2)空间O(logn) 非稳定
归并:时间O(nlogn) 空间O(n) 稳定
堆排序:时间O(nlogn) 空间O(1) 不稳定
手撕的算法一般都是剑指offer和leetcode的原题,一般都是简单和中等
2.翻转链表(字节,阿里)
3.最大连续子数组和(携程)
4.快排找第k大(阿里)
7.手撕LRU cachae(百度)
8.了解压缩算法吗(字节,当时被面自闭了,没想起哈夫曼编码,其他的压缩算法也不会)
LRU最近最少使用缓存机制
使用HashMap哈希表和双向链表实现
用一个哈希表和一个双向链表维护所有在缓存中的键值对
双向链表按照使用的顺序存储这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的
哈希表即为普通的哈希映射,通过缓存数据的键映射到其在双向链表中的位置。
https://www.zhihu.com/question/25536695
远程过程调用
两个不同的服务器上的应用,调用对方的函数方法,像本地函数一样的调用。
进程间通信
Netty封装Socket
它是一种软件架构风格,不是标准
https://zhuanlan.zhihu.com/p/97507715
同一个url可能是不同的实现。
一种资源可以有多种表现形式
springmvc中
@PathVariable:可以解析出来URL中的模板变量({id}/{name})
@RequestMapping:通过设置method属性的CRUD,可以将同一个URL映射到不同的HandlerMethod方法上。
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping注解同@RequestMapping注解的method属性设置。
https://blog.csdn.net/qq_19782019/article/details/80292110
https://www.cnblogs.com/htyj/p/8619198.html
https://blog.csdn.net/u010653908/article/details/53485688
Servlet
可以用来创建并返回一个包含基于客户请求性质的动态内容的完整的html页面
filter
要实现init()、doFilter()、destroy()三个方法,空实现也行。它不能产生一个请求和响应,它只是修改对某一资源的请求,或者响应,在启动服务器时候会加载过滤器实例调用init()方法来初始化实例;当每一次请求时都只调用方法doFilter()进行处理;停止服务器时调用destroy()方法,销毁实例。
Listener
servlet filter都是针对url之类的,而Listener是针对对象的操作。如session的创建session.setAttribute的发生,在这样的事情发生时做一些事情Servlet的Filter我们都需要配置url,而对于Listener则不需要。
web.xml 的加载顺序是:context- param -> listener -> filter -> servlet
servlet生命周期:init(),getServletConfig() service() getServletInfo() destroy(),先进行初始化,然后再提供服务,最后销毁。
servlet是java编写的服务器端程序,tomcat接收到客户端请求request,然后给对应的servlet,servlet进行处理生成响应内容,然后返回对应的response,然后再由tomcat返回给客户端。
servlet可以动态的生成web页面。它工作于客户端请求和服务器的中间层。
filter是一个可以复用的代码片段,可以用来转换请求,响应以及头信息,filter不能产生请求和响应,只能在请求到达servlet之前对请求进行修改,或者在请求返回客户端之前对响应进行处理。
主要是对请求到达servlet之前对请求和请求头信息进行前处理,和对数据返回客户端之前进行后处理
servlet的流程比较短,url来了之后就对其进行处理,处理完就返回数据或者转向另一个页面
filter的流程比较长,在一个filter处理之后还可以转向另一个filter进行处理,然后再交给servlet,但是servlet处理之后不能向下传递了。
filter可用来进行字符编码的过滤,检测用户是否登陆的过滤,禁止页面缓存等
https://blog.csdn.net/active_pig/article/details/105613944
1、@Controller 控制层使用,标识该类是Spring MVC Controller处理器,用来创建处理http请求的对象,
2、@Service 在业务逻辑层使用,标注业务层组件
3、@Repository ,用于标注数据访问组件,即DAO组件。
4、@Component 泛指组件,当组件不好归类的审核,可以使用这个注解进行标注
5、@Autowired 把配置好的bean拿来用,完成属性、方法的组装、它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作
6、@Resource 相当于@Autowired,一个byname,一个bytype
7、@Bean 标注一个Bean对象,交给Spring的容器管理。
8、@Configuration 声明当前类为配置类
当发起请求时被前置的控制器拦截到请求,根据请求参数生成代理请求,找到请求对应的实际控制器,控制器处理请求,创建数据模型,访问数据库,将模型响应给中心控制器,控制器使用模型与视图渲染视图结果,将结果返回给中心控制器,再将结果返回给请求者。
上图为SpringMVC的一个较完整的流程图,实线表示SpringMVC框架提供的技术,不需要开发者实现,虚线表示需要开发者实现。
简要分析流程:
1、DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。
2、HandlerMapping为处理器映射。DispatcherServlet调用。
HandlerMapping,HandlerMapping根据请求url查找Handler。
3、HandlerExecution表示具体的Handler,其主要作用是根据请求url查找控制器。如hello
4、HandlerExecution将解析后的信息传递给DispatcherServlet,如解析控制器映射等。
5、HandlerAdapter表示处理器适配器,其按照特定的规则去执行Handler
6、Handler让具体的Controller执行。
7、Controller将具体的执行信息返回给HandlerAdapter,如ModelAndView.
8、HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet
9、DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。
10、视图解析器将解析的逻辑视图名传给DispatcherServlet。
11、DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图。
12、最终视图呈现给用户。
首先,提到Spring框架,就要想到IOC(控制反转)、DI(依赖注入)、DI是实现IOC的方式,或者说DI和IOC是同一个事情两个不同的维度理解。IOC是Spring的核心。
这个问题我们只聊AOP
AOP是Spring中最重要的功能。
面向切面编程,通过预编译方式和运行期动态代理实现程序功能统一维护的一种技术。通过AOP可以使业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高了开发的效率。
比如我们要在业务中写一个日志的业务,日志这个业务就是我们的横切关注点,日志这个类就算切面,通知是切面里面的方法。
说的通俗一点,其实就是将程序里面一些和核心业务无关的代码,比如日志事务等,这些所谓的周边功能定义成切面。核心业务功能和切面功能独立开放,然后将切面的功能横切进去编织在一起。这样的好处就是减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。这样够通俗易懂了吧。
代理模式:
说完通俗易懂的,人人都知道的大白话后,面试官肯定不买账啊。
接下来我们就要说一下Spring AOP的底层实现了,SpringAOP的底层实现,实际上是基于代理模式的,代理模式是一种常用的设计模式。给一个对象提供一个代理对象,并由代理对象控制原对象的引用。
角色分析:抽象角色:一般会使用接口或者抽象类来解决。真实角色:被代理的角色。代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作。
代理模式分为静态代理和动态代理,他们的代理角色都一样,区别是:
动态代理的代理类是动态生成的,不是我们直接写好的,我们不再需要手动的创建代理类,只需要编写一个动态处理器就可以了
动态代理分为两大类:基于接口的动态代理---JDK动态代理,基于类的动态代理----cglib。Java字节码实现--javasist
问到动态代理,就要想到两个类
JDK动态代理:
**Proxy:**提供了创建动态代理类的实例的静态方法。
**InvocationHandler:**指定动态处理器,由代理实例的调用处理程序实现的接口,每个代理实例都有一个关联的调用处理程序。
做业务增强的,里面有involk方法 dosomethingbefore dosmethingend
CGLib动态代理:
需要引入依赖,实现MethodInterceptor接口,重写intercept方法。
总结一下:CGLIB创建代理对象性能更高,但是花费时间多。对于单例对象,用CGLIB合适。
动态代理的好处:
可以使真实的角色的操作更加纯粹,不用去关注一些公共的业务
公共业务就交给代理角色!实现了业务的分工!
公共业务发生扩展的时候,方便集中管理!
一个动态代理类可以实现多个类,只要是实现了同一个接口即可
AOP实现方式:
- JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
- CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
- 区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。
https://blog.csdn.net/wangzhihao1994/article/details/80913210
- 在代理对象不是接口类型或不是代理类时,指定proxyTargetClass=true后,执行CGLIB代理
- 代理对象是接口类型或是代理类,使用JDK代理
传统的方式下,我们程序员自己new对象,需要什么对象就创建什么对象,这样导致程序之间强耦合,而且对象之间也没有统一进行管理和配置。IOC容器就是来帮我们解决这个问题的。所有对象由IOC容器进行管理,我们只需要告诉程序什么时候我们需要什么就可以了。IOC其实也就是控制反转,控制是指我们创建对象的这个权力,反转是说这个权力,控制权反转了,创建程序的权力由我们程序员变成了IOC容器手里。IOC容器解决了对象之间的耦合问题,我们不需要通过new来创建对象,而是从容器中获取,达到了松耦合。
IOC容器帮我们管理bean。那DI依赖注入和IOC又有什么关系呢?其实他们描述的都是一件事情(对象的实例化和依赖关系)不同的是,IOC是一种思想,DI是一种具体的技术实现手段。IOC是站在对象的角度上。DI是站在容器的角度上。
IOC, 首先要可以存储对象,还要有注解注入的功能。我们可以通过反射机制来获取对象的各种属性,存储对象,移除对象,更多的获取和注册对象的方法。
https://blog.csdn.net/weixin_44259720/article/details/95996541
1、IOC像工厂一样,工厂设计模式
2、Spring中的Bean作用域是单例的,用到了单例设计模式。
3、AOP面向切面编程,用到了代理模式,AOP就是基于动态代理的
4、以 Template 结尾的对数据库操作的类,用到了模板方法模式
5、HandlerAdapter
作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller
作为需要适配的类,用到了适配器模式。
https://www.cnblogs.com/kenshinobiy/p/4652008.html
实例化---->设置属性---->Aware注入(BeanName BeanFactory ApplicationContext)---->BeanPostProcessor(BeforeInitialzation AfterInitialization )---->destory
- 实例化 Instantiation
- 属性赋值 Populate
- 初始化 Initialization
- 销毁 Destruction
==singleton:单例模式,Spring IoC容器中只会存在一个共享的Bean实例,无论有多少个Bean引用它,始终指向同一对象。Singleton作用域是Spring中的缺省作用域,也可以显示的将Bean定义为singleton模式,配置为:==
prototype:原型模式,每次通过Spring容器获取prototype定义的bean时,容器都将创建一个新的Bean实例,每个Bean实例都有自己的属性和状态,而singleton全局只有一个对象。根据经验,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。 request:在一次Http请求中,容器会返回该Bean的同一实例。而对不同的Http请求则会产生新的Bean,而且该bean仅在当前Http Request内有效。 ,针对每一次Http请求,Spring容器根据该bean的定义创建一个全新的实例,且该实例仅在当前Http请求内有效,而其它请求无法看到当前请求中状态的变化,当当前Http请求结束,该bean实例也将会被销毁。 session:在一次Http Session中,容器会返回该Bean的同一实例。而对不同的Session请求则会创建新的实例,该bean实例仅在当前Session内有效。 ,同Http请求相同,每一次session请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的session请求内有效,请求结束,则实例将被销毁。 global Session:在一个全局的Http Session中,容器会返回该Bean的同一个实例,仅在使用portlet context时有效。
- BeanPostProcessor:Bean的后置处理器,主要在bean初始化前后工作。(before和after两个回调中间只处理了init-method)
- InstantiationAwareBeanPostProcessor:继承于BeanPostProcessor,主要在实例化bean前后工作(TargetSource的AOP创建代理对象就是通过该接口实现)
- BeanFactoryPostProcessor:Bean工厂的后置处理器,在bean定义(bean definitions)加载完成后,bean尚未初始化前执行。
- BeanDefinitionRegistryPostProcessor:继承于BeanFactoryPostProcessor。其自定义的方法postProcessBeanDefinitionRegistry会在bean定义(bean definitions)将要加载,bean尚未初始化前真执行,即在BeanFactoryPostProcessor的postProcessBeanFactory方法前被调用。
https://www.cnblogs.com/myseries/p/11729800.html
这种线程模式不是线程安全的。要保证变量线程安全,可以用ThreadLocal来封装
Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究。
所以其实任何无状态单例都是线程安全的。
https://blog.csdn.net/chinacr07/article/details/78817449
编程式事务
声明式事务
基于TransactionProxyFactoryBean的声明式事务管理
基于@Transactional的声明式事务
基于Aspectj AOP配置事务
Spring的事务传播行为
==默认是Required的事务传播行为,如果没有事务就新建一个事务,如果已经存在一个事务就加入到这个事务。==
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
https://blog.csdn.net/jiangyu1013/article/details/84397366
@Transactional 实现原理:
1) 事务开始时,通过AOP机制,生成一个代理connection对象,
并将其放入 DataSource 实例的某个与 DataSourceTransactionManager 相关的某处容器中。
在接下来的整个事务中,客户代码都应该使用该 connection 连接数据库,
执行所有数据库命令。
[不使用该 connection 连接数据库执行的数据库命令,在本事务回滚的时候得不到回滚]
(物理连接 connection 逻辑上新建一个会话session;
DataSource 与 TransactionManager 配置相同的数据源)
2) 事务结束时,回滚在第1步骤中得到的代理 connection 对象上执行的数据库命令,
然后关闭该代理 connection 对象。
(事务结束后,回滚操作不会对已执行完毕的SQL操作命令起作用
https://blog.csdn.net/fumushan/article/details/80090947
在对象内部的方法中调用该对象的其他使用AOP注解的方法,被调用方法的AOP注解失效。
我们可以看出当方法被代理时,其实是动态生成了一个代理对象,代理对象去执行 invoke方法,在调用被代理对象的方法来完成业务。当在被代理对象的方法中调用被代理对象的方法时。其实是没有用代理调用,是通过被代理对象本身调用的。
在上面的例子中,调用UserService中的hello()方法时,Spring的动态代理帮我们动态生成了一个代理的对象,暂且叫他$UserService。所以调用hello()方法实际上是代理对象$UserService调用的。但是在hello()方法内调用同一个类的另外一个注解方法saveUser()时,实际上是通过this.saveUser()执行的, this 指的是UserService 对象,并不是$UserService代理对象调用的,没有走代理。所以注解失效。
解决办法
Spring解决方案
通过AopContext.currentProxy获取当前代理对象,通过代理对象调用方法。最好的方法是避免在方法内部调用。
放入ThreadLocal中
修改XML 新增如下语句;先开启cglib代理,开启 exposeProxy = true,暴露代理对象
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
public class UserService{
@Transactional
public void hello(){
System.out.println("开始hello");
try {
//通过代理对象去调用saveUser()方法
(UserService)AopContext.currentProxy().saveUser();
} catch (Exception e) {
logger.warn("发送消息异常");
}
}
@Transactional
public void saveUser(){
User user = new User();
user.setName("zhangsan");
System.out.println("将用户存入数据库");
}
}
SpringBoot解决方案
通过实现ApplicationContext获取代理对象。新建获取代理对象的工具类SpringUtil
public class UserService{ //买火车票
@Transactional
public void hello(){
System.out.println("开始hello");
try {
//通过代理对象去调用saveUser()方法
SpringUtil.getBean(this.getClass()).saveUser()
} catch(Exception e) {
logger.error("发送消息异常");
}
}
@Transactional
public void saveUser(){
User user = new User();
user.setName("zhangsan");
System.out.println("将用户存入数据库");
}
}
评论:直接在类内部注入自身代理对象
https://blog.csdn.net/qq_36525300/article/details/102886871
https://mp.weixin.qq.com/s/0qk2kaCKLdAViVzsw401sg
Spring有两个核心思想,IOC和AOP,有效的管理了Bean对象,解决了程序高耦合度的问题,同时也带来了其他问题,大量繁琐的配置,配置地狱,SpringBoot减少了大量的XML配置,约定大于配置,简化了大量XML配置,开箱即用,约定大于配置。我们只需要按照约定配置我们需要的就可以了。
@SpringBootApplication
@ComponentScan,自动扫描包下符合条件(加@Component,@Repository这类注解的)的Bean,加载到IOC容器中
@EnableAutoConfiguration,是SpringBoot自动化配置的核心注解,通过这个注解把spring应用所需要的bean自动注入容器中。
@SpringBootConfiguration ,与@Configuration类似,标注当前是配置类。
https://blog.csdn.net/u014745069/article/details/83820511
Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
一定要记得XxxxProperties类的含义是:封装配置文件中相关属性;XxxxAutoConfiguration类的含义是:自动配置类,目的是给容器中添加组件。
而其他的主方法启动,则是为了加载这些五花八门的XxxxAutoConfiguration类。
https://blog.csdn.net/gui694278452/article/details/104668798
每个xxxAutoConfiguration都是一个基于java的bean配置类。实际上,这些xxxAutoConfiguration不是所有都会被加载,会根据xxxAutoConfiguration上的@ConditionalOnClass等条件判断是否加载;通过反射机制将spring.factories中@Configuration类实例化为对应的java实列。
https://www.cnblogs.com/herberts/p/13178161.html
https://blog.csdn.net/mnicsm/article/details/93893669
1、创建SpringApplication实例
2、判断项目类型
3、设置应用上下文初始化器ApplicationContextInitializer
4、设置监听器 ApplicationListener
5、设置程序的主类
6、执行run方法,创建计时器,打印Banner,创建应用上下文,准备刷新应用上下文,执行Runner,触发监听器running方法
@Autowired注解默认是按照类型来装配依赖对象的,如果存在多个类型,会使用byName,可以通过Qualifier指定name.
@Resource有name和type属性,默认是按照name来装配对象的。
https://zhuanlan.zhihu.com/p/196688174
https://www.cnblogs.com/aspirant/p/9082858.html
BeanFactory是spring中比较原始的Factory,Bean的容器根接口,给Spring的容器定义一套规范,给IOC容器提供了一套完整的规范,比如常用到的getBean方法等。
而FactoryBean是一种Bean创建的方式,对Bean的一种扩展,对于复杂的Bean对象初始化创建使用其可封装对象的创建细节。
Spring通过反射机制利用的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory .FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。
- BeanFactory是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能。
- ApplicationContext应用上下文,继承BeanFactory接口,它是Spring的一个更高级的容器,提供了更多的有用的功能。如国际化,访问资源,载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,消息发送、响应机制,AOP等。
- BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化。ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化
BeanFactory是Spring的底层接口,实现了对Bean的配置和管理;ApplicationContext是BeanFactory的子接口,并且扩展了一些功能,包括AOP,国际化,事件驱动,BeanPostProcessor和BeanFactoryPostProcessor
https://www.cnblogs.com/like5635/articles/13597943.html
https://www.jianshu.com/p/6c359768b1dc
https://cloud.tencent.com/developer/article/1497692
构造器注入构成的循环依赖,这种循环依赖方式是无法解决的,只能抛出BeanCurrentlyInCreationException
异常,根本原因是Spring解决循环依赖靠的是Bean的“中间态”这个概念,而中间态指的是已经实例化,但还没有初始化的状态,而构造器是完成实例化的东东,所以构造器的循环依赖无法解决。
Spring创建Bean的流程
createBeanInstance
:实例化,其实也就是调用对象的构造方法实例化对象populateBean
:填充属性,这一步主要是对bean的依赖属性进行注入(@Autowired
)initializeBean
:回到一些形如initMethod
、InitializingBean
等方法
三级缓存
singletonObjects
:用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用earlySingletonObjects
:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖singletonFactories
:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖
Spring使用了三级缓存解决了循环依赖的问题。在populateBean()给属性赋值阶段里面Spring会解析你的属性,并且赋值,当发现,A对象里面依赖了B,此时又会走getBean方法,但这个时候,你去缓存中是可以拿的到的。因为我们在对createBeanInstance对象创建完成以后已经放入了缓存当中,所以创建B的时候发现依赖A,直接就从缓存中去拿,此时B创建完,A也创建完,一共执行了4次。至此Bean的创建完成,最后将创建好的Bean放入单例缓存池中。(非单例的实例作用域是不允许出现循环依赖)
只使用一级缓存可以吗?
不可以,会把成品状态的bean对象和半成品状态的bean对象放在一起,而半成品对象无法暴漏给外部使用,所以要将成品和半成品分开,一级缓存中放成品,二级缓存中放半成品对象。
只使用二级缓存可以吗?
如果整个应用程序中不涉及aop的存在,那么二级缓存中足以解决循环依赖问题,如果aop中存在了循环依赖,那么就必须使用三级缓存才能解决。
为什么需要三级缓存?
三级缓存的value类型是ObjectFactory,是一个函数式接口,不是直接调用的,只有在调用getObject方法的时候才会去调用里面存储的lambda表达式,存在的意义是保证整个容器运行过程中同名的bean对象只能有一个。
getobject实际调用了个lambda表达式,去判断到底是生成代理对象还是生成原始对象。
https://blog.csdn.net/bingguang1993/article/details/88915576
https://blog.csdn.net/u010853261/article/details/77940767
https://www.freesion.com/article/62151334702/
什么是循环依赖,就是循环引用,2个或者以上bean互相持有对方,最终形成闭环。
使用构造函数注入的时候才会发生这种问题,如果使用的是其他类型的注入,不应该出现此问题。因为依赖项将在需要时注入,而不是在上下文加载时注入。
Spring为了解决单例的循环依赖问题,使用了三级缓存。
解决办法 1、重新设计 2、@Lazy注解 3、使用setter注入,而不是使用构造函数注入。这样Spring只会在需要的时候注入依赖,在需要之前不会注入依赖 4、使用@PostConstruct 5、实现ApplicationContextAwre和InitializingBean
Spring无法解决传入参数的循环依赖,也就是构造函数的循环依赖无法解决。
https://www.cnblogs.com/hopeofthevillage/p/11427438.html
https://www.cnblogs.com/happyflyingpig/p/7739749.html
使用缓存来减少与数据库交互的次数,从而提高运行效率,进行查询后,将结果放在缓存中,查询时从缓存中拿 一级缓存:是SQLSession级别的,操作数据库需要SQLSession对象,在对象中有一个HashMap用来缓存数据,在同一个SQLSession中执行两次相同的查询时,第一次会进行缓存,第二次从缓存中拿,执行修改操作后,缓存失效,保证数据的有效性
执行SQL查询中间发生了增删改的操作,MyBatis会把SqlSession的缓存清空。mybatis的的一级缓存是SqlSession级别的缓存,一级缓存缓存的是对象,当SqlSession提交、关闭以及其他的更新数据库的操作发生后,一级缓存就会清空。
二级缓存:默认是关闭的,是Mapper级别的,当多个SQLSession使用同一个Mapper的SQL语句操作数据库的时候,得到的数据会在二级缓存中,也用HashMap存,作用域是Mapper的namespace,不同的SQLSession两次执行相同的SQL,第二次会从二级缓存中拿
SqlSessionFactory层面上的二级缓存默认是不开启的,二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。 也就是要求实现Serializable接口,配置方法很简单,只需要在映射XML文件配置就可以开启缓存了,如果我们配置了二级缓存就意味着:**二级缓存是SqlSessionFactory级别的缓存,同一个SqlSessionFactory产生的SqlSession都共享一个 ** 二级缓存,二级缓存中存储的是数据,当命中二级缓存时,通过存储的数据构造对象返回。查询数据的时候,查询的流程是二级缓存>一级缓存>数据库
https://blog.csdn.net/u013552450/article/details/72528498
#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。
$将传入的数值直接显示生成在sql中
#方式能够很大程度防止sql注入$无法防止sql注入
MyBatis排序时使用order by 动态参数时需要注意,用$而不是#
在Spring MVC 中使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求
@GetMapping用于将HTTP get请求映射到特定处理程序的方法注解 具体来说,@GetMapping是一个组合注解,是@RequestMapping(method = RequestMethod.GET)的缩写。
@PostMapping用于将HTTP post请求映射到特定处理程序的方法注解 具体来说,@PostMapping是一个组合注解,是@RequestMapping(method = RequestMethod.POST)的缩写。
@PathVariable
@PathVariable("xxx")
通过 @PathVariable 可以将URL中占位符参数{xxx}绑定到处理器类的方法形参中@PathVariable(“xxx“)
@RequestMapping(value=”user/{id}/{name}”)
请求路径:http://localhost:8080/hello/show5/1/james
@Valid
用于验证注解是否符合要求,@Valid注解用于校验,所属包为:javax.validation.Valid。
@RequestParam
https://blog.csdn.net/sswqzx/article/details/84195043
@RequestParam:将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解)
语法:@RequestParam(value=”参数名”,required=”true/false”,defaultValue=””)
value:参数名
required:是否包含该参数,默认为true,表示该请求路径中必须包含该参数,如果不包含就报错。
defaultValue:默认参数值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值
单例模式:
https://www.cnblogs.com/binaway/p/8889184.html
/**
* 饿汉式单例模式
*不管会不会用到该实例对象,先创建了再说,很着急的样子
* 优点:
*实现起来简单,没有多线程同步问题
* 缺点:
* 消耗内存
*/
public class Singletonhungry {
//将自身实例化对象设置为一个属性,并用static、final修饰
private static final Singletonhungry instance=new Singletonhungry();
//构造方法私有化
private Singletonhungry(){}
//静态方法返回该实例
public static Singletonhungry getInstance(){
return instance;
}
}
/**
* 懒汉式单例模式
* 延迟加载,先不着急实例化对象,等到要用的时候才创建
* get方法中进行new实例化
* 优点:实现起来比较简单,节省内存
* 缺点:多线程下不能保证是单例的状态
*
*/
public class Singletonlazy {
//将自身实例化对象设置为一个属性,用static修饰
private static Singletonlazy instance;
//构造方法私有化
private Singletonlazy(){}
//静态方法返回该实例
public static Singletonlazy getInstance(){
if(instance==null){
instance=new Singletonlazy();
}
return instance;
}
}
/**
*
* DCL双重锁检测机制
* 线程安全的懒汉单例模式
*
*/
public class Singletonlazy2 {
//将自身实例化对象设置为一个属性,并用static修饰
private static volatile Singletonlazy2 instance;
//构造方法私有化
private Singletonlazy2(){}
//静态方法返回该实例
public static Singletonlazy2 getInstance(){
//第一次检测instance是否被实例化出来,如果没有进入if块
if(instance==null){
synchronized (Singletonlazy2.class){
//某个线程取得了类锁、实例化对象前第二次检查instance是否已经被实例化
//出来,如果没有,才最终实例出对象
if(instance==null){
instance=new Singletonlazy2();
}
}
}
return instance;
}
}
/**
*
* 枚举的单例模式
*
*/
public enum EnumSingle {
ENUM_SINGLE;
public static EnumSingle getEnumSingle(){
return ENUM_SINGLE;
}
}
模板模式:
package com.ccl.juc.模板模式;
/**
* @author 超厉害的我啊
* @date 2021/6/8 22:19:40
*/
public abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
//模板
public final void play(){
//初始化游戏
initialize();
//开始游戏
startPlay();
//结束游戏
endPlay();
}
}
package com.ccl.juc.模板模式;
/**
* @author 超厉害的我啊
* @date 2021/6/8 22:22:09
*/
public class Game1 extends Game{
@Override
void initialize() {
System.out.println("游戏1初始化");
}
@Override
void startPlay() {
System.out.println("游戏1开始了");
}
@Override
void endPlay() {
System.out.println("游戏1结束了");
}
}
package com.ccl.juc.模板模式;
/**
* @author 超厉害的我啊
* @date 2021/6/8 22:26:17
*/
public class Game2 extends Game{
@Override
void initialize() {
System.out.println("游戏2初始化");
}
@Override
void startPlay() {
System.out.println("游戏2开始了");
}
@Override
void endPlay() {
System.out.println("游戏2结束了");
}
}
package com.ccl.juc.模板模式;
/**
* @author 超厉害的我啊
* @date 2021/6/8 22:23:50
*/
public class TemplatePatternDemo {
public static void main(String[] args) {
Game game=new Game1();
game.play();
System.out.println();
game=new Game2();
game.play();
}
}
输出结果:
游戏1初始化
游戏1开始了
游戏1结束了
游戏2初始化
游戏2开始了
游戏2结束了
单例,工厂,代理、装饰器,观察者
开闭原则:对扩展开放,对修改关闭
里氏替换原则:继承必须确保超类所拥有的性质在子类中仍然成立
依赖倒置原则:要面向接口编程,不要面向实现编程
单一职责原则:控制类的粒度大小,将对象解耦、提高其内聚性
接口隔离原则:控制类的粒度大小,将对象解耦、提高其内聚性
迪米特法则:只与你的直接朋友交谈,不跟陌生人说话
合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑继承关系来实现
继承Thread类,
实现Runnable接口;
实现Callable接口;可以有返回值,返回值通过FutureTask进行封装。
实现Runnable和Callable接口的类只能当作一个可以在线程中允许的任务,不是真正意义上的线程,因此最后还需要通过Thread来调用。可以理解为Thread来调用。
使用线程池创建
初始状态,可运行状态,终止状态和休眠状态四大类。
//NEW (新生)--->RUNNABLE(运行)----->BLOCKED(阻塞)----->WAITING(等待,死死的等)------>TIMED_WAITING(超时等待)------->TERMINATED(终止)
线程新建的时候就是初始状态,还未start。
可运行状态就是可以运行,可能正在运行,也可能正在等 CPU 时间片。
休眠状态分为三种,一种是等待锁的 blocked 状态,一种是等待条件的 waitting 状态,或者有时间限制的等待 timed_waitting 状态。
- 等待条件的操作有:Object.wait、Thread.join、LockSupport.park()
- 时间等待就是上面设置了timeout参数的方法,例如Object.wait(1000)。
终止状态就是线程结束执行了,可以是结束任务后的自动结束,也可以是产生了异常而结束。
1、创建状态 new出来的时候
2、就绪状态 调用start方法
3、阻塞状态 sleep、wait、同步锁定
4、运行状态
5、死亡状态 一旦进入死亡状态,线程就不能再次启动
1、通过Object的wait和notify
2、通过Condition的awiat和signal
3、volatile和synchronized
4、通过ArrayBlockingQueue 阻塞队列(生产者消费者)
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
不可变: 不可变的对象一定是线程安全的,不需要再采取线程安全保障措施,只要一个不可变对象被正确的构建出来,永远也不会看到它在多个线程之中处于不一致的状态。不可变类型:final关键字修饰的基本数据类型,String、枚举类型、Long Double等包装类型。
**互斥同步:**synchronized 和ReentrantLock
非阻塞同步: 互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。属于悲观的并发策略。
乐观的并发策略:先进行操作,如果没有其他线程争用共享数据,那操作就成功,否则采取补偿措施,(不断重试,直到成功为止)
1、CAS 乐观锁需要操作和冲突检测两个步骤具备原子性。靠硬件来完成。CAS是比较并交换(Compare-and-Swap)CAS指令需要三个操作数,分别是内存地址V、旧的预期值A和新增B。当执行操作时,只有当V的值等于A,才将V的值更新为B。
2、AtomicInteger
3、ABA问题:如果一个变量初次读取的时候是A值,它的值被改成了B,后来又被改回A,那CAS操作就会误认为它从来没有被改变过。
JUC包提供了一个带有标记的原子引用类AtomicStampedReference来解决这个问题,它可以通过控制变量值的版本来保证CAS的正确性。大部分情况下ABA问题不会影响程序并发的正确性。
无同步方案:
要保证线程安全,并不是一定就要同步,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
1、栈封闭 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
2、线程本地存储 共享数据的可见范围限制在同一个线程之内。最经典的就是Web交互模型中的:“一个请求对应一个服务器线程”的处理方式,这种处理方式应用使得很多web服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用ThreadLocal 类来实现线程本地存储功能。
3、可重入代码:
这种代码也叫做纯代码。可以在代码执行的任何时刻中断它,转而去执行另外一段代码。而在控制权返回后,原来的程序不会出现任何错误。
提到线程池就会问的问题:三大方法,7大参数,4种拒绝策略
1、降低资源的消耗
2、提高响应的速度
3、方便管理
https://blog.csdn.net/weixin_43732570/article/details/88580603
创建线程阿里巴巴开发手册说,不要用Executor,自己手动自定义线程池!ExecutorService threadPool=new ThreadPoolExecutor();
1.线程管理池(ThreadPool):用于创建并管理线程池,有创建,销毁,添加新任务; 2.工作线程(PoolWorker):线程池中的线程在没有任务的时候处于等待状态,可以循环的执行任务; 3.任务接口(Task):每个任务必须实现接口,用来提供工作线程调度任务的执行,规定了任务的入口以及执行结束的收尾工作和任务的执行状态等; 4.任务队列:用于存放没有处理的任务,提供一种缓存机制
https://blog.jboost.cn/threadpool.html
https://blog.csdn.net/acohi68664/article/details/102178447
问题:三大方法、7大参数、4种拒绝策略
ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程 ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小 ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
线程池的好处
1、降低资源的消耗
2、提高响应的速度
3、方便管理
线程复用,可以控制最大并发数,管理线程
- 降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁线程造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
1、CPU密集型 几核,就是几,可以保证CPU的效率最高
2、IO密集型 判断程序中十分耗IO的线程,设置的值大于这些线程。
https://blog.csdn.net/ye17186/article/details/89467919
1、corePoolSize:线程池核心线程大小。2
2、maximumPoolSize:线程池最大线程数量。5
3、keepAliveTime:空闲线程存活时间。3
4、unit:空闲线程存活时间单位。
5、workQueue:工作队列 3候客区
6、threadFactory 线程工厂 (指定优先级,指定线程名称,方便监控,指定是否是守护线程)
7、handler 拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略就算解决这个问题的,jdk提供了4种拒绝策略。
四种拒绝策略:
1、CallerRunsPolicy:该策略下,在调用者线程中直接执行被拒绝任务的run方法。除非线程池已经shutdown,则直接抛弃任务。哪来的去哪里
2、AbortPolicy:该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。银行人超出,抛出异常。这是线程池的默认拒绝策略。
3、DiscardPolicy:该策略下,直接丢弃任务,什么都不做。
4、DiscardOlddestPolicy:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
https://blog.csdn.net/u011582840/article/details/107537142
为什么不能用Excutor
- ArrayBlockingQueue:底层是数组,有界队列,如果我们要使用生产者-消费者模式,这是非常好的选择。
- LinkedBlockingQueue:底层是链表,可以当做无界和有界队列来使用,所以大家不要以为它就是无界队列。
- SynchronousQueue:本身不带有空间来存储任何元素,使用上可以选择公平模式和非公平模式。
- PriorityBlockingQueue:无界队列,基于数组,数据结构为二叉堆,数组第一个也是树的根节点总是最小值。
举例 ArrayBlockingQueue 实现并发同步的原理:原理就是读操作和写操作都需要获取到 AQS 独占锁才能进行操作。如果队列为空,这个时候读操作的线程进入到读线程队列排队,等待写线程写入新的元素,然后唤醒读线程队列的第一个等待线程。如果队列已满,这个时候写操作的线程进入到写线程队列排队,等待读线程将队列元素移除腾出空间,然后唤醒写线程队列的第一个等待线程。
- 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。
- 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。
- 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
四种拒绝策略:
1、CallerRunsPolicy:该策略下,在调用者线程中直接执行被拒绝任务的run方法。除非线程池已经shutdown,则直接抛弃任务。
2、AbortPolicy:该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
3、DiscardPolicy:该策略下,直接丢弃任务,什么都不做。
4、DiscardOlddestPolicy:该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
https://blog.csdn.net/XF777/article/details/110697818
double check 和 静态实例饿汉
使用饿汉的方式,简单,线程安全。
1、首先用户提交任务到线程池,判断当前工作的线程池有没有大于核心线程数,如果小于核心线程数,则创建线程并执行任务,如果大于核心线程数,则判断任务队列是否已满。
2、如果任务队列没有满,则把任务缓存到任务队列中
如果任务队列已满,则判断当前工作线程数量是否大于最大线程数量
3、如果没有大于最大线程数量,则创建线程并执行任务,如果大于最大线程数量,则启用线程池的拒绝策略handler
线程复用:
https://blog.csdn.net/weixin_47277170/article/details/107063055
try {
//一直执行 如果task不为空 或者 从队列中获取的task不为空
while (task != null || (task = getTask()) != null) {
task.run();//执行task中的run方法
}
}
completedAbruptly = false;
} finally {
//1.将 worker 从数组 workers 里删除掉
//2.根据布尔值 allowCoreThreadTimeOut 来决定是否补充新的 Worker 进数组 workers
processWorkerExit(w, completedAbruptly);
}
源码里面有一段while true循环,当还可以获取到任务的时候,就不停的执行,所有线程都在while循环里面
通过tash.run()来执行具体的任务,而不是新建线程。
线程回收:
https://www.cnblogs.com/kingsleylam/p/11241625.html
ThreadPoolExecutor回收工作线程,一条线程getTask()返回null,就会被回收。
分两种场景。
- 未调用shutdown() ,RUNNING状态下全部任务执行完成的场景
线程数量大于corePoolSize,线程超时阻塞,超时唤醒后CAS减少工作线程数,如果CAS成功,返回null,线程回收。否则进入下一次循环。当工作者线程数量小于等于corePoolSize,就可以一直阻塞了。
- 调用shutdown() ,全部任务执行完成的场景
shutdown() 会向所有线程发出中断信号,这时有两种可能。
2.1)所有线程都在阻塞
中断唤醒,进入循环,都符合第一个if判断条件,都返回null,所有线程回收。
2.2)任务还没有完全执行完
至少会有一条线程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里会调用tryTerminate(),向任意空闲线程发出中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收。
CAS就是compare and swap 比较并交换。
https://blog.csdn.net/lengyue309/article/details/83574721
水平触发和边沿触发
这个例子很明确的显示了水平触发和边沿触发的区别。
- 水平触发只关心文件描述符中是否还有没完成处理的数据,如果有,不管怎样
epoll_wait
,总是会被返回。简单说——水平触发代表了一种“状态”。 - 边沿触发只关心文件描述符是否有新的事件产生,如果有,则返回;如果返回过一次,不管程序是否处理了,只要没有新的事件产生,
epoll_wait
不会再认为这个fd被“触发”了。简单说——边沿触发代表了一个“事件”。
https://zhuanlan.zhihu.com/p/71156910
再问synchronized,把这篇文章熟读
https://blog.csdn.net/weixin_36759405/article/details/83034386
JVM对synchronized锁的优化:减少获得锁和释放锁带来的性能消耗。
synchronized从无锁到偏向锁,再到轻量级锁(自旋锁),再到重量级锁,都是悲观的。
在轻量级锁状态下竞争,没抢到锁的线程将自旋,即不停的循环判断锁是否能被成功获取。(自旋的默认次数是10次),还引入了自适应自旋
锁消除,锁粗化。
- 修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
- 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
- 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
可中断锁,字面意思是“可以响应中断的锁”
synchronized就是不可中断锁,而Lock的实现类都是**可中断锁,**可以简单看下Lock接口。
轻量级锁是相对于传统的重量级锁而言,它使用CAS操作来避免重量级锁使用互斥量的开销,对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用CAS操作进行同步,如果CAS失败了再改用互斥量进行同步。
每个线程对资源操作前都尝试先加锁,成功加锁后才能操作。操作结束后解锁。
t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续
https://blog.csdn.net/u010983881/article/details/80257703
首先join() 是一个synchronized方法, 里面调用了wait(),这个过程的目的是让持有这个同步锁的线程进入等待,那么谁持有了这个同步锁呢?答案是主线程,因为主线程调用了threadA.join()方法,相当于在threadA.join()代码这块写了一个同步代码块,谁去执行了这段代码呢,是主线程,所以主线程被wait()了。然后在子线程threadA执行完毕之后,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,会继续执行。
1、Synchronized 是内置的Java关键字,Lock是一个Java类。
2、Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁。
3、Synchronized 会自动释放锁,Lock必须要手动释放锁,如果不释放锁会造成死锁。
4、Synchronized 线程1(获得锁,阻塞)线程2(等待,傻傻的等)Lock锁不会一直等待下去。
5、Synchronized 可重入锁,不可中断,非公平,Lock可重入锁,可以中断锁,可以自己设置非公平。
6、Synchronized 适合锁少量代码同步问题,Lock 适合锁大量的同步代码!
互斥同步。Java提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是JVM实现的synchronized,另一个是JDK实现的ReentrantLock。
两个的不同:
1、实现方式:
synchronized是JVM实现的,而ReentrantLock是JDK实现的
2、性能:
新版本Java对synchronized进行了很多优化,例如自旋锁等,synchronized与ReentrantLock大致相同。
3、等待可中断:
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock可中断(Lock的实现类都是可中断的),而synchronized是不可中断的。
4、公平锁:
公平锁是指多个线程在等待同一个锁时,必须按照申请时间的顺序来依次获得锁。
synchronized中的锁是非公平的。ReentrantLock 默认情况下也是非公平的,但是可以通过构造函数传参实现公平锁。
非公平锁吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁
5、锁绑定多个条件:
一个ReentrantLock可以同时绑定多个Condition对象。
使用选择:
除非需要使用ReentrantLock的高级功能,否则优先使用synchronized,这是因为synchronized是JVM实现的一种锁机制,JVM原生支持它,而ReentrantLock不是所有的JDK版本都支持,并且使用synchronized不用担心没有释放锁而导致死锁问题,因为JVM会确保锁的释放。
Synchronized和ReentrantLock都是可重入锁,ReentrantLock需要手动解锁,而Synchronized不需要
ReentrantLock支持设置超时时间,可以避免死锁,比较灵活,并且支持公平锁,可中断,支持条件判断
Synchronized不支持超时,非公平,不可中断,不支持条件。
一般情况下用Synchronized就够了,ReentrantLock比较灵活,支持的功能多,复杂情况用ReentrantLocK 。两者的性能现在差不多了。
https://blog.csdn.net/u_my_heart/article/details/90648609
https://blog.csdn.net/weixin_36759405/article/details/83034386
synchronized又有很多个阶段,轻量级锁阶段:
轻量级锁操作的就是对象头的 MarkWord 。
如果判断当前处于无锁状态,会在当前线程栈的当前栈帧中划出一块叫 LockRecord 的区域,然后把锁对象的 MarkWord 拷贝一份到 LockRecord 中称之为 dhw(就是那个set_displaced_header 方法执行的)里。
然后通过 CAS 把锁对象头指向这个 LockRecord 。
如果当前是有锁状态,并且是当前线程持有的,则将 null 放到 dhw 中,这是重入锁的逻辑。
逻辑还是很简单的,就是要把当前栈帧中 LockRecord 存储的 markword (dhw)通过 CAS 换回到对象头中。
如果获取到的 dhw 是 null 说明此时是重入的,所以直接返回即可,否则就是利用 CAS 换,如果 CAS 失败说明此时有竞争,那么就膨胀!
synchronized重量级锁的原理:
synchronized 底层是利用 monitor 对象,CAS 和 mutex 互斥锁来实现的,内部会有等待队列(cxq 和 EntryList)和条件等待队列(waitSet)来存放相应阻塞的线程。
未竞争到锁的线程存储到等待队列中,获得锁的线程调用 wait 后便存放在条件等待队列中,解锁和 notify 都会唤醒相应队列中的等待线程来争抢锁。
然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。
所以又引入了自适应自旋机制,来提高锁的性能。
JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。
具体实现是在编译之后在同步方法调用前加入一个monitor.enter
指令,在退出方法和异常处插入monitor.exit
的指令。
对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit
之后才能尝试继续获取锁。
流程图如下:
Synchronized是可重入的,所以自己不会把自己锁死。synchronized一旦被一个线程持有,其他试图获取该锁的线程将被阻塞。
Synchrinized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但是获得了高吞吐量。
可重入锁:字面意思就是可以重新进入的锁,即允许同一个线程多次获取同一把锁。比如递归函数里有加锁的操作,递归过程中,锁会阻塞自己吗?如果不会,那么这个锁就算可重入的,所以说可重入锁也叫做递归锁。
Java中以Reentrant[rei en chen t]开头命名的都是可重入锁,synchronized关键字锁也是可重入的。 面试题总结,还不错
Synchronized 的原理其实就是基于一个锁对象和锁对象相关联的一个 monitor 对象。
在偏向锁和轻量级锁的时候只需要利用 CAS 来操控锁对象头即可完成加解锁动作。
在升级为重量级锁之后还需要利用 monitor 对象,利用 CAS 和 mutex(互斥量) 来作为底层实现。
monitor 对象头部会有等待队列和条件等待队列,未竞争到锁的线程存储到等待队列中,获得锁的线程调用 wait 后便存放在条件等待队列中,解锁和 notify 都会唤醒相应队列中的等待线程来争抢锁。
然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。
所以才会有偏向锁和轻量级锁的优化,并且引入自适应自旋机制,来提高锁的性能。
关于 Synchronized 其实我写过两篇文章,看完这两篇文章你可以跟面试官说,你看过 JVM 源码,毫不夸张,因为就是从源码级别上分析的。
而且指明了一个几乎网上都错了的观点和一个常见的认知错误。
总而言之,看完之后对 Synchronized 基本上超越很多人了。
Synchronized 升级到重量级锁之后就下不来了?你错了!
来源:方圆想当图灵
依赖对象头中的MarkWord和monitor监视器,在Hotspot虚拟机中,是ObjectMonitor对象 其中MarkWord是实现偏向锁和轻量级锁的关键 monitor是实现重量级锁的原理,当系统检测到是重量级锁的时候,会把等待想到获取锁的线程进行阻塞,被阻塞的线程不会消耗CPU,但是阻塞和唤醒一个线程时,都需要操作系统来实现,而要完成用户态与内核态之间的转换,状态转换的开销会很大,对应的字节码指令是monitorenter和monitorexit
ReentrantLock 其实就是基于 AQS 实现的一个可重入锁,支持公平和非公平两种方式。
内部实现其实就是依靠一个 state 变量和两个等待队列:同步队列和等待队列。
利用 CAS 修改 state 来争抢锁。
争抢不到则入同步队列等待,同步队列是一个双向链表。
条件 condition 不满足时候则入等待队列等待,也是个双向链表。
是否是公平锁的区别在于:线程获取锁时是加入到同步队列尾部还是直接利用 CAS 争抢锁。
就是这么回事儿,理解起来应该不难,操心的我再画个图,嘿嘿。
作者:方圆想当图灵
ReentrantLock的静态内部类Sync实现了抽象类AQS(AbstractQueuedSynchronizer),其中有一个重要的字段是state,它在ReentrantLock中代表的是重入次数,为0是代表锁没有被任何线程持有,为1是被一个线程持有,每重入一次,state加一,每执行一次unlock方法,state减一;而ReentrantLock的公平锁和非公平锁机制是通过AQS中的队列来实现的,若是公平锁的话,每有一个线程想要获取这个锁,需要进入队列排队,而且不能插队,若是非公平锁的话,队列中的线程是可以插队的
谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!
类如其名,抽象的队列式的同步器(同步抽象队列),AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch...。
AQS将一些操作封装起来,比如入队等基本方法,暴露出方法,便于其他相关JUC锁的使用。就是起到了一个抽象,封装的作用。
两个作用:1、保证可见性。2、禁止指令重排序。
https://www.cnblogs.com/dolphin0520/p/3920373.html文章写的巨好。
指令重排序:处理器为了提高程序的运行效率,可能会对输入的代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果一致。
指令重排序在单线程下,不会有影响,但是会影响到线程并发执行的正确性。
也就是说要想并发程序正确的执行,必须保证原子性,可见性,和有序性,只要有一个没有被保证,就可能导致程序运行不正确。
Java内存模型规定所有变量都是存在主存当中,每个线程都有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接对内存进行操作。并且每个线程不能访问其他线程的工作内存。
那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?
原子性:
Java内存模型只保证了 读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性:
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值立刻更新到主存中,当有其他进程需要读取的时候,会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
volatile关键字的两层语义:
1、保证了不同线程对这个变量进行操作的可见性,即一个线程修改了某个变量的值,这个新的值对其他线程来说是可见的。
2、禁止进行指令重排序
volatile关键字能保证可见性,但是不能保证原子性,可见性只能保证每次读取的都是最新的值,但是volatile没办法保证对变量的操作的原子性。
volatile的原理和实现机制
前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile的内存屏障在单例模式中运用的最广,DCL懒汉式。
提到volatile关键字后就聊单例模式:下面是你的主场。
使用volatile关键字必须具备的两个条件:
1、对变量的写操作不依赖于当前值。
2、该变量没有包含在具有其他变量的不变式中。
其实理解起来就是volatile保证可见性,但不保证原子性(一定程度上保证有序性:因为他能禁止指令重排序),所以在使用的时候程序保证原子性的同时,使用volatile关键字才能使程序在并发的时候正确执行。
每个Thread中都有一个ThreadLocalMap对象,而ThreadLocalMap中存储的是多个ThreadLocal对象。其中ThreadLocalMap中的key为ThreadLocal对象,value为ThreadLocal中我们要存储的值。
会遇到的问题:内存泄漏
可以从强引用和弱引用 GC回收机制来说
一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,
广义并通俗的说,就是:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
在ThreadLocal中内存泄漏是指ThreadLocalMap中的Entry中的key为null,而value不为null。因为key为null导致value一直访问不到,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。如果 key 是强引用,那么发生 GC 时 ThreadLocalMap 还持有 ThreadLocal 的强引用,会导致 ThreadLocal 不会被回收,从而导致内存泄漏。弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 方法时被清除,这算是最优的解决方案。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。这个储值的Map并非ThreadLocal的成员变量,而是java.lang.Thread 类的成员变量。ThreadLocalMap实例是作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,”线程局部变量”为值,所以一个线程下可以保存多个”线程局部变量”。对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。由于Tomcat线程池的原因,我最初使用的”线程局部变量”保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值。所以在线程池的情况下,处理完成后主动调用该业务treadLocal的remove()方法,将”线程局部变量”清空,避免本线程下次处理的时候依然存在旧数据。
https://blog.csdn.net/clam_clam/article/details/6803667
https://www.cnblogs.com/binaway/p/8889184.html
/**
* 饿汉式单例模式
*不管会不会用到该实例对象,先创建了再说,很着急的样子
* 优点:
*实现起来简单,没有多线程同步问题
* 缺点:
* 消耗内存
*/
public class Singletonhungry {
//将自身实例化对象设置为一个属性,并用static、final修饰
private static final Singletonhungry instance=new Singletonhungry();
//构造方法私有化
private Singletonhungry(){}
//静态方法返回该实例
public static Singletonhungry getInstance(){
return instance;
}
}
/**
* 懒汉式单例模式
* 延迟加载,先不着急实例化对象,等到要用的时候才创建
* get方法中进行new实例化
* 优点:实现起来比较简单,节省内存
* 缺点:多线程下不能保证是单例的状态
*
*/
public class Singletonlazy {
//将自身实例化对象设置为一个属性,用static修饰
private static Singletonlazy instance;
//构造方法私有化
private Singletonlazy(){}
//静态方法返回该实例
public static Singletonlazy getInstance(){
if(instance==null){
instance=new Singletonlazy();
}
return instance;
}
}
/**
*
* DCL双重锁检测机制
* 线程安全的懒汉单例模式
*
*/
public class Singletonlazy2 {
//将自身实例化对象设置为一个属性,并用static修饰
private static volatile Singletonlazy2 instance;
//构造方法私有化
private Singletonlazy2(){}
//静态方法返回该实例
public static Singletonlazy2 getInstance(){
//第一次检测instance是否被实例化出来,如果没有进入if块
if(instance==null){
synchronized (Singletonlazy2.class){
//某个线程取得了类锁、实例化对象前第二次检查instance是否已经被实例化
//出来,如果没有,才最终实例出对象
if(instance==null){
instance=new Singletonlazy2();
}
}
}
return instance;
}
}
/**
*
* 枚举的单例模式
*
*/
public enum EnumSingle {
ENUM_SINGLE;
public static EnumSingle getEnumSingle(){
return ENUM_SINGLE;
}
}
https://blog.csdn.net/whgtheone/article/details/82990139
double check会被反射和序列化破坏,
但是枚举的类型会避免这些问题,一是在反射的时候会判断是否是枚举类型,如果是枚举的类型会抛出异常。
另外在序列化和反序列化的时候,枚举类型是有自己的一套规则,序列化通过name属性进行序列化保存,枚举对象不进行序列化,反序列化的时候,并没有创建新的对象,而通过name属性拿到原有对象,因此保证了枚举类型实现单例模式的序列化安全
缺点:
不可继承,无法扩展
ls -a #列出目录所有文件,包含以.开始的隐藏文件
cd 目录名 #进入目录 cd .. 返回上级目录
pwd #查看当前目录路径
mkdir #创建文件夹
rm #删除
chmod #改变文件的访问权限,控制用户对文件的权限的命令
ps -ef | grep xxx #查看指定进程
ps top #查看进程状态,top是动态的
find #在指定目录下查找文件
grep #用于查找文件里符合条件的字符串 可以用来查询日志
top #用于实时显示进程的动态
ps #用于显示当前进程的状态,类似于widowns的任务管理器
ifconfig #用于显示或设置网络设备,可设置网路设备的状态,或显示目前的设置。
su #切换用户
tail service.log -n 100 -f #查看底部即最新100条日志记录,并实时刷新
#less 用于查看大文件
less -n file
less +G -n file # 打开文件的时候,直接定位到文件的最底部,默认情况下是在首行。
大文件日志
https://www.cnblogs.com/cxhfuujust/p/12036916.html
du -h #查看当前目录下的所有目录的大小(使用-h参数)disk usage 磁盘使用情况
chmod 777 filename
4读2写 1执行 777三位代表 所有者、群组、其他人
在磁盘中搜索
find 目录 文件名称 find / nginx
查看文件位置
whereis 文件名称
mkdir后添加-p的参数
mkdir -p ccl/ccltest/test2
1、先使用top命令,找到cpu占用最高的进程PID
2、使用 ps -mp pid -o THREAD,tid,time 查询进程中,哪个线程的cpu占用率高,找到tid
3、jstack tid >> xxx.log 打印出该进程下线程日志
nginx是一个高性能的HTTP和反向代理web服务器。他可以反向代理和负载均衡,反向代理就是nginx代理服务器,当一个请求过来的时候,通过nginx代理,再发送到服务器端。将请求按照一定规则分发到不同的服务器上就是负载均衡。负载均衡策略有2种:内置策略和扩展策略。内置策略为轮询,加权轮询,Ip hash。
https://www.jianshu.com/p/79ca08116d57
是实现了高级消息队列协议的开源消息中间件。消息队列是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传输。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。
消息队列是一种应用间的异步协作机制,实现业务的解耦。
MiaoshaController里面实现InitializingBean接口,重写里面的afterPropertiesSet方法,实现的预加载。继承该接口的类,初始化bean的时候都会执行该方法。
通过后台管理进行添加,修改redis缓存和数据库中的值。
redis里面有一个decr() 方法,它实现的是递减操作,能够保证原子性。
https://blog.csdn.net/aoxida/article/details/115769230
分布式锁解决超卖
解决超卖问题:
悲观锁、分布式锁、乐观锁、队列串行化,异步队列分散,redis原子操作。
分布式锁无优化带来的问题:
同一个商品多个用户下单,会基于分布式锁串行化处理,导致没法同时处理大量的下单的请求,也就是无法应对高并发,对于低并发的小电商系统,还可以接收。
对分布式锁进行高并发优化
我现在就想使用分布式锁来解决,一秒对一个千万级别的订单。
基于ConcurrentHashMap 分段锁的思想
分段加锁,比如有1000个库存,完全可以拆成20个库存段。
stock_01 stock_02在redis中存放20个key,总之就算把库存拆开
然后写个简单的随机算法,每秒1000个请求过来了,每个请求随机在20个分段库存中选择一个进行加锁,这样同一时间就可以有多个线程去处理请求,然后需要注意当发现分段库存不足的时候,就自动释放锁,然后马上换下一个分段库存,再次尝试加锁。
这样的好处是并发性能增加了,但是也有不足:实现起来比较复杂
比如得分段存储数据
自己写随机算法
分段中如果数据不足,还要自动切换数据去处理。
一旦某个库存数量低于一定的阈值,停止响应极短时间做库存整理,将其汇总到其他库存中,直到新的库存数量大于阈值,类似GC的标记整理
https://blog.csdn.net/weixin_40001275/article/details/111381196
使用setnx,如果返回1,那我就开始去执行后面的逻辑,如果返回0,那就说明已经被人占用了,我就要继续等待。
当服务器1拿到锁之后,进行了业务处理,完成后,还需要释放锁,
setnx
del
这里会出现的问题,比如redis宕机了,导致key一种存在内存中,出现死锁的问题。
解决的方式:
1、set设置完key之后,直接expire设置key的有效期,超过这个时间就自动释放,避免死锁。
2、通过value判断,服务器1添加的时候,把value设置为当前时间timeout+1秒。服务器2get的时候如果发现已经超过系统当前时间了,说明服务器1没有释放锁,服务器1可能出问题了,然后服务器2在执行删除key的操作,并且执行setnx。这时候又出现问题了,如果这个时候出现了服务器3,会导致两个都拿到锁。
3、需要用getset命令,获取当前的值,并且设置新值
假设服务器2发现key过期了,开始调用 getset 命令,然后用获取的时间判断是否过期,如果获取的时间仍然是过期的,那就说明拿到锁了。
如果没有,则说明在服务2执行getset之前,服务器3可能也发现锁过期了,并且在服务器2之前执行了getset操作,重新设置了过期时间。
那么服务器2就需要放弃后续的操作,继续等待服务器3释放锁或者去监测key的有效期是否过期。 https://blog.csdn.net/weixin_40001275/article/details/111381196
redis雪崩是指缓存的值在某个时间段,缓存集中过期失效,或者Redis宕机,解决方案就是搭建集群,实现redis高可用,还有就是限流降级,缓存失败后,通过加锁或者队列来控制读数据库和写数据库的线程数量。还有就是数据预热,在正式部署前先把可能的数据加载到缓存中,设置不同的失效时间,让缓存失效时间点均匀分布。热点数据永不失效或者随机时间分布均匀。
缓存穿透是指缓存和数据库中都没有数据,(查不到)解决方案就是布隆过滤器,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合就丢弃,避免了对底层系统的查询压力。或者缓存空对象,当存储层没有命中后,返回的空对象缓存起来,并且设置一个过期时间,之后访问会从缓存中去获取,保护了后端数据源。
缓存击穿:设置热点数据永不过期;加互斥锁
没用RabbitMQ之前:
秒杀------>判断是否 有库存,如果没有秒杀失败------->如果有库存,判断是否已经秒杀过了,如果已经有秒杀过的订单,不能重复秒杀-------没有秒杀过,并且有库存,进行 减库存、下订单、写入秒杀订单,这三个操作要保证原子性,在一个事务里面。
问题:
超卖问题
一个用户同时发出两个请求,都能秒杀到,一个用户秒杀到了2个商品,这是我们不允许的,我们要在数据库的表上,user_id和goods_id字段上建一个唯一索引,当有两个相同的订单生成的时候会回滚,
一个商品,同时被两个用户抢到,都减库存下订单,会造成数据库被减成负数,在sql减库存的时候,sql判断只有库存数>0的时候才执行。这样数据库本身会对一条数据加锁,一条执行完毕后,另一条执行的时候,如果库存已经为0了就不会在执行了。
加入RabbitMQ实现异步下单:
秒杀接口优化思路
==减少对数据库的访问==
系统初始化的时候,将商品库存数量加载到redis中,收到秒杀请求后,redis预减库存,库存不足直接返回,否则入队,请求入队,立即返回排队中,请求出队,生成订单,减少库存,客户端轮询是否秒杀成功。
1、系统初始化时,将秒杀商品库存加载到Redis中
2、收到请求,在Redis中预减库存,库存不足时,返回秒杀失败。
3、秒杀成功,将秒杀请求压入消息队列,返回前端消息“排队中”
4、消息出队,生成订单,减少库存。
5、客户端在以上执行过程中,一直轮询是否秒杀成功
秒杀成功后就进行减少商品库存,创建秒杀订单,这两个操作是在一个事务中执行的,有个成功的标志,减库存成功了,才会去执行创建订单的操作。
https://www.cnblogs.com/daofaziran/p/10933221.html
秒杀业务肯定是分布式的多台服务器,第一个请求落到第一台服务器,如果此时第二次请求落到了第二台服务器上,没有分布式session的话,所有的信息就都丢失了。
很多种解决方案,session同步。
粘性session:
以Nginx为例,在upstream模块配置ip_hash属性即可实现。实现方式:配置tomcat,开启tomcat集群功能
服务器session复制(同步):
一个服务器上的session同步到另一个服务器上,但这种方式运用的并不多,因为存在一个性能的问题。
session共享机制
分布式缓存方案粘性session,非粘性:主从复制
session持久到数据库
我的方式是客户端存储一个token,在用户登陆的时候,不同的用户生存不同的令牌token,然后存储到cookie中,这样之后用户请求的时候,客户端都带着这个token,对数据库访问的过程中,不断的上传这个token。用Redis存储
,在Redis中通过token值来获取用户信息
将html存入redis中,如果请求来了,先去redis中取缓存,存在的话,直接返回html,不存在的话,就手动渲染ThymeleafViewResolver和WebContext。可以在redis中设置一个我们能接受的过期时间比如60s。
上面的方法一般不常用,而是通过静态化处理,比较常用的技术有Vue,通过静态化处理将页面缓存在客户端浏览器中,不需要与服务器交互就能访问页面。在项目中我使用了JQuery来实现。去掉了Model向前端传值的逻辑,将所有需要的值封装到GoodsDetailVo对象中。
其实就和token一样,getByToken() 根据token值从Redis中获取对象信息。对象缓存是细粒度最小的缓存。
消息队列是应用间的一种通信方式,消息发送后可以立即返回,由消息系统来保证可靠传输,发布者只需要发布到MQ中,不用管谁来取,使用者只管从MQ中取,不用管谁发布的,双方不知道对方的存在。
秒杀按钮点击后,扣减库存,生成订单,这些业务逻辑放在一块执行,但随着订单和业务的增长,我们需要把不需要立即生效的操作拆分出来,异步执行,业务解耦,
https://www.cnblogs.com/myseries/p/11891132.html
https://blog.csdn.net/weixin_38035852/article/details/81191622
用户恶意下单,知道了URL地址,不停的刷,怎么办?
通过隐藏URL地址来避免这个问题的,当访问秒杀接口的时候,先从后端随机生成一个字符串,保存到redis中,前端在根据这个字符串拼接的地址进行访问,后端把这个字符串和redis的做比对,一致的话,才能继续访问。
秒杀接口提前暴露出去,可以被人通过直接访问url提前秒杀,那我做一个时间校验不就可以了吗?不到时间不能秒杀。但是这样也是存在问题的,我可以写一个程序,不断的获取最新的时间,可以达到毫秒级,在00毫秒秒杀开始的时候,进行访问,这样肯定比人工点击成功率高,可以一毫米发送N次请求,这样可能会一下把商品秒杀光。这样怎么避免呢?
就是url动态化,连写代码的都不知道url是什么,利用MD5加密随机字符串,通过前端代码获取url后台校验之后才能通过。
具体思路:
1、秒杀之前,先请求服务端的一个地址,/getpath,用来获取秒杀地址,传入商品id,服务端随机数MD5加密后作为Path存入缓存中,将这个path返回前端。
2、获取path后,前端用path拼接url再去请求domiaosha的服务。
3、domiaosha后端秒杀接口接收到了这个path后与缓存中的比对。如果一样,进行秒杀逻辑,不一致,抛出业务异常,非法请求。
这种操作可以避免用户在获取token的情况下,不断调用秒杀地址接口,刷单的恶意请求。只有真正的点击秒杀按钮,才会根据商品和用户id生成对应的秒杀地址。
但是这样仍然不能解决利用按键精灵或者机器人频繁的点击按钮的操作,为了进一步的提高系统的安全性,我们还需要规定一个用户在一定时间内可以请求的次数。或者加入图形验证码来提高系统高并发。
接口限流防刷怎么实现的?
通过拦截器实现,我自定义了一个注解,它的功能就是标注在方法上,规定它单位时间内的访问次数,如果超过这个要求的话,就会被拦截。
拦截器继承了HandlerInterceptorAdapter,重写preHandle方法,在该方法中,将访问的次数同步到redis中,键值对也设置有效期,最后把拦截器配置到项目中,继承WebMvcConfigurerAdapter,重写addInterceptors()方法将自定义的拦截器配置进去。
接口限流防刷,在规定时间内固定访问的次数。实现的方式是在需要限制防刷的方法上添加自定义注解,通过自己的拦截器对注解进行处理限制访问的次数,继承HandlerInterceptorAdapter,重写preHandle方法
https://www.cnblogs.com/xuwc/p/9123078.html
计数器算法:最开始设置一个计数器count=0,来一个请求count++,请求不稳定,突刺现象
漏桶算法:限制请求的速率,准备队列,通过线程池执行,弊端:无法应对短时间的突发流量
令牌桶算法:对漏桶算法的改进,限制请求速率的同时,还允许一定程度的突发调用
https://blog.csdn.net/kavito/article/details/91403659
Publish/subscribe发布订阅模式 交换机Fanout广播模式
routing路由模型 交换机类型 direct
Topics通配符模式 交换机类型topics
1、采用workqueue,多个消费者监听同一队列
2、接收到消息以后,通过线程池,异步消费
1、消费者的ACK机制。可以防止消费者丢失消息。
但是如果消费者消费之前,MQ宕机了,消息就没了
2、可以将消息进行持久化。前提是队列、Exchange都持久化
消息确认机制(ACK)
当消费者获取信息后,会向RabbitMQ发送回执ACK,告知消息已经被接收,不过这种回执ACK分两种情况:
- 自动ACK:消息一旦被接收,消费者自动发送ACK
- 手动ACK:消息接收后,不会发送ACK,需要手动调用
自动ACK存在问题:
消费者抛出异常,但是消息依然被消费,实际上我们还没获取到消息。
MQ收到消息,将消息进行持久化,在存储中新增一条记录
RabbitMQ是采用Erlang语音实现的AMQP(高级消息队列协议)的消息中间件。
有一个生产者和消费者,通过Exchange绑定对应的队列,由生产者发送消息到服务节点,服务节点再发送给消费者。