并发编程(二)java中的锁体系

系列文章目录

一:计算机模型&volatile关键字详解
二:java中的锁体系
三:synchronized关键字详解
五:Atomic原子类与Unsafe魔法类详解



前言

由上篇文章可知,volatile关键字可以帮助解决可见性和有序性,但是在原子性上,volatile是在多线程情况下是无法保证原子性的,那么就需要使用锁来保证原子性,能做保证在多线程运行时,只能有一个线程访问临界资源。在Java 中,提供了两种方式来实现同步互斥访问,synchronize关键字和Lock。还有一种是使用CAS(Compare and Swap)思想类保证原子性。
在这里插入图片描述


一、悲观锁与乐观锁

1、悲观锁

对于多线程并发操作,加了悲观锁的线程认为每一次修改数据时都会有其他线程来跟它一起修改数据,所以在修改数据之前先会加锁,确保其他线程不会修改该数据。
由于悲观锁在修改数据前先加锁的特性,能保证写操作时数据正确,所以悲观锁更适合写多读少的场景。
在这里插入图片描述

2、乐观锁

乐观锁在每一次修改数据时,都认为没有其他线程来跟它一起修改,所以在修改数据之前不会去添加锁,如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。
由于乐观锁是一种无锁操作,所以在使用乐观锁的场景中读的性能会大幅度提升,适合读多写少

它们个有优缺点,没有好坏之分,只有适应场景的不同区别。比如:乐观锁适合用于写比较少的情况下,即冲突真的很少发生的场景,这样可以省去锁的开销,加大了系统的整个吞吐量。但是如果经常产生冲突,上层应用会不断的进行重试,这样反而降低了性能,所以这种场景悲观锁比较合适。
在这里插入图片描述
在Java中悲观锁的实现有:synchronized、Lock实现类,
乐观锁的实现有 CAS

二、自旋锁与适应性自旋锁

1.自旋锁

CPU线程的切换(用户态与内核态切换)是一个非常消耗资源的操作。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。所以当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态,直到获取到某个锁。
自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。
所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁在JDK 1.4中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启;在JDK1.6中默认开启,同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。
如果通过参数-XX:PreBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。
假如将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如多自旋一两次就可以获取锁)。于是JDK1.6引入适应性自旋锁。
在AQS中和Atomic原子操作中,使用的大量的自旋操作

2.适应性自旋锁

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间(比如上一次或者到的锁的自旋次数是10次,那么此次可能自旋次数为15次)。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

三、无锁、偏向锁、轻量级锁与重量级锁

从jdk1.6开始为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。锁共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。随着竞争情况锁状态逐渐升级、锁可以升级但不能降级。
在这里插入图片描述

1、无锁(锁消除)

锁消除是发生在编译器级别的一种锁优化方式。有时候我们写的代码完全不需要加锁,却执行了加锁操作。

  /**
     * 锁的消除
     */
    public void test2(){
        //jvm的优化,JVM不会对同步块进行加锁
        synchronized (new Object()) {
            //jvm是否会加锁?
        }
    }

2、偏向锁

偏向锁是一种针对加锁操作的优化手段。它的核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。这样就节省了大量有关锁申请的操作,从而提高了程序性能。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的 MarkWord 里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下 MarkWord 中偏向锁的标识是否设置成 1(表示当前是偏向锁),如果没有设置,则使用 CAS 竞争锁,如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。

3、轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时MarkWord的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

4、重量级锁

重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

四、公平锁与非公平锁

1、公平锁

如果多个线程申请一把公平锁,那么获得锁的线程释放锁的时候,先申请的先得到,很公平。
在这里插入图片描述

2、非公平锁

相比公平锁,在非公平锁中后申请的线程可能先获得锁,是随机获取还是其它方式,都是根据实现算法而定的。
在这里插入图片描述
对 ReentrantLock 类来说,通过构造函数可以指定该锁是否是公平锁,默认是非公平锁。因为在大多数情况下,非公平锁的吞吐量比公平锁的大,如果没有特殊要求,优先考虑使用非公平锁。
而对于 synchronized 锁而言,它只能是一种非公平锁,没有任何方式使其变成公平锁。这也是 ReentrantLock 相对于 synchronized 锁的一个优点,更加的灵活。

五、可重入锁与非可重入锁

1、可重入锁

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

public static void reentrantlock(){
        synchronized (object) {
            System.out.println("message Info:--->进入第一个同步块");
            synchronized (object){
                System.out.println("message Info:--->进入第二个同步块");
            }
        }
    }

2、非可重入锁

如果不是可重入锁,在递归函数中就会造成死锁,所以 Java 中的锁基本都是可重入锁,非可重入锁的意义不是很大
在这里插入图片描述

六、共享锁与排他锁

1、排他锁

是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得写锁的线程即能读数据又能修改数据。

2、共享锁

是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得读锁的线程只能读数据,不能修改数据


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值