浅谈Java中的各种锁

78 阅读4分钟

image.png

悲观锁和乐观锁

悲观锁

image.png 定义

线程每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了

例子

synchronized 和 ReentrantLock

场景

写多读少

乐观锁

image.png

定义

操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据,使用版本号机制和CAS算法实现

例子

java.util.concurrent.atomic包下的原子类

场景

写少读多

独占锁和共享锁

独占锁

image.png

定义

如果一个线程对数据加上独占锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

例子

synchronized和java.util.concurrent(JUC)包中Lock的实现类

共享锁

image.png

定义

锁可被多个线程所持有,如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据

例子

ReentrantReadWriteLock

互斥锁和读写锁

互斥锁

image.png

定义

是独占锁的一种常规实现

读写锁

image.png

定义

是共享锁的一种具体实现

公平锁和非公平锁

公平锁

image.png

定义

多个线程按照申请锁的顺序来获取锁

例子

image.png

非公平锁

image.png

定义

多个线程获取锁的顺序并不是按照申请锁的顺序

例子

synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。

可重入锁

image.png

定义

同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁

例子

synchronized 和 ReentrantLock

自旋锁

image.png

定义

线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋

目的

减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

例子

AtomicInteger#getAndAddInt方法

扩展

在JDK1.6又引入了自适应自旋,一种动态调整自旋次数的机制,根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定当前的自旋次数,旨在减少不必要的上下文切换,避免浪费处理器资源。

分段锁

image.png

定义

一种锁的设计,并不是具体的一种锁,将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

例子

ConcurrentHashMap 底层就用了分段锁,使用Segment

锁升级

JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

无锁

无锁状态其实就是上面讲的乐观锁,这里不再赘述。

偏向锁

一个线程多次访问同步锁,对象头记录偏向线程id,自动获取锁,该线程不需要进行CAS加锁解锁。

偏向锁优化带来的性能提升指的是避免了获取锁进行系统调用导致的用户态和内核态的切换,因为都是同一条线程获取锁,没有必要每次获取锁的时候都要进行系统调用

轻量级锁

当锁是偏向锁时,被另一个线程访问,升级,通过自旋CAS获取锁,有竞争,不阻塞

重量级锁

轻量级锁自旋一定次数,还没获得,膨胀重量级,其实就是互斥锁了

锁优化

锁粗化

多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗

举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。

private static final Object LOCK = new Object();

for(int i = 0;i < 100; i++) {
    synchronized(LOCK){
        // do some magic things
    }
}

经过锁粗化后就变成下面这个样子了:

 synchronized(LOCK){
     for(int i = 0;i < 100; i++) {
        // do some magic things
    }
}

锁消除

jvm在编译时,去除不可能存在共享资源竞争的锁

StringBuffer的append是一个同步方法,但是在使用它的方法中基本上都是new 一个StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除