锁机制
1.按照实现方式
按照实现方式分为乐观锁和悲观锁,并不是真实存在的锁,而是设计思想
-
乐观锁
认为资源和数据不会被别人修改,读取不会上锁,但是写入操作会判断是否被修改过
使用场景:
高性能、高可用、高并发的场景,适用于写操作比较少的场景,冲突比较少
实现方案:- 版本号机制
版本号机制是在数据表中加上一个 version 字段来实现的,表示数据被修改的次数,当执行写操作并且写入成功后,version = version + 1,当线程A要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功 - CAS实现
CAS(Compare And Swap),不使用锁实现变量同步,也叫非阻塞同步
三个元素- 需要读写的内存值V
- 进行比较的值A
- 拟写入的新值B
当且仅当A与V值相同时,将内存值V修改为B,否则什么也不做
Java中的
java.util.concurrect.atomic
下的原子变量就是通过CAS实现的,JDK1.5中JUC(java.util.concurrect
)包建立在CAS基础上- 乐观锁的缺点
- ABA问题
如果一个变量第一次读取为A,第二次读取也为A,并不代表没被修改过,有可能是A->B->A,JDK1.5之后AtomicStampedReference
类就提供了此种能力,其中的compareAndSet
方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。DCAS
就是在CAS基础上增加引用的表示修改次数的标记符。 - 循环开销大
乐观锁在进行写操作判断是否能写入成功,如果不成功会触发等待->重试
机制,是一种自旋锁,适用于短期获取不到锁的情况,若长期获取不到,性能开销较大,如果锁竞争严重,CAS自旋的概率较大,此时应该有效考虑Synchronized
- ABA问题
- 版本号机制
-
悲观锁
一种悲观思想,认为数据可能会被其他人修改,所以在持有数据时把数据或资源锁住,其他线程请求资源会阻塞,直到悲观锁释放,数据库中的行锁,表锁,读锁,写锁都是在操作之前上锁
Java中Synchronization
和ReetranLock
等排他锁(独占锁)也是悲观锁
使用场景:
因为对读写都要枷锁,性能较低,高安全性的场景,适用于写操作比较多的场景,冲突比较多
2. 按照冲突的处理方式
-
自旋锁
定义:当一个线程尝试获取一个锁,若锁被占用,则职该线程将会等待,间隔一段时间再次尝试获取,进入循环->等待
机制。
优势:如果持有锁的线程可以在短时间释放,那么等待竞争的线程不需要做内核态/用户态的切换进入阻塞态,他们只需要等待持有锁的线程释放,避免了用户态内核态切换的消耗(避免了进程调度和线程切换
),所以操作系统大量使用自旋锁。
劣势:如果长时间上锁,自旋锁非常消耗性能,则指了其他线程的调度运行。线程持有锁的时间越长,则该线程被OS调度程序中断的风险越大。如果发生中断,其他线程将保持自旋,而持有锁的线程并不打算释放锁,导致无限推迟,直到该线程完成工作。解决该问题可以给自旋锁是指自选时间,时间到了就释放自旋锁,自旋锁会占用CPU资源,影响系统性能。 -
互斥锁
定义:当一个线程获取一个锁,若锁被占用,则该线程进入阻塞态,等待重新调度
自旋锁不适用于线程长时间持有CPU资源的情况,会加剧系统负担。但互斥锁在加锁失败后进入阻塞态,此时需要进行线程上下文切换,消耗系统资源。 -
原子操作
原子操作:不能中断的操作,在此操作过程中,CPU不会再去其他的针对该值的操作,通过CPU指令实现,其它的同步技术常常依赖于原子操作