各种锁
mysql的锁:间隙锁、自增锁、行锁、表锁、页锁、意向锁、
java的锁:重量级锁(syn)
悲观锁、乐观锁、意向锁、
偏向锁、轻量级锁、重量级锁、公平锁、非公平锁、自旋锁、分段锁、可重入锁(递归锁)、不可重入锁(自旋锁)
从思想上可将锁分为:乐观锁(CAS)、悲观锁(syn)
按锁的互斥程度可分为:共享、排他锁
按锁的粒度可分为:行锁、表锁、页锁
按锁是否公平可分为:公平锁、非公平锁
按锁是否可重入分为:可重入锁、不可重入锁
按锁是否阻塞分为:阻塞锁、非阻塞锁
按锁的自旋分为:自旋锁、适应性自旋锁
按锁的竞争程度分为:轻量级锁、重量级锁、偏向锁
减少锁冲突和死锁
尽量使用较低的隔离级别; 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
选择合理的事务大小,小事务发生锁冲突的几率也更小;
给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。
间隙锁:
它封锁索引记录中的间隔,主要目的,就是为了防止其他事务在间隔中插入数据,以导致“幻读”。
SELECT * FROM user WHERE id BETWEEN 8 AND 15 FOR UPDATE;
轻量级锁:
如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
Mark Word是对象头的一部分;每个线程都拥有自己的线程栈(虚拟机栈),记录线程和函数调用的基本信息。二者属于JVM的基础内容,此处不做介绍。
当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。
适合无实际竞争,多个线程交替使用锁;允许短时间的锁竞争场景。
缺点:
同自旋锁相似,如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。
偏向锁:
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让线程获得的锁的代价更低,从而引入偏向锁的。
在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,无竞争且只有一个线程使用锁的情况下,解决使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
适合无竞争,且将来只有第一个申请锁的线程会使用锁的场景。
缺点:
只有第一个申请偏向锁的线程能够返回成功,后续线程都必然失败
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
如果存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。不过这个副作用已经小的多。
如果需要,使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)。
重量级锁:
同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等
适合有实际竞争,且锁竞争时间长的场景
自增锁(Auto-inc Locks) :
一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。
最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行是连续的主键值。
行锁:
行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。不通过索引查询行锁会变表锁
表锁:
表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
悲观锁:
性能低,synchronized是一种悲观锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据,读写互斥。
乐观锁:
性能高,乐观锁严格来说并不是锁,只是通过原子性来保证数据的同步,比如说数据库的乐观锁,就是通过版本控制来实现,CAS通过cpu指令保证操作原子性;乐观的认为在数据更新期间没有其他线程影响,导致会出现线程不同步。
意向锁
意向共享锁(IS Lock)事务想要获得一张表中某几行的共享锁
意向排他锁(IX Lock)事务想要获得一张表中某几行的排他锁
有什么作用 ?
如果表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B需要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。
如果存在意向锁,那么假如事务A在更新一条记录之前,先加意向锁,再加排它锁X锁,事务B先检查该表上是否存在意向锁,存在的意向锁是否与自己准备加的锁冲突,如果有冲突,则等待直到事务A释放,而无须逐条记录去检测。
事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就行了。
加锁的语法为:
select ... lock in share mode; 要设置IS锁;
select ... for update; 要设置IX锁;
公平锁:
多个线程按照申请锁的顺序来获取锁
优点:等待锁的线程不会饿死。
缺点:整体吞吐效率相对非公平锁要低
非公平锁:
优点:可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
分段锁:
ConcurrentHashMap
可重入锁:
比如:递归锁
不可重入锁:
比如:自旋锁
阻塞锁:
非阻塞锁:
自旋锁:
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。
如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁的目标是降低线程切换的成本。
缺点:
单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。
自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。
如果锁被占用的时间很短,自旋等待的效果就非常好。但是如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以自旋等待的时间必须要有一定的限度,自旋超过了限定次数(默认10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
竞争激烈的场景中,应主动禁用自旋锁。
使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。
适应性自旋锁
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。
缺点:
然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值。
避免死锁,这里只介绍常见的三种
在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
总结
内置锁只能沿着偏向锁、轻量级锁、重量级锁的顺序逐渐膨胀
如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。
如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。