Java学习手册:Java锁的分类和特点

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/MaybeForever/article/details/100108684

一、公平锁、非公平锁

公平锁指多个线程按照申请锁的顺序来获取锁,非公平锁就是没有顺序完全随机,所以能会造成优先级反转或者饥饿现象 。synchronized 就是非公平锁,ReentrantLock(使用 CAS 和 AQS 实现)通过构造参数可以决定是非公平锁还是公平锁,默认构造是非公平锁。非公平锁的吞吐量性能比公平锁好。

二、可重入锁

又名递归锁,指在同一个线程在外层方法获取锁的时候在进入内层方法会自动获取锁,synchronized 和 ReentrantLock 都是可重入锁,可重入锁可以在一定程度避免死锁

三、独享锁、共享锁

独享锁是指该锁一次只能被一个线程持有,共享锁指该锁可以被多个线程持有。synchronized 和 ReentrantLock 都是独享锁,ReadWriteLock 的读锁是共享锁,写锁是独占锁。ReentrantLock 的独享锁和共享锁也是通过 AQS 来实现的。

四、互斥锁、读写锁

互斥锁,指的是一次最多只能有一个线程持有的锁。对于互斥锁,如果资源已被占用,资源申请者只能进入睡眠状态。互斥锁实质就是 ReentrantLock,读写锁实质就是 ReadWriteLock。

五、乐观锁、悲观锁

这个分类不是具体锁的分类,而是看待并发同步的角度:①悲观锁认为对于同一个数据的并发操作一定是会发生修改的(哪怕实质没修改也认为会修改),因此对于同一个数据的并发操作,悲观锁采取加锁的形式,因为悲观锁认为不加锁的操作一定有问题(共享线程资源每次只给一个线程使用,其他的线程阻塞,用完后再把资源转让给其他线程)。②乐观锁则认为对于同一个数据的并发操作是不会发生修改的,在更新数据的时候会采用不断的尝试更新,乐观锁认为不加锁的并发操作是没事的。由此可以看出悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升,悲观锁在 java 中很常见,乐观锁其实就是基于 CAS 的无锁编程,譬如 java 的原子类就是通过CAS 自旋实现的。

(1)乐观锁创建的两种实现方式:版本号机制CAS算法

  • 1、版本号机制
    一般是在数据表中加上一个数据版本号version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程A 要更新数据值时,在读取数据的同时也会读取version 值,在提交更新时,若刚才读取到的version 值为当前数据库中的version 值相等时才更新,否则重试更新操作,直到更新成功。
  • 2、CAS算法
    即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数①需要读写的内存值 V;②进行比较的值 A;③拟写入的新值 B;当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值B 来更新V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

(2)乐观锁的缺点

  • 1、ABA问题
    如果一个变量V 初次读取的时候是A 值,并且在准备赋值的时候检查到它仍然是A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS 操作就会误认为它从来没有被修改过。这个问题被称为CAS 操作的 "ABA"问题。
  • 2、循环时间开销大
    自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU 带来非常大的执行开销。
  • 3、只能保证一个共享变量的原子操作
    CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。

六、分段锁

实质是一种锁的设计策略,不是具体的锁。对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效并发操作。当要 put 元素时并不是对整个 hashmap 加锁,而是先通过 hashcode 知道它要放在哪个分段,然后对分段进行加锁,所以多线程 put 元素时只要放在的不是同一个分段就做到了真正的并行插入,但是统计 size 时就需要获取所有的分段锁才能统计。分段锁的设计是为了细化锁的粒度。

七、偏向锁、轻量级锁、重量级锁

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

(1)重量级锁

重量级锁:Synchronized是通过对象内部的一个叫做监视器锁来实现的。但是监视器锁本质又是依赖于底层的操作系统的实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为"重量级锁"。

(2)轻量级锁

轻量级锁:“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

(3)偏向锁

偏向锁: 引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况,就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

(4)无锁状态

无锁状态:在代码进入同步块的时候,同步对象锁状态为无锁状态。

八、自旋锁

自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环(即所谓的自旋,就是自己执行空循环),如果在若干个空循环后,线程可以获得锁,则继续执行。如果线程依然不能获得锁,才会被挂起。(挂起:线程的挂起操作实质上就是使线程进入“非可执行”状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。在线程挂起后,可以通过重新唤醒线程来使之恢复运行。)

使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往依然无法获得对应的锁,不仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。

可能引起的问题:
1)占据过多CPU时间:如果锁的当前持有者长时间不释放该锁,那么等待者将长时间的占据cpu时间片,导致CPU资源的浪费,因此可以设定一个时间,当锁持有者超过这个时间不释放锁时,等待者会放弃CPU时间片阻塞。
2)死锁问题:如果有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁的时候,检测到锁已被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不能继续执行,这样就引起了死锁。因此递归程序使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的同的自旋锁。

九、可中断锁可中断锁

synchronized 是不可中断的,Lock是可中断的,这里的可中断建立在阻塞等待中断,运行中是无法中断的。

猜你喜欢

转载自blog.csdn.net/MaybeForever/article/details/100108684