java中的锁

简介

锁是并发编程中的一个重要概念,在并发编程中,经常会遇到多个线程访问同一个共享变量,当他们同时对该共享资源进行读写操作时,就会产生数据不一致的情况;锁就是用来控制多个线程访问共享资源的方式,简单来说,一个锁能够防止多个线程同时访问共享资源。 Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。
在这里插入图片描述

锁的分类

1. 乐观锁&悲观锁

从宏观上来看,锁可以分为乐观锁与悲观锁;体现了对待锁的不同思想;

  • 乐观锁:乐观锁认为自己在访问资源的时候,不会有其他线程修改数据,所以不会对线程添加锁,而是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

    java中的乐观锁有两种实现方式:CAS、版本号控制;

    CAS即Compare And Swap,是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则返回false,不进行任何操作;

    但是CAS算法本身有个很大的问题,就是ABA问题,即如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。未解决该问题引入了版本号机制,即数据表中加上版本号字段 version,表示数据被修改的次数。当数据被修改时,这个字段值会加1,提交必须满足“ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略。

    CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。

    乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

  • 悲观锁:相对的,悲观锁认为写操作较多,即遇到并发写的可能性更高,所以每次去读取数据时总认为别人会修改,因此每次读取数据时都会上锁,其他线程想访问该数据时只能阻塞,直到这个线程释放了锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock就是悲观锁思想的实现。

    悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。Java中主要通过关键字synchronized实现。

    悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

2. 自旋锁&适应性自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态即切换上下文来完成,这种状态转换需要耗费处理器时间。在许多场景中,如果同步代码块中的内容过于简单,那么同步资源的锁定时间很短,然而状态转换消耗的时间有可能比用户代码执行的时间还要长,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。所以自选锁即是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待(不放弃CPU资源),然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,此时获取锁的线程一直处于活跃状态(而非阻塞)。

自旋锁并不能代替阻塞,在线程阻塞时,CPU会将阻塞线程挂起,切换到另一个线程处理其他操作,而自旋会一直占用CPU资源,如果操作时间短不会有太大影响,长时间的占用只会导致CPU资源的浪费;所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
在这里插入图片描述
自旋锁在Java1.6中改为默认开启,并引入了自适应的自旋锁。
自适应意味着自旋的次数不在固定,**而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态共同决定。**如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

3. 公平锁&非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

  • 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。 但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

4. 可重入锁&非可重入锁

  • 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。

  • 不可重入锁:与可重入相反,获取锁后不能重复获取,否则会死锁(自己锁自己)。

5. 独享锁&共享锁

  • 独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

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

锁的状态

锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁;这四种状态是专门针对synchronized的,在 JDK 1.6之前, synchronized 还是一个重量级锁 ,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对synchronized 进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁), 并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别) ,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

  • 无锁

    无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功

    无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

  • 偏向锁

    偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

    初次执行到synchronized代码块的时候,锁对象变成偏向锁,字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。 当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

  • 轻量级锁

    轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

    锁竞争: 如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

  • 重量级锁

    如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

    重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

    简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资。

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到索竞争的线程,使用自旋会消耗CPU追求响应速度,同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较慢
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值