Java基础-并发编程-锁机制

Java工程师知识树 / Java基础


锁机制

Java锁机制其实就是一种等待机制,将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,这样锁就可以用来保障线程安全了。

锁(Lock)可以理解为对共享数据进行保护的一个许可证. 对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证. 一个线程只有在持有许可证的情况下才能对这些共享数据进行访问; 并且一个许可证一次只能被一个线程持有; 许可证线程在结束对共享数据的访问后必须释放其持有的许可证。

一个线程在访问共享数据前必须先获得锁; 获得锁的线程称为锁的持有线程; 一个锁一次只能被一个线程持有. 锁的持有线程在获得锁之后和释放锁之前这段时间所执行的代码称为临界区(Critical Section)。

锁具有排他性(Exclusive), 即一个锁一次只能被一个线程持有.这种锁称为排它锁或互斥锁(Mutex)

Java线程锁是如何实现线程的原子性,可见性与有序性,从而实现对共享数据的安全访问的?

  • 锁是通过互斥保障原子性. 一个锁只能被一个线程持有, 这就保证临界区的代码一次只能被一个线程执行.使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性。
  • 可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的. 在java平台中,锁的获得隐含着刷新处理器缓存的动作, 锁的释放隐含着冲刷处理器缓存的动作。
  • 锁能够保障有序性.写线程在临界区所执行的,在读线程所执行的临界区里看来像是完全按照源码顺序执行的。

注意:
使用锁保障线程的安全性,必须满足以下条件:
这些线程在访问共享数据时必须使用同一个锁。
即使是读取共享数据的线程也需要使用同步锁。

JVM锁分类:

JVM把锁分为内部锁(内置锁)和显示锁两种. 内部锁(内置锁)通过synchronized关键字实现; 显示锁通过java.concurrent.locks.Lock,java.util.concurrent.locks.ReadWriteLock接口的实现类实现的。

可以将JVM锁的分类细分,通过不同的方式划分成不同的锁:

按照锁的特性划分:

  1. 可重入锁:又名递归锁,在执行对象中所有同步方法不用再次获取锁。指在同一个线程在外层方法获取锁的时候在进入内层方法会自动获取锁,synchronized 和 ReentrantLock 都是可重入锁,可重入锁可以在一定程度避免死锁。
  2. 可中断锁:在等待获取锁过程中可中断。synchronized 是不可中断的,Lock 是可中断的,这里的可中断建立在阻塞等待中断,运行中是无法中断的。
  3. 公平锁:公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁就是没有顺序完全随机,所以能会造成优先级反转或者饥饿现象;synchronized 就是非公平锁,ReentrantLock(使用 CAS 和 AQS 实现) 通过构造参数可以决定是非公平锁还是公平锁,默认构造是非公平锁;非公平锁的吞吐量性能比公平锁大好。
  4. 读写锁:对资源读取和写入的时候拆分为两个部分处理,读的时候可以多线程一起读,写的时候必须同步写。读写锁中,读锁是共享锁,写锁是独占锁。
    1. 共享锁与独占锁(互斥锁/排他锁):是读写锁的延申,独享锁是指该锁一次只能被一个线程持有,共享锁指该锁可以被多个线程持有;synchronized 和 ReentrantLock 都是独享锁,ReadWriteLock 的读锁是共享锁,写锁是独占锁。

按照锁的状态划分:

无锁、偏向锁、轻量级锁、重量级锁。这种分类是按照锁状态来归纳的,并且是针对 synchronized 的,java 1.6 为了减少获取锁和释放锁带来的性能问题而引入的一种状态,其状态会随着竞争情况逐渐升级,锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后无法降为偏向锁,这种升级无法降级的策略目的就是为了提高获得锁和释放锁的效率。

按照锁的设计划分:

  1. 乐观锁与悲观锁: 这个分类不是具体锁的分类,而是看待并发同步的角度;悲观锁认为对于同一个数据的并发操作一定是会发生修改的(哪怕实质没修改也认为会修改),因此对于同一个数据的并发操作,悲观锁采取加锁的形式,因为悲观锁认为不加锁的操作一定有问题;乐观锁则认为对于同一个数据的并发操作是不会发生修改的,在更新数据的时候会采用不断的尝试更新,乐观锁认为不加锁的并发操作是没事的;由此可以看出悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升,悲观锁在 java 中很常见,乐观锁其实就是基于 CAS 的无锁编程,譬如 java 的原子类就是通过 CAS 自旋实现的。
  2. 自旋锁与适应性自旋锁: 其实是相对于互斥锁(独占锁/排他锁)的概念,互斥锁线程会进入 WAITING 状态和 RUNNABLE 状态的切换,涉及上下文切换、cpu 抢占等开销,自旋锁的线程一直是 RUNNABLE 状态的,一直在那循环检测锁标志位,机制不重复,但是自旋锁加锁全程消耗 cpu,起始开销虽然低于互斥锁,但随着持锁时间加锁开销是线性增长。

Java锁结构示意图

上图地址:https://www.processon.com/diagraming/601219d2e0b34d1f2cce6db5

你可能感兴趣的:(Java基础-并发编程-锁机制)