欢迎点击主页查看更多内容....
Redis 数据结构 压缩列表和跳跃表的区别
压缩列表(ziplist)本质上就是一个字节数组,是 Redis 为了节约内存而设计的一种线性
数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指
针,从而达到快速访问节点的目的。跳跃表支持平均 O(logN)、最坏 O(N)复杂度的
节点查找,还可以通过顺序性操作来批量处理节点
1.redis的hash怎么实现的?(实现原理)rehash过程
redis初始创建hash表,有序集合,链表时, 存储结构采用一种ziplist的存储结构, 这种结构内存排列更紧密, 能提高访存性能.
hash_max_ziplist_entries和hash_max_ziplist_value值作为阀值,hash_max_ziplist_entries表示一旦ziplist中元素数量超过该值,则需要转换为dict结构;hash_max_ziplist_value表示一旦ziplist中数据长度大于该值,则需要转换为dict结构。
哈希等价于Java语言的HashMap或者是Python语言的字典(Dict)
redis hash 的内部结构.第一维是数组,第二维是链表.组成一个 hashtable.
在 Java 中 HashMap 扩容是个很耗时的操作,需要去申请新的数组,为了追求高性能,Redis 采用了渐进式 rehash 策略.这也是 hash 中最重要的部分.
在扩容的时候 rehash 策略会保留新旧两个 hashtable 结构,查询时也会同时查询两个 hashtable.Redis会将旧 hashtable 中的内容一点一点的迁移到新的 hashtable 中,当迁移完成时,就会用新的 hashtable 取代之前的.当 hashtable 移除了最后一个元素之后,这个数据结构将会被删除.
https://juejin.im/post/5cfe6383e51d45599e019d8f
与java的hashmap的rehash区别
个人理解:hashmap的rehash是一次性拷贝的,不同的是,Redis的字典只能是字符串,另外他们rehash的方式不一样,因为Java的HashMap的字典很大时,rehash是个耗时的操作,需要一次全部rehash。Redis为了追求高性能,不能堵塞服务,所以采用了渐进式rehash策略。
rehash的详细步骤
https://www.cnblogs.com/meituantech/p/9376472.html
与ConcurrentHashMap扩容的策略比较?
ConcurrentHashMap采用的扩容策略为: “多线程协同式rehash“。
1.扩容所花费的时间对比: 一个单线程渐进扩容,一个多线程协同扩容。在平均的情况下,是ConcurrentHashMap 快。这也意味着,扩容时所需要 花费的空间能够更快的进行释放。
2.读操作,两者性能相差不多。
3.写操作,Redis的字典返回更快些,因为它不像ConcurrentHashMap那样去帮着扩容(当要写的桶位已经搬到了newTable时),等扩容完才能进行操作。
4.删除操作,与写一样。
http://xytschool.com/resource/236.html
redis如何保证高可用
保证redis高可用机制需要redis主从复制、redis持久化机制、哨兵机制、keepalived等的支持。
主从复制的作用:数据备份、读写分离、分布式集群、实现高可用、宕机容错机制等。
redis主从复制原理
首先主从复制需要分为两个角色:master(主) 和 slave(从) ,注意:redis里面只支持一个主,不像Mysql、Nginx主从复制可以多主多从。
(1)redis的复制功能是支持多个数据库之间的数据同步。一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。
(2)通过redis的复制功能可以很好的实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。
https://blog.csdn.net/itcats_cn/article/details/82428716
说说redis的持久化机制,为啥不能用redis做专门的持久化数据库存储?
个人理解:强一致性的数据是不适合放在缓存中的。另外MySQL对事务的支持也是redis本身不能达到的,需要单独实现
一般不是说redis or MySQL,而是redis+MySQL
https://blog.csdn.net/u011784767/article/details/76824822
为什么Redis进行RDB持久化数据时,新起一个进程而不是在原进程中起一个线程来持久化数据
(1)Redis RDB持久化机制会阻塞主进程,这样主进程就无法响应客户端请求。
(2)我们知道Redis对客户端响应请求的工作模型是单进程和单线程的,如果在主进程内启动一个线程,这样会造成对数据的竞争条件,为了避免使用锁降低性能。基于以上两点这就是为什么Redis通过启动一个进程来执行RDB了
---单线程的redis为什么这么快
(1)纯内存操作
(2)单线程操作,避免了频繁的上下文切换
(3)采用了非阻塞I/O多路复用机制
1
Redis的数据类型以及使用场景
(1)String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。
一般做一些复杂的计数功能的缓存。
(2)hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,
就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
(3)list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,
做基于redis的分页功能,性能极佳,用户体验好。
(4)set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?
因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再启一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
(5)sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。sorted set可以用来做延时任务。最后一个应用就是可以做范围查找
redis的过期策略以及内存淘汰机制
redis采用的是定期删除+惰性删除+内存淘汰策略。
[2020年6月29日17:25:36在平时的项目中测试,不定期会产生无用token的key数据,平时可以进行模糊删除]
缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方案:
(一)利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
(二)采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,
异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
(三)提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。
迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
解决方案:
(一)给缓存的失效时间,加上一个随机值,避免集体失效。
(二)使用互斥锁,但是该方案吞吐量明显下降了。
(三)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。
自己做缓存预热操作。然后细分以下几个小点
1 从缓存A读数据库,有则直接返回
2 A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
3 更新线程同时更新缓存A和缓存B。
如何解决redis的并发竞争key问题
分析:这个问题大致就是,同时有多个子系统去set一个key。这个时候要注意什么呢?大家思考过么。
需要说明一下,博主提前百度了一下,发现答案基本都是推荐用redis事务机制。博主不推荐使用redis的事务机制。
因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,
这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。
回答:如下所示
(1)如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
(2)如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA-->valueB-->valueC的顺序变化。这种时候我们在数据写入数据库的时候,
需要保存一个时间戳。假设时间戳如下
系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。
redis分页
HSCAN testHash “0” count 10
注:测试field数量在22条时(没有测试Redis中Hash使分页生效时的field数量的下限),分页未生效。
mysql 执行一个 sql 的过程
执行完毕之后有一个缓存的过程
https://www.cnblogs.com/luoying/p/12073812.html
MySQL分页limit速度太慢的优化方法
1.子查询优化法
先找出第一条数据,然后大于等于这条数据的id就是要获取的数据
缺点:数据必须是连续的,可以说不能有where条件,where条件会筛选数据,导致数据失去连续性
2.limit限制优化法
把limit偏移量限制低于某个数
3.where条件先过滤后分页
wait notify 为什么要搭配使用?
单独调用会报异常
只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。因为程序验证通常是在对象的同步方法或同步代码块中调用它们的。如果尝试在未获取对象锁时调用这三个方法,
“java.lang.IllegalMonitorStateException:current thread not owner”。
底层把对象作为一个监视器
栈会溢出吗?什么时候溢出?方法区会溢出吗?
栈是线程私有的,它的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来
存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,
对象引用类型。如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError 异常,方法递归调用产生这种结果。如果 Java 虚拟机栈可以动态扩展,
并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时
候没有足够的内存去创建对应的虚拟机栈,那么 Java 虚拟机将抛出一个 OutOfMemory 异
常。(线程启动过多)。
方法区会发生溢出。
HotSpot jdk1.7 之前字符串常量池是方法区的一部分,方法区叫做“永久代”,在 1.7 之前
无限的创建对象就会造成内存溢出,提示信息:PermGen space 而是用 jdk1.7 之后,开始逐
步去永久代,就不会产生内存溢出。
方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,
如果动态生成大量的 Class 文件,也会产生内存溢出。常见的场景还有:大量 JSP 或动态产生
JSP 文件的应用(JSP 第一次运行时需要编译为 java 类)、基于 OSGi 的应用(即使是同一个
类文件,被不同的类加载器加载也会视为不同的类)
redis排行榜代码
public Object countPoint(Long pointId,String ponitList, Long userId) {
String key = RedisConstants.get(pointId.toString());
String userIdStr = userId.toString();
boolean flag = redisService.isMember(key, userIdStr);
DetailVo vo = DetailVo.builder()
.pointId(pointId)
.likeCreateTime(new Date())
.likeUserId(userId)
.likedUserId(likedUserId)
.build();
Map resultMap = new HashMap();
if (flag) {
redisService.srem(key, userIdStr);
vo.setType(2);
//减分
redisService.incrScoreZset(ponitList,likedUserId.toString(),-1);
resultMap.put("no", 0);
} else {
redisService.sadd(key, userIdStr);
//加分
redisService.incrScoreZset(ponitList,likedUserId.toString(),1);
vo.setType(1);
resultMap.put("yes", 1);
}
//发送消息
mQProducer.sendUpdateUp(vo);
resultMap.put("Number", redisService.scard(key));
return resultMap;
}
请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?
Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;
接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的
参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符
串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个
<select>、<insert>、<update>、<delete>标签,都会被解析为一个
MapperStatement 对象。
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯
一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id 为
findStudentById 的 MapperStatement。
Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻
找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK
动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而
执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。
Mybatis 是如何进行分页的?分页插件的原理是什么?
Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内
存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分
页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件
的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物
理分页语句和物理分页参数。
Mybatis 的一级、二级缓存
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为
Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就
将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap
存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,
如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要
实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置
<cache/> ;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存
Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将
被 clear。
Redis面试专题
redis 和 memcached 什么区别?为什么高并发下有时单线程的 redis 比多线程的
memcached 效率要高?
区别:
1.mc 可缓存图片和视频。rd 支持除 k/v 更多的数据结构;
2.rd 可以使用虚拟内存,rd 可持久化和 aof 灾难恢复,rd 通过主从支持数据备份;
3.rd 可以做消息队列。
原因:mc 多线程模型引入了缓存一致性和锁,加锁带来了性能损耗。
假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用 keys 指令可以扫出指定模式的 key 列表。
对方接着追问:如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问
题?
这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一
段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指
令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客
户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
Synchronized 锁升级过程
首先,synchronized 是什么?我们需要明确的给个定义——同步锁,没错,它就是把锁。
可以用来干嘛?锁,当然当然是用于线程间的同步,以及保护临界区内的资源。我们知道,锁是个非常笼统的概念,像生活中有指纹锁、密码锁等等多个种类,那 synchronized 代表的锁具体是把什么锁呢?
答案是—— Java 内置锁。在 Java 中,每个对象中都隐藏着一把锁,而 synchronized 关键字就是激活这把隐式锁的把手(开关)。
先来简单了解一下 synchronized,我们知道其共有 3 种使用方式:
关于扫描时markword,可以去了解下对象在JVM中的结构,这里简单说明,每个对象都会有一个对象头markword,这块区域可以存放hashcode和锁信息以及GC信息
Synchronized 的使用
修饰静态方法:锁住当前 class,作用于该 class 的所有实例
修饰非静态方法:只会锁住当前 class 的实例
修饰代码块:该方法接受一个对象作为参数,锁住的即该对象
使用方法就不在这里赘述,可自行搜索其详细的用法,这不是本篇文章所关心的内容。
知道了 synchronized 的概念,回头来看标题,它说的锁升级到底是个啥?对于不太熟悉锁升级的人来说,可能会想:
所谓锁,不就是啪一下锁上就完事了吗?升级是个什么玩意?这跟打扑克牌也没关系啊。
对于熟悉的人来说,可能会想:
不就是「无锁 ==> 偏向锁 ==> 轻量级锁 ==> 重量级锁 」吗?
你可能在很多地方看到过上面描述的锁升级过程,也能直接背下来。但你真的知道无锁、偏向锁、轻量级锁、重量级锁到底代表着什么吗?这些锁存储在哪里?以及什么情况下会使得锁向下一个 level 升级?
想知道答案,我们似乎必须先搞清楚 Java 内置锁,其内部结构是啥样的?内置锁又存放在哪里?
答案在开篇提到过——在 Java 对象中。
那么现在的问题就从「内置锁结构是啥」变成了「Java 对象长啥样」。
对象结构
从宏观上看,Java 对象的结构很简单,分为三部分:
Java 对象结构
从微观上看,各个部分都还可以深入展开,详见下图:
Java 详细对象结构
接下来分别深入讨论一下这三部分。
对象头
从脑图中可以看出,其由 Mark Word、Class Pointer、数组长度三个字段组成。简单来说:
Mark Word:主要用于存储自身运行时数据
Class Pointer:是指针,指向方法区中该 class 的对象,JVM 通过此字段来判断当前对象是哪个类的实例
数组长度:当且仅当对象是数组时才会有该字段
Class Pointer 和数组长度没什么好说的,接下来重点聊聊 Mark Word。
Mark Word 所代表的「运行时数据」主要用来表示当前 Java 对象的线程锁状态以及 GC 的标志。而线程锁状态分别就是无锁、偏向锁、轻量级锁、重量级锁。
所以前文提到的这 4 个状态,其实就是 Java 内置锁的不同状态。
在 JDK 1.6 之前,内置锁都是重量级锁,效率低下。效率低下表现在
而在 JDK 1.6 之后为了提高 synchronized 的效率,才引入了偏向锁、轻量级锁。
随着锁竞争逐渐激烈,其状态会按照「无锁 ==> 偏向锁 ==> 轻量级锁 ==> 重量级锁 」这个方向逐渐升级,并且不可逆,只能进行锁升级,而无法进行锁降级。
接下来我们思考一个问题,既然 Mark Word 可以表示 4 种不同的锁状态,其内部到底是怎么区分的呢?(由于目前主流的 JVM 都是 64 位,所以我们只讨论 64 位的 Mark Word)接下来我们通过图片直观的感受一下。
(1)无锁
无锁
这个可以理解为单线程很快乐的运行,没有其他的线程来和其竞争。
(2)偏向锁
偏向锁
首先,什么叫偏向锁?举个例子,一段同步的代码,一直只被线程 A 访问,既然没有其他的线程来竞争,每次都要获取锁岂不是浪费资源?所以这种情况下线程 A 就会自动进入偏向锁的状态。
后续线程 A 再次访问同步代码时,不需要做任何的 check,直接执行(对该线程的「偏爱」),这样降低了获取锁的代价,提升了效率。
看到这里,你会发现无锁、偏向锁的 lock 标志位是一样的,即都是 01,这是因为无锁、偏向锁是靠字段 biased_lock 来区分的,0 代表没有使用偏向锁,1 代表启用了偏向锁。为什么要这么搞?你可以理解为无锁、偏向锁在本质上都可以理解为无锁(参考上面提到的线程 A 的状态),所以 lock 的标志位都是 01 是没毛病的。
PS:这里的线程 ID 是持有当前对象偏向锁的线程
(3)轻量级锁
轻量级锁
但是,一旦有第二个线程参与竞争,就会立即膨胀为轻量级锁。企图抢占的线程一开始会使用自旋:
的方式去尝试获取锁。如果循环几次,其他的线程释放了锁,就不需要进行用户态到内核态的切换。虽然如此,但自旋需要占用很多 CPU 的资源(自行理解汽车空档疯狂踩油门)。如果另一个线程 一直不释放锁,难道它就在这一直空转下去吗?
当然不可能,JDK 1.7 之前是普通自旋,会设定一个最大的自旋次数,默认是 10 次,超过这个阈值就停止自旋。JDK 1.7 之后,引入了适应性自旋。简单来说就是:这次自旋获取到锁了,自旋的次数就会增加;这次自旋没拿到锁,自旋的次数就会减少。
(4)重量级锁
重量级锁
上面提到,试图抢占的线程自旋达到阈值,就会停止自旋,那么此时锁就会膨胀成重量级锁。当其膨胀成重量级锁后,其他竞争的线程进来就不会自旋了,而是直接阻塞等待,并且 Mark Word 中的内容会变成一个监视器(monitor)对象,用来统一管理排队的线程。
这个 monitor 对象,每个对象都会关联一个。monitor 对象本质上是一个同步机制,保证了同时只有一个线程能够进入临界区,在 HotSpot 的虚拟机中,是由 C++ 类 ObjectMonitor 实现的。
那么 monitor 对象具体是如何来管理线程的?接下来我们看几个 ObjectMonitor 类关键的属性:
ContentionQueue:是个队列,所有竞争锁的线程都会先进入这个队列中,可以理解为线程的统一入口,进入的线程会阻塞。
EntryList:ContentionQueue 中有资格的线程会被移动到这里,相当于进行一轮初筛,进入的线程会阻塞。
Owner:拥有当前 monitor 对象的线程,即 —— 持有锁的那个线程。
OnDeck:与 Owner 线程进行竞争的线程,同一时刻只会有一个 OnDeck 线程在竞争。
WaitSet:当 Owner 线程调用 wait() 方法被阻塞之后,会被放到这里。当其被唤醒之后,会重新进入 EntryList 当中,这个集合的线程都会阻塞。
Count:用于实现可重入锁,synchronized 是可重入的。
对象体
对象体包含了当前对象的字段和值,在业务中u l是较为核心的部分。
对齐字节
就是单纯用于填充的字节,没有其他的业务含义。其目的是为了保证对象所占用的内存大小为 8 的倍数,因为HotSpot VM 的内存管理要求对象的起始地址必须是 8 的倍数。
锁升级
了解完 4 种锁状态之后,我们就可以整体的来看一下锁升级的过程了。
锁的详细升级过程
1.一开始对象是无锁状态的
2.一个线程尝试执行Synchronize代码块时,成功获得对象的锁,通过CAS操作往该对象markword中插入当前线程id, 同时修改偏向锁的标志位 。此时是偏向锁(偏向这个线程的锁,锁计数+1),同一个线程可以重复进入该锁,锁计数+1,执行完毕会锁计数-1,直到锁计数复0,释放锁。
正是因为有记录线程id,所以Synchronized实现了可重入锁的逻辑(简单说就是一个锁的拥有者可以重复的获取自己的锁,而不会产生阻塞问题)
关于扫描时markword,可以去了解下对象在JVM中的结构,这里简单说明,每个对象都会有一个对象头markword,这块区域可以存放hashcode和锁信息以及GC信息
线程 A 进入 synchronized 开始抢锁,JVM 会判断当前是否是偏向锁的状态,如果是就会根据 Mark Word 中存储的线程 ID 来判断,当前线程 A 是否就是持有偏向锁的线程。如果是,则忽略 check,线程 A 直接执行临界区内的代码。
但如果 Mark Word 里的线程不是线程 A,就会通过自旋尝试获取锁,如果获取到了,就将 Mark Word 中的线程 ID 改为自己的;如果竞争失败,就会立马撤销偏向锁,膨胀为轻量级锁。
后续的竞争线程都会通过自旋来尝试获取锁,如果自旋成功那么锁的状态仍然是轻量级锁。然而如果竞争失败,锁会膨胀为重量级锁,后续等待的竞争的线程都会被阻塞。
无锁状态、偏向锁、轻量级锁、重量级锁 ,这是锁膨胀的过程,不可逆,但只有偏向锁可以变回无锁态。
转载博文:
https://cloud.tencent.com/developer/article/2074879#:~:text=%E8%AF%A6%E7%BB%86%E4%BA%86%E8%A7%A3%20Synchronized%20%E9%94%81%E5%8D%87%E7%BA%A7%E8%BF%87%E7%A8%8B%201%20%E4%BF%AE%E9%A5%B0%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%EF%BC%9A%E9%94%81%E4%BD%8F%E5%BD%93%E5%89%8D%20class%EF%BC%8C%E4%BD%9C%E7%94%A8%E4%BA%8E%E8%AF%A5%20class,%E7%9A%84%E6%89%80%E6%9C%89%E5%AE%9E%E4%BE%8B%202%20%E4%BF%AE%E9%A5%B0%E9%9D%9E%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95%EF%BC%9A%E5%8F%AA%E4%BC%9A%E9%94%81%E4%BD%8F%E5%BD%93%E5%89%8D%20class%20%E7%9A%84%E5%AE%9E%E4%BE%8B%203%20%E4%BF%AE%E9%A5%B0%E4%BB%A3%E7%A0%81%E5%9D%97%EF%BC%9A%E8%AF%A5%E6%96%B9%E6%B3%95%E6%8E%A5%E5%8F%97%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E4%BD%9C%E4%B8%BA%E5%8F%82%E6%95%B0%EF%BC%8C%E9%94%81%E4%BD%8F%E7%9A%84%E5%8D%B3%E8%AF%A5%E5%AF%B9%E8%B1%A1
锁优化篇:
JDK1.6引入了大量的优化,如:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁。锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。但是有一点,不可以进行锁降级
一、自旋锁:
线程频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,会给系统带来很大的压力。同时很多锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁,如果释放了,就可以抢到锁。那怎么等待呢?其实就是执行一段无意义的循环,大家是不是瞬间觉得好low,原来就是执行一段for循环,别急着下结论,我们继续来分析
执行一段无意义的循环。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好。但是如果自旋很久都没抢到锁,那自旋就是浪费资源,说的难听点就是占着茅坑不拉屎。所以说,自旋等待的时间或者次数必须要有一个限度,如果超过了定义的时间仍然没有获取到锁,则把它挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;但是无论你怎么调整这些参数,都无法满足不可预知的情况。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
二、适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,叫做自适应自旋锁。他的自旋次数是会变的,我用大白话来讲一下,就是线程如果上次自旋成功了,那么这次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么这次自旋也很有可能会再次成功。反之,如果某个锁很少有自旋成功,那么以后的自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。大家现在觉得没这么low了吧
三、锁消除
锁消除用大白话来讲,就是在一段程序里你用了锁,但是jvm检测到这段程序里不存在共享数据竞争问题,也就是变量没有逃逸出方法外,这个时候jvm就会把这个锁消除掉
我们程序员写代码的时候自然是知道哪里需要上锁,哪里不需要,但是有时候我们虽然没有显示使用锁,但是我们不小心使了一些线程安全的API时,如StringBuffer、Vector、HashTable等,这个时候会隐形的加锁。比如下段代码
复制
public void sbTest(){
StringBuffer sb= new StringBuffer();
for(int i = 0 ; i < 10 ; i++){
sb.append(i);
}
System.out.println(sb.toString());
}
复制
复制
上面这段代码,JVM可以明显检测到变量sb没有逃逸出方法sbTest()之外,所以JVM可以大胆地将sbTest内部的加锁操作消除。
四、锁粗化
众所周知在使用锁的时候,要让锁的作用范围尽量的小,这样是为了在锁内执行代码尽可能少,缩短持有锁的时间,其他等待锁的线程能尽快拿到锁。在大多数的情况下这样做是正确的。但是连续加锁解锁操作,可能会导致不必要的性能损耗,比如下面这个for循环:
锁粗化前:
for (...) {
synchronized (obj) {
// 一些操作
}
}
锁粗化后:
synchronized (this) {
for (...) {
// 一些操作
}
}
复制
复制
大家应该能看出锁粗化大概是什么意思了。就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。即加锁解锁操作会移到for循环之外。
五、偏向锁
当我们创建一个对象时,该对象的部分Markword关键数据如下。
bit fields
是否偏向锁
锁标志位
hash
0
01
从图中可以看出,偏向锁的标志位是“01”,状态是“0”,表示该对象还没有被加上偏向锁。(“1”是表示被加上偏向锁)。该对象被创建出来的那一刻,就有了偏向锁的标志位,这也说明了所有对象都是可偏向的,但所有对象的状态都为“0”,也同时说明所有被创建的对象的偏向锁并没有生效。
不过,当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。
所谓临界区,就是只允许一个线程进去执行操作的区域,即同步代码块。CAS是一个原子性操作
此时的Mark word的结构信息如下:
bit fields
是否偏向锁
锁标志位
threadId
epoch
1
01
此时偏向锁的状态为“1”,说明对象的偏向锁生效了,同时也可以看到,哪个线程获得了该对象的锁。
偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。也就是说:在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:
Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.
如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.
如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
释放锁 偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:
暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态;
撤销偏向锁,恢复到无锁状态或者轻量级锁的状态;
安全点会导致stw(stop the word),导致性能下降,这种情况下应当禁用;
查看停顿–安全点停顿日志
要查看安全点停顿,可以打开安全点日志,通过设置JVM参数 -
XX:+PrintGCApplicationStoppedTime 会打出系统停止的时间,
添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,可以查看到使用偏向锁导致的停顿,时间非常短暂,但是争用严重的情况下,停顿次数也会非常多;
注意:安全点日志不能一直打开:
- 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件如果不在/dev/shm,可能被锁。
- 对于一些很短的停顿,比如取消偏向锁,打印的消耗比停顿本身还大。
- 安全点日志是在安全点内打印的,本身加大了安全点的停顿时间。
所以安全日志应该只在问题排查时打开。
如果在生产系统上要打开,再再增加下面四个参数:
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log
打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,/dev/shm目录(内存文件系统)。
此日志分三部分:
第一部分是时间戳,VM Operation的类型
第二部分是线程概况,被中括号括起来
total: 安全点里的总线程数
initially_running: 安全点时开始时正在运行状态的线程数
wait_to_block: 在VM Operation开始前需要等待其暂停的线程数
第三部分是到达安全点时的各个阶段以及执行操作所花的时间,其中最重要的是vmop
spin: 等待线程响应safepoint号召的时间;
block: 暂停所有线程所用的时间;
sync: 等于 spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时;
cleanup: 清理所用时间;
vmop: 真正执行VM Operation的时间。
可见,那些很多但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。
jvm开启/关闭偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
六、轻量级锁
自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record(Lock Record:JVM检测到当前对象是无锁状态,则会在当前线程的栈帧中创建一个名为LOCKRECOD表空间用于copy Mark word 中的数据),如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。
缺点:同自旋锁相似:如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。
七、重量级锁
轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
当轻量级所经过锁撤销等步骤升级为重量级锁之后,它的Markword部分数据大体如下
bit fields
锁标志位
指向Mutex的指针
10
为什么说重量级锁开销大呢
主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
这就是说为什么重量级线程开销很大的。
互斥锁(重量级锁)也称为阻塞同步、悲观锁
八、总结
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个
线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将
对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
转载博文:https://cloud.tencent.com/developer/article/1698812?from=article.detail.2019347
Mysql json数据查询
1、使用 字段->'$.json属性' 进行查询条件
2、使用 json_extract 函数查询,json_extract(字段, "$.json属性")
3、根据json数组查询,用 JSON_CONTAINS(字段, JSON_OBJECT('json属性', "内容"))
举例:
SELECT * from
test
-- WHERE attributes -> '$.orderInviteCode.inviterUserId' = 310000000780
-- WHERE json_extract(attributes, "$.orderInviteCode.inviterUserId") = 310000000780
Thread
Thread类有7个基本构造函数,当指定线程执行顺序时,可调用start方法,然后调用join方法,其中join的实现如下
public final void join() throws InterruptedException {
join(0);
}
↓
↓
↓
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
java无法销毁一个线程,但是当调用isAlive方法时,返回false则已销毁
为什么放弃了stop方法?
this method is inherently unsafe. Stopping a thread with
* Thread.stop causes it to unlock all of the monitors that it
* has locked (as a natural consequence of the unchecked
* {@code ThreadDeath} exception propagating up the stack). If
* any of the objects previously protected by these monitors were in
* an inconsistent state, the damaged objects become visible to
* other threads, potentially resulting in arbitrary behavior. Many
* uses of {@code stop} should be replaced by code that simply
* modifies some variable to indicate that the target thread should
* stop running. The target thread should check this variable
* regularly, and return from its run method in an orderly fashion
* if the variable indicates that it is to stop running. If the
* target thread waits for long periods (on a condition variable,
* for example), the {@code interrupt} method should be used to
* interrupt the wait.
* For more information, see
* <a href="{@docRoot}/java.base/java/lang/doc-files/threadPrimitiveDeprecation.html">Why
* are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
防止死锁ddd
所以已经在1.2就过期了
说明Thread interrupt() isinterrupted()interrupted 的区别和含义
Thread.interrupt() 设置状态
isInterrupted() 判断 返回Boolean
interrupted 即判断又清除
yield
首先是一个native方法
public static native void yield();
向调度程序提示当前线程愿意让步,使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了.