图标
创作项目友邻自述归档留言

浅谈并发:锁

前言

接着写浅谈系列。

这次要聊的是锁,锁在并发编程中扮演着非常重要的角色,Java 中提供了多种类型的锁,不同类型的锁在不同的场景中都有不同的性能表现。

锁的类别

锁可以按照特性进行分类,分类后我们就可以较为直观的区分不同的锁和区别不同锁的不同特性。

锁的对比

分类了锁后,我们就可以对其进行对比分析了。

悲观锁 & 乐观锁

对于一个数据的并发操作,悲观锁总是认为自己在使用数据的时候别的线程会来修改数据,所以为了防止被修改,在获取数据的时候会对该数据进行加锁操作,确保只有取得锁的线程能对此数据进行操作。

与之相反,乐观锁总是认为自己在使用数据的时候别的线程不会来修改数据,所以也就不需要加锁,通常是使用 CAS 操作,在更新的时候通过比较判断数据是否被修改,如果没有被修改,则更新成功,如果被修改则进行报错或重试。

悲观锁更适合写多读少的场景,读取操作并不会改变数据,所以如果用在读取多的场景下则会造成严重的性能浪费。

乐观锁则更适合写少读多的场景,由于乐观锁一般是采用 CAS(比较并替换)的实现方式,每次写入操作的时候都要复制一份原始数据,而这操作是非常耗能的,用在写入少的场景下就不如使用悲观锁了。而读取的时候并不会加锁,这也就意味着可以并行的读取,所以在读取的方面上有非常好的性能。

非自旋锁 & 自旋锁

自旋指的是在获取数据失败后是采用进行循环重试而不是休眠线程等待锁的释放。

通常情况下,锁住某个资源的时间都是很短的,如果为了这点时间挂起线程恢复线程,则有可能得不偿失。在这种场景下,我们可以进行循环尝试,一旦锁释放了,那么就可以立即对数据进行操作,而不需要再等待线程的唤醒。

不过自旋也是有缺点的,由于是循环尝试,所以虽然避免了线程的切换,但需要占用处理器时间,所以一般都是进行一定范围的尝试,超出了范围后切换成阻塞。

公平锁 & 非公平锁

公平锁是指线程获取锁的先后顺序是按照申请锁时的先后顺序排序的。

非公平锁下的线程会先尝试一下获取锁,如果刚好锁被释放了,那么这个线程就可以直接获取锁,而无需进行排队。当第一次获取的时候无法获取到的时候才会进入排队的状态。

公平锁下不会出现线程饿死(无限等待)的情况,但是缺点也很明显,每个线程都需要阻塞等待,而阻塞和唤醒的开销并不小。

非公平锁由于可以插队,如果刚好可以获取到锁的话就不需要阻塞和唤醒了,减少了部分的开销,整体的并发性能较好。但是由于可以插队,那么就有可能会出现部分线程长时间处于等待的状态,或者是饿死的情况。

可重入锁 & 不可重入锁

可重入锁和不可重入锁的区别在于一个线程是否可以再次获取该锁。不可重入锁容易发生死锁的问题。

Java 中的 ReentrantLock 和 synchronized 都是可重入锁,NonReentrantLock 是不可重入锁。

共享锁 & 排他锁

故名思意,共享锁就是当一个线程获取该锁的时候,另外一个线程也可以获取这个锁。

排他锁则相反,当一个线程获取该锁的时候,另外一个线程不可获取该锁。

共享锁一般用于读取操作,而排他锁一般用于写入操作。在 Java 中 ReentrantReadWriteLock 就是读写分离的锁,里面有 ReadLock 和 WriteLock 两把锁。读锁采用的是共享锁,再并发的时候可保证高效的并发,写锁采用排他锁,确保写操作的互斥。

无锁 & 偏向锁 & 轻量级锁 & 重量级锁

这四个锁指的是锁的状态,是 synchronized 的 4 种锁状态。

无锁就是对数据进行锁定操作,所有的线程都可以修改数据,一般是使用 CAS 等操作来保证线程安全。无锁由于不需要对线程进行休眠和唤醒,在一些场景下的性能是很好的。

偏向锁指的是一段代码一直被某个线程获取,那么这个线程就会自动获取锁。当再次操作的时候就看看偏向锁是否指向该线程,如果是,则不需要加锁操作了。

轻量级锁可以认为就是自旋锁,当锁处于轻量级锁的状态时,线程尝试获取锁失败的时候不会进入阻塞状态,而是会循环尝试。

重量级锁就是非自旋锁,当获取锁失败的时候,线程会进入阻塞的状态。

从无锁到重量级锁,其对应的额外消耗是逐渐递增的。

锁的升级 & 降级

所谓的升级和降级其实是 JVM 对 synchronized 的优化策略,在合适的场景使用合适的锁,而不是直接使用重量级锁。

无锁 <=> 偏向锁

当线程访问同步数据的时候就会将锁升级成偏向锁。

当 JVM 处于全局安全点(在这个时间点上没有正在执行的字节码)的时候会将偏向锁撤销,恢复成无锁的状态。

偏向锁 => 轻量级锁

当锁处于偏向锁的情况下,没有持有偏向锁的线程尝试获取锁时,偏向锁就会被撤销,同时锁升级成轻量级锁。

轻量级锁无法降级。

轻量级锁 => 重量级锁

当自旋超过一定指定的范围,或者是一个线程持有锁,一个线程在自旋,这时又有第三个线程尝试获取锁时,锁就会升级成重量级锁。

重量级锁无法降级。

结语

又是一篇水文 ?,本文主要是简单概述下锁的一些类别和升降级流程,所以有很多细节的地方并未说明,比如 Java 的对象头,所以,如果要更加深入的了解的话还是要另外阅读一些高质量的文章或书籍。文章不写这些主要是我不太精通,写出来的效果不好 ?。

浅谈并发:锁

https://blog.ixk.me/post/talking-about-concurrent-locks
  • 许可协议

    BY-NC-SA

  • 本文作者

    Otstar Lin

  • 发布于

    2020/08/26

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

聊聊现状-[2020-09]浅谈并发:基础