java并发系列(一) 让人眼花缭乱的锁

220 阅读6分钟

美团技术团队

1. 乐观锁 VS 悲观锁

乐观锁和悲观锁是两种思想。

乐观锁:认为自己使用数据时,不会有别的线程修改数据。所以不加锁。在更新数据的时候,如果数据没有被修改,那么更新成功。如果数据被修改,更新失败,进行其他操作,比如重试或者报错

悲观锁:认为自己使用数据时,一定会有别的线程来修改数据。所以直接加锁

悲观锁适合多写入的场景。常见的实现:sychronized,Lock

乐观锁适合多读入的场景。常见的实现:原子类操作(java.util.concurrent包中的原子类)

那乐观锁如何正确实现同步呢?通过CAS(compareAndSwap),一种无锁算法。

2. 自旋锁 VS 适应性自旋锁

自旋锁:阻塞和唤醒一个线程需要操作系统切换CPU状态来完成,比较耗费时间。如果线程请求锁失败,正常是要进行阻塞的。但是我们延迟阻塞,如果在延迟期间,锁被释放了,那么当前线程就获取到了锁,避免阻塞造成的性能损耗。延迟的操作就是自旋。

但自旋也是有弊端的。自旋本身是需要占用处理器时间的,如果在自旋时间内,获取到锁,那么可以节省时间。如果在自旋时间锁没有释放,那么自旋失败,当前线程进入阻塞状态。这样,自旋时占用的资源就被浪费了。所以自旋的时间需要有限度。

自旋的原理:CAS(乐观锁也是这样实现的)

适应性自旋锁:自旋的时间不是固定的,而是根据上一个在同一个锁上的自旋时间和锁的拥有者的状态决定的。如果在同一个锁上,当前运行中的线程是通过自旋获得锁的,那么就认为当前线程也是可以通过自旋获得锁的。如果对于某个锁,自旋的成功率很低,那么就不自旋,直接进入阻塞状态,避免自旋浪费资源。

3. sychronized :无锁VS偏量锁VS轻量级锁VS重量级锁

这四种锁是指锁的状态。是专门对 sychronized 的优化.级别从低到高:无锁,偏向锁,轻量级锁,重量级锁

无锁:多个线程可以同时访问资源,但同时只有一个线程可以修改成功。没有修改成功的,会一直重试直到修改成功。 适用于: 持有锁的线程是否会很快释放锁。

原理: 自旋。

偏向锁:锁一直是某个线程获取的,那么这个线程可以自动获得锁,节省了获取锁的开销。

适用场景:多线程环境下,同步资源一直是由某个线程访问,那么这个线程可以一直获得偏向锁。

对象头中有个 threadId,默认是空的。当第一次获取锁时,将线程Id写入锁对象的threadId,并将是否偏向锁标志位这是为 01。 下次获取锁,将线程id和锁对象头的threadId比较,如果相同,当前线程自动获取锁。

在遇到其他线程竞争偏向锁是,持有偏向锁的线程才会释放锁(锁撤销),线程不会主动释放偏向锁。

偏向锁的撤销:需要等到全局安全点(在这个点没有字节码执行),先暂停拥有锁的线程,在判断锁对象是否处于锁定状态,最后膨胀为轻量级锁

原理是:通过 CAS 更新 threadId.

轻量级锁: 当锁是偏向锁的时候,有另外的线程来竞争锁,锁会膨胀为轻量级锁。 适用场景:多线程场景下,多个线程交替访问同步资源,不存在锁竞争。

轻量级锁:当一个线程获得锁,会在当前线程的栈帧内创建一个锁记录空间(Lock Record),通过CAS将锁对象的Mardword拷贝到Lock Record中,并将Mardwrod更新为指向Lock Record的指针,同时将Lock Record中的owner指向锁对象的MarkWord。 当下次有线程请求锁,先检查锁对象的MarkWord是否指向当前线程的栈帧,如果是,获取锁。如果不是,自旋等待 如果当前有两个线程,一个持有锁,一个自旋,再来一个线程的,那么轻量级锁膨胀为重量级锁

原理是:通过 CAS 更新 MardWord.(比偏向锁更新 threadId 频繁)和自旋

重量级锁:等待锁的线程会被阻塞

原理:使用操作系统内部的互斥锁

4. 公平锁VS非公平锁

公平锁:按照申请锁的顺序,将锁添加到等待队列。等锁被释放后,按照申请顺序来获得锁。

非公平锁:在访问的时候,就去尝试获得锁,有可能会出现后申请的线程先获得锁。

公平锁:不会出现等待线程 ==饿死(一直获取不到锁)== 的情况。但是吞吐量比较低,除了第一个等待的线程,其他线程都处于阻塞状态,阻塞是比较耗性能的。

非公平锁:吞吐量比较高,但会出现线程饿死情况。

ReentrantLock有两个内部类,分别实现了公平锁和非公平锁

5. 可冲入锁VS非可重入锁

可重入锁:又名递归锁。如果一个线程获得外部方法的锁,在访问内部方法时,不需要再次获得锁。前提都用的同一个锁对象。 可以避免死锁问题

ReentrantLock 和 sychronized 是可重入锁。

非可重入锁:和可重入锁相反。会出现死锁。NonReentrantLock

独占锁VS共享锁

独占锁:又叫排他锁,互斥锁。synchronized和Lock都是。

共享锁:该锁可以被多个线程持有。获得共享锁的线程不能修改数据,只能读数据。

可以研究 ReentrantReadWriteLock 的源码。读锁是共享锁,写锁是独占锁

总结

不同的锁,只是从解决问题的角度进行分类。我们实际在用的锁,大都是多种类型并存的。比如ReentrantLock 既是可重入锁,也是互斥锁,也可能是公平锁或者非公平锁(内部有实现),但是同一类型 只能存在一种,比如:不能既是乐观锁又是悲观锁。

参考链接

不可不说的Java“锁”事

Java 线程优化 偏向锁,轻量级锁、重量级锁