Java并发编程—Java中的所有锁

76 阅读6分钟

前言

Java提供了各种锁,每种锁因为其特性的不同,在适当的场景下能够非常的高效。本篇文章在对锁相关原理、使用场景进行举例,为各位看官老爷介绍主流锁的知识点,以及不同的锁的适用场景。

Java中所有锁的分类

Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。

Java的锁分类.png

1、乐观锁和悲观锁

在数据中因为总是假设最坏的情况,所以每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁,有任何一线程对数据进行读写操作都会上锁,数据库的行锁、表锁、读锁,写锁都是悲观锁,Java中synchronized锁和ReentrantLock 就是悲观锁思想的实现。

乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,有任何一线程对数据进行写操作都会上锁;也就是读的时候不上锁,写的时候才上锁,

乐观锁和悲观锁运行过程.png

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

2、独占锁(排他锁)和共享锁

独占锁也叫排他锁。独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

悲观锁和独占锁的区别: 悲观锁是针对读写上锁,而独占锁是针对线程的独占,这一点需要区分,但其本质是一样的,有人说独占锁就是悲观锁,这样讲也可以。

独占锁.png

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

共享锁.png

3、公平锁和非公平锁

公平锁指的是多个线程按照指定的顺序执行,整个过程有序地进行每个线程,就像银行排队一样,需要办理业务的人们排成一排,有序地进行办理业务,上一个人业务办理完成后进行下一个人的业务办理!

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
公平锁.png

非公平锁顾名思义,就是不公平的,多个线程通过抢占的方式进行执行,谁抢到就给谁执行,就跟强盗逻辑是一样的,谁抢到就是谁的;一旦有一个线程抢到锁资源之后,其他的线程只能进入阻塞状态,等待下一轮的抢占;

  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死!
非公平锁.png

4、无锁、偏向锁、轻量级锁和重量级锁

在jdk1.6之前,synchronized 是一把很重的锁,每次加锁的时候都会向操作系统申请锁资源,但是有时候我们在只有一个线程的情况下并不需要这么重的锁,因此每次都用这么重的锁会带来很大的消耗;所以在jdk1.6开始,为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

总结:偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

锁升级

在jdk1.6之前,synchronized 是一把很重的锁,每次加锁的时候都会向操作系统申请锁资源,但是有时候我们在只有一个线程的情况下并不需要这么重的锁,因此每次都用这么重的锁会带来很大的消耗;所以在jdk1.6开始,为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

锁升级.png

5、可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁 Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

可重入锁.png

6、自旋锁

自旋锁是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环会一直判断是否有可用的锁资源,如果有可用的锁资源,就会通过比较和交换(CAS)的方式去抢占资源,如果抢不到则进入下一个忙循环,一直到抢到锁资源为止,这就是所谓的自旋。

自旋锁.png

自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。 如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。

参考资料

  • 《Java并发编程艺术》
  • Java中的锁
  • Java并发——关键字synchronized
  • Synchronized锁升级的过程