锁的定义在计算机程序中锁用于独占资源,获取到锁才可以操作对应的资源。
锁的实现锁在计算机底层的实现,依赖于cpu提供的cas指令(compare and swsp),对于一个内存地址,会比较原值以及尝试去修改的值,通过值是否修改成功,来表示是否强占到了这个锁。
jvm中的锁jvm中,有2个常用的锁
synchronizedsynchronized是java提供的关键字锁,可以锁对象,类,方法。
在jdk1.6以后,对synchronized进行了优化,增加了偏向锁和轻量锁模式,现在synchronized锁的运行逻辑如下:
在初始加锁时,会增加偏向锁,即“偏向上一次获取该锁的线程”,在偏向锁下,会直接cas获取该锁。该模式大大提高了单线程反复获取同一个锁的吞吐情况,在java官方看来,大部分锁的争抢都发生在同个线程上。
如果偏向锁cas获取失败,说明当前线程与偏向锁偏向的线程不同,偏向锁就会升级成轻量锁,轻量锁的特点就是通过自旋cas去获取锁。
如果自旋获取失败,那么锁就会升级成重量锁,所有等待锁的线程将被jvm挂起,在锁释放后,再由jvm统一通知唤醒,再去尝试cas锁,如果失败,继续挂起。
很显然,偏向锁设计的目的是“在java官方看来,对同一个锁的争抢大部分都发生在同个线程上”。
轻量锁设计的目的是“在短期内,锁的争抢通过自旋cas就可以获取到,短时间内的cpu自旋消耗小于线程挂起再唤醒的消耗”。
重量锁就是最初优化前的synchronized的逻辑了。
reentrantlock说到reentrantlock,就不得不说到juc里的aqs了。
aqs全称abstractqueuesynchronizer,几乎juc里所有的工具类,都依赖aqs实现。
aqs在java里,是一个抽象类,但是本质上是一种思路在java中的实现而已。
aqs的实现逻辑如下:
构造一个队列
队列中维护需要等待锁的线程
头结点永远是持有锁(或持有资源)的节点,等待的节点在头结点之后依次连接。
头结点释放锁后,会按照顺序去唤醒那些等待的节点,然后那些节点会再次去尝试获取锁。
在synchronized锁优化以后,aqs的本质与synchronized并没有太大不同,两者的性能也并没有太大差距了,所以aqs现在的特点是:
是在java api层面实现的锁,所以可以实现各种并发工具类,操作也更加灵活
因为提供了超时时间等机制,操作灵活,所以不易死锁。如果发生死锁,它将更难以排查,因为jstack将不会显示死锁标识。
可以实现公平锁,而synchronized必定是非公平锁。
因为是javaapi层实现的锁,所以可以响应中断。
到这里你会发现,其实reentrantlock可以说是synchronized在javaapi层的实现。
mysql 锁共享锁(s) 与排它锁(x)作用范围这两种锁都包括行级锁和表级锁。
获取共享锁时,如果该数据被其他事务的排它锁锁住,则无法获取,需要等待排它锁释放。
意向锁作用范围意向锁为表锁,在获取表锁之前,一定会检查意向锁。
意图锁定协议如下:
在事务获得表中某行的共享锁之前,它必须首先获得表上的 is 锁或更强的锁。
在事务获得表中行的排他锁之前,它必须首先获得表的 ix 锁。
在获取任意表锁的共享锁或排它锁之前,一定会检查该表上的共享锁。
表锁以及意向锁的互斥规则如下:
x ix s is
x conflict conflict conflict conflict
ix conflict compatible conflict compatible
s conflict conflict compatible compatible
is conflict compatible compatible compatible
意向锁的作用在于:在获取表锁时,可以通过意向锁来快速判断能否获取。
因为获取行级锁时,会先获取对应的意向锁,这样另外的事务在获取表锁时就可以通过意向锁快速的判断,而不需要每行去扫描。
特别注意的是,意向锁是可以叠加的,即会存在多个,如t1事务获取了意向锁ix1和行级锁x1,t2事务依旧可以获取意向锁ix2和行级锁x2,所以仅在获取表级锁之前,才会检查意向锁。
记录锁记录锁生效在索引上,用以在select c1 from t where c1 = 10 for update时保护该行数据不被其他事务更改。
记录锁在没有索引时依旧会生效,因为innodb会为每张表创建一个隐藏的索引。
记录锁是最基本的行锁。
间隙锁间隙锁生效在索引上,用于锁定索引值后的行,防止插入,在select from table where index=? for update时会生效,例如index=1,则会锁住index=1索引节点相关的行,防止其他事务插入数据。
但是并不会防止update语句,哪怕update的数据不存在。
next-key locks这个锁是记录锁和间隙锁的组合,简而言之在select from table where index=? for update时,既会有间隙锁防止insert,也会有记录锁在index上防止这一条数据的update和delete。这个next-key只是对这两种锁的一种概括,因为这两种锁在select for update时通常会一起出现。
insert intention locks插入意向锁,和意向锁类似。不过是特殊的间隙锁,并不发生在select for update,而是在同时发生insert时产生,例如在两个事务同时insert索引区间为[4,7]时,同时获得该区间的意向锁,此时事务不会阻塞,例如a:insert-5,b:insert-7,此时不会阻塞两个事务。
插入意向锁是一个特殊的间隙锁,是为了防止正常间隙锁锁区间的情况下,insert频繁阻塞而设计的,例如a:insert-5,b:insert-7,如果没有插入意向锁,那么5和7都要去尝试获取间隙锁,此时第二个事务就会被阻塞,但是通过插入意向锁,第二个事务就不会被阻塞,只有到插入的行确实冲突,才会被阻塞。
auto-inc locks自增锁,这个锁很明显是表级insert锁,为了保证自增主键的表的主键保持原子自增。
对于锁这个东西,大家应该多去理解各种锁设计运行的原理和模型,这样在加深理解后,在使用起来才会更加深入和透彻。
常见锁使用的场景和用法double check众所周知,mysql的事务对防止重复插入并没有什么卵用,唯一索引又存在很多缺点,业务上最好不要使用,所以一般来说防止重复插入的通用做法就是使用分布式锁,这就有一种比较常用的写法。
final weekendnoticereadcountdo weekendnoticereadcountdo = weekendnoticereadrepositoryservice.selectbynoticeid(noticerequestdto.getnoticeid());if (weekendnoticereadcountdo == null) { final string lockkey = redisconstant.lock_weekend_read_count_insert + ":" + noticerequestdto.getnoticeid(); clusterlock lock = clusterlockfactory.getclusterlockredis( redisconstant.redis_key_prefix, lockkey ); if (lock.acquire(redisconstant.redis_lock_default_timeout)) { //double check final weekendnoticereadcountdo weekendnoticereadcountdo = weekendnoticereadrepositoryservice.selectbynoticeid(noticerequestdto.getnoticeid()); if (weekendnoticereadcountdo == null) { try { lock.execute(() -> { weekendnoticereadcountdo readcountdo = new weekendnoticereadcountdo(); readcountdo.setnoticeid(noticerequestdto.getnoticeid()); readcountdo.setreadcount(1l); readcountdo.setcreatetime(new date()); readcountdo.setupdatetime(new date()); weekendnoticereadrepositoryservice.insert(readcountdo); return true; }); } catch (apiexception err) { throw err; } catch (exception e) { log.error("插入", e); throw new apiexception(errorenum.server_error.getcode(), "服务端出错"); } } else { weekendnoticereadrepositoryservice.noticecountadd(weekendnoticereadcountdo); } } else { log.warn("redis锁获取超时,key:{}", lockkey); throw new apiexception(errorenum.server_error.getcode(), "服务器繁忙,请稍后重试"); }}
在获取到锁之后,可能是经过等待才获取到的锁,此时上一个释放锁的线程可能已经插入了数据了,所以在锁内部,依旧要再次校验一下数据是否存在。
这种写法适合大多数需要唯一性的写场景。
避免死锁如何避免死锁?最简单有效的方法就是:**不要在锁里再去获取锁,简而言之就是锁最好单独使用,不要套娃。
也要注意一些隐性锁,比如数据库。
事务a:
插入[5,7],插入意向锁。
select for update更新[100,150],间隙锁。
事务b:
select for update更新[90,120],间隙锁。
插入[4,6],插入意向锁。
此时在并发场景下,就可能会出现a持有了[5,7]的间隙锁,在等待事务b[90,120]的间隙锁,事务b也一样,就死锁了。
**
顺带谈谈并发场景下常见的问题读写混乱在写业务代码,定义一些工具类或者缓存类的时候,很容易疏忽而发生类似的问题。
比如构建一个static缓存,没有使用concurrenthashmap中的putifabsent等方法,也没有加锁去构建,导致上面的线程刚put了,下面的线程就删掉了,或者重复构建2次缓存。
redis或者一些并发操作释放锁或者资源,没有检查是否是当前线程持有这点在redis锁的示例代码也讲到了。
线程a获取到锁,此时b,c在等待,然后a执行时间过长,导致锁超时被自动释放了,此时b获取到了锁,在快乐的执行,然后a执行完了之后,释放锁时没有判断是否还是自己持有,导致b持有的锁被删除了,此时c又获取到了锁,bc同时在执行。
以上就是java与mysql锁相关知识点有哪些的详细内容。
