Java常用锁的分类及实现原理

乐观锁与悲观锁

乐观与悲观的区别跟读写无关,区别在于对数据操作时加锁操作的差异,数据操作包括(读,写,删等);

-乐观锁:总是假设最好的情况,每次拿数据时都认为别人不会修改数据,所以不会上锁,但在更新数据时会判断在更新期间是否有其他人修改数据,可以使用版本号和CAS(compare and swap 比较与交换)算法来实现;(只有更新时会判断是否有其他人更新数据)

-悲观锁:总是假设最坏的情况,每次去拿数据时都认为别人会修改数据,因此都会上锁,这样别人再想拿数据时都会被阻塞,直到获取到锁,比较常见的使用悲观锁的例子:传统的关系型数据库的行锁,表锁等,读锁,写锁;以及Java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现;

-两种锁的使用场景

乐观锁适用于读多写少的场景,因为出现数据冲突的概率会很小,可以省去加锁的开销,(包括线程状态、上下文切换),增加系统的吞吐量;

悲观锁适用于读少写多的场景,在写多的场景中,数据出现冲突的概率会非常大,这时使用乐观锁会不断的进行重试,会增加系统的开销;

-乐观锁的实现方式:版本号机制或者CAS算法

1.版本号机制:

一般是在数据表中加上一个version字段,来表示数据被修改的次数,当数据被修改时,version值会加1,;当某线程对数据做修改时,会将version的版本加1,在将数据更新至数据表时必须要满足修改数据后的版本号大于当前数据表版本号的要求,如果不满足则重试更新操作,直至成功;

2.CAS算法:

Compare and swap 比较与替换,是一种无锁算法,无锁编程即在不使用锁的情况下实现多线程之间的同步,也就是在没有线程被阻塞的情况下实现变量同步,也叫做非阻塞同步;

CAS算法涉及三个操作数:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当V的值(数据表中的数值)等于A(读取数据时的备份数据)时,CAS通过原子方式用新值B来更新V,否则不会执行任何操作,比较和替换是一个原子操作,并且此操作是一个自旋操作,即不断的重试;

-乐观锁的缺点

1.ABA问题

如果变量V在读取时是A值,并在更新时仍然是A值,并不能保证V的值没有被其他线程修改过,因为有可能会出现线程1将V值更新为B,线程2又将V值从B更新为A,CAS操作会误认为起没有更改过;

解决ABA问题很简单,可以结合版本号,每次更新数据时要判断值以及版本号是否一致,当全部一致时才认为没有更改;

2.循环时间长开销大

因为CAS操作是一个自旋操作,所以当一直更新数据失败时,会造成CPU空转,造成资源的浪费;

解决方法:如果底层的JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3.只能保证一个共享变量的原子操作

CAS一次只能保证一个共享变量的原子性,但对多个共享变量进行操作时,CAS就无法保证了;

但从JDK1.5后提供了AtomicReference类来保证引用对象之间的原子性,可以将多个共享变量放入一个对象中,然后利用AtomicReference来保证数据一致;

公平锁与非公平锁

-公平锁:先来先得,锁被占用时进入队列,先获取锁的线程会在锁释放时优先获取锁;

-非公平锁 :首先会判断当前锁是否被释放,如果已释放,则线程立即获取锁,否则进入队列进行等待,等待过程与公平锁相同;

因此当一直没有其他线程来获取锁时,非公平锁队列中的线程获取锁的行为与公平锁相同,都是先来先得;

非公平锁的优势在于:减少挂起状态的线程数量,减少线程被唤起的开销;因为锁被占用的情况下,如果有新线程来获取锁,该线程会被挂起阻塞,因此会额外付出一次被唤醒的开销;

以ReentrantLock的公平锁与非公平锁为例,实现区别在于公平锁在获取锁时需要判断等待队列是否为空或当前线程是否为头结点,如果满足这个条件才可以去获取锁,但非公平锁没有这个条件直接可以获取锁;

可重入锁和非可重入锁

-可重入锁:即当前线程获取锁后,在持有锁的期间仍然可以再次获取该锁;Java中ReentrantLock和Synchronized都是可重入锁;

-非可重入锁:当前线程获取锁后不可再次在持锁期间获取该锁;

非可重入锁有可能会造成死锁;

例如:父类有一个同步方法,子类继承了该类,并且子类方法中会调用父类方法,这时如果是非可重入锁,会一直等待,造成死锁;如果是非可重入锁递归调用都会造成死锁;

独享锁与共享锁

-独享锁:同一时刻只能被一个线程所持有;

-共享锁:同一时刻可被多个线程持有;

典型的例子是ReentrantReadwriteLock 可重入读写锁中的读锁为共享锁,写锁为独享锁;

当有多个读线程在持有锁时,写线程需阻塞直到所有读线程释放锁,当有写线程持有锁时,其他不管是读线程还是写线程都要阻塞等待锁释放;

自旋锁

自旋锁即内部不断轮询加锁、解锁条件的一种锁;

例如:

pubic Class SpinClass {

private AtomicRefrence sign = new AtomicRefrence<>();

public boolean lock () {

Thread current = Thread.CurrentThread();

while (!sign.compareAndSet(null, current)){}

}

public boolean unlock() {

Thread current = Thread.CurrentThread();

while (!sign.compareAndSet(current, null)){}

}

}

compareAndSet(predict value, set value) 该方法是一个原子操作,为了比较当前值是否符合预期,如果符合则设置持有锁的线程为当前线程;因此当使用sign去比较时,当满足sign==null时代表锁没有被其他线程持有,(并且防止其他线程在此期间获取了锁)因此将current的线程赋值给sign,当前线程获得该自旋锁;

自适应自旋锁

自旋锁适用的场景为:同步代码块执行时间较短,为了尽量减少线程状态切换所带来的的开销,因此让线程进入循环,来等待锁的释放,但是如果自旋的次数过多,又会浪费CPU资源,所以java6 新增了适应性自旋锁,根据线程上一次获取该锁的次数来决定当前的自旋次数,如果上一次该线程获取了锁,则当前会增加自旋的次数,否则,如果上一次该线程没有获取到锁,则直接跳过自旋,进入阻塞状态,节省CPU资源;

在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock

偏向锁

偏向的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。

简单的意思就是该锁偏向于某个线程,只有第一次获取时需要使用CAS,但出现多个线程获取该锁时就需要转换为轻量级锁,使用CAS来进行同步;

你可能感兴趣的:(Java常用锁的分类及实现原理)