java中的各种锁

什么是锁?

锁(lock)是一种同步机制,用于在多线程环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。

锁的三个概念:锁有开销、会产生竞争、要避免产生死锁

锁开销 lock overhead:锁占用内存空间、cpu初始化和销毁锁、获取和释放锁的时间。

锁竞争 lock contention:一个进程或线程试图获取另一个进程/线程持有的锁,就会发生竞争。锁粒度越小,发生锁竞争的可能性越小。

死锁 deadlock:至少两个任务中的每一个都在等待另一个任务持有的锁的情况。

锁的种类:

1、公平锁/非公平锁

    公平锁,多个线程按照申请锁的顺序来获取锁;以打水为例,所有人排队,管理员不接收插队

ReentrantLock(true)公平锁,默认是非公平锁

    非公平锁,不按照申请顺序,有可能造成优先级反转或者饥饿现象;所有人排队,管理员接收插队

ReentrantLock、Synchronized

可重入锁

    同一个线程可以重入上锁的代码段,不同的线程则需要进行阻塞。Synchronized具有重入锁的功能,在一个synchronized方法/块内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

    以打水为例,可重入锁相当于一个人有多个水桶,水桶和锁绑定。非重入锁,第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和水桶绑定无法打水。当前线程出现死锁。

通过重入锁ReentrantLock和非可重入锁NonReentrantLock的源码分析:

都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status==0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status!=0,则判断当前线程是否获取到这个锁的线程,如果是的话执行status+1,且当前线程再次获取锁。而非可重入锁是直接去获取并尝试更新status的值,如果status!=0的话会导致其获取锁失败,当前线程阻塞。

2、独享锁/共享锁

    独享锁,该锁一次只能被一个线程所持有;

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

互斥锁/读写锁

    互斥锁/读写锁是上面独享锁/共享锁的具体实现

ReentrantLock和ReentrantReadWriteLock:独享锁和共享锁。

3、乐观锁/悲观锁

    不是具体类型的锁,而是指看待并发同步的角度。

    悲观锁认为对于同一数据的并发操作,一定会发生修改。所以采取先加锁的形式。

    乐观锁则认为对于同一数据的并发操作,是不会发生修改的。更新数据的时候,会采用尝试更新,不断更新的方式更新数据。

    悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景。悲观锁在java中的使用,显示的锁定之后再操作同步资源;而乐观锁则直接去操作同步资源,就是无锁编程,常采用CAS算法,通过CAS自旋实现原子操作的更新。java.util.concurrent包中的原子类就是通过CAS实现了乐观锁。jdk1.5开始提供了AtomicReference类来保证引用对象之间的原子性。重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁。

CAS是一种无锁算法,有三个操作,内存值V,旧的预期值A,要修改的新值B。当且仅当旧的预期值A和内存值V相同时,才将内存值V修改为B,否则什么都不做。类似乐观锁

4、分段锁

    分段锁是一种锁的设计,并不是具体的一种锁。对于ConcurrentHashMap,其并发实现就是通过分段锁的形式来实现高效的并发操作。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode计算出要放在哪一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不在一个分段中,就实现了真正的并行的插入。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

5、无锁/偏向锁/轻量级锁/重量级锁

    这四种是指锁的状态,并且是针对Synchronized的。两个重要的概念:“Java对象头“,”Monitor”

Java对象头:Hotspot虚拟机的对象头主要包括:Mark Word(标记字段)和 Klass Pointer(类型指针)

        Mark Word:默认存储对象的HashCode、分代年龄和锁标志信息。被设计成非固定的数据结构。

        Klass Pointer:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor:可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段放拥有该所的线程的唯一标识,表示该锁被这个线程占用。

synchronized通过Monitor来实现线程同步,Monitor是依赖底层的操作系统的Mutex Lock(互斥锁)来实现线程同步。

依赖操作系统Mutex Lock所实现的锁,称之为:重量级锁,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入“偏向锁”和“轻量级锁”。

目前锁一共有4种状态:无锁:偏向锁、轻量级锁和重量锁。锁状态只能升级不能降级。

在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

           当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word是否存储着指向当前线程的偏向锁。引入偏向锁的目的是:在无多线程竞争情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

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

    重量级锁是指当锁是轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低

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

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

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mark Word复制到锁记录中,官方称为:Displaced Mark Word;整个synchronized锁流程如下:

    1.检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁

    2.如果不是,则使用CAS将当前线程的ID替换Mark Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1

    3.如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁

    4.当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁

    5.如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

    6.如果自旋成功则依然处于轻量级锁状态

    7.如果自旋失败,则升级为重量级锁

6、自旋锁/适应性自旋锁

    阻塞或者唤醒一个java线程需要操作系统切换CPU来完成,这种状态装换需要耗费处理器时间,如果同步代码块内容太简单执行速度很快,同步资源的锁定时间很短,切换CPU得不偿失。因而引入自旋锁,让当前线程“稍等一下”,让线程进行自旋。

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁的时间非常短的代码块来说性能能大幅度提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。

    自适应意味着自旋的时间(次数)不固定,刚刚自旋获得过锁,虚拟机允许自旋等待持续相对更长的时间;如果自旋很少成功过,可能在尝试获取锁过程中忽略自旋,直接阻塞线程。

我们所熟知的Java锁机制无非就是Synchronized锁和Lock锁

Synchronized是基于JVM来保证数据同步的,而Lock则是在硬件层面,依赖特殊的CPU指令实现数据同步的。

    Synchronized:它就是一个:非公平、悲观、独享、互斥、可重入的重量级锁。

    ReentrantLock:它就是一个:默认非公平但也实现公平的、悲观、独享、互斥、可重入的重量级锁。

    ReentrantReadWriteLock:它是一个:默认非公平但可实现公平的、悲观、写独享、读共享、读写可重入的重量级锁。

线程安全实现方式:

     互斥同步(锁机制):

    非阻塞同步(使用循环CAS实现原子操作):

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值