1 乐观锁 VS 悲观锁
锁 适用场景 解释
乐观锁 适合写操作多的场景 乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
悲观锁 适合读操作多的场景 悲观锁在 Java 中使用 synchronized 关键字和 Lock 来实现。
悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。在 Java 中使用 synchronized 关键字和 Lock 来实现。
乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,因此在获取数据的时候不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。在 Java 中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
2 自旋锁 VS 适应性自旋锁
自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
锁 相同 不同
自旋锁 任何时候只能有一个获得锁 如果资源已经被占用,资源申请者只能进入睡眠状态。等到锁被释放否再唤醒资源申请者,CPU再切换到新的资源申请者。
互斥锁 任何时候只能有一个获得锁 如果资源已经被占用,资源申请者不会进入睡眠状态,而是循环等待看锁是否被释放。如果锁被释放就会立即获得锁,而不需要经过唤醒和CPU上下文切换。
优点
在一些同步资源的锁定时间很短的业务中,线程挂起和恢复现的时间会比自旋的时间要长的适合使用自旋锁。
缺点
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。