• Java随笔-锁


    概述

    Java中哪些是线程安全的,哪些是线程不安全的?使用final修饰的就是安全的,Vector是一个线程安全的容器,ArrayList线程不安全的,为了解决线程安全问题,引入了锁机制。锁其实就是自己使用的时候加上锁,不让别人用,用完了再把锁打开,让别人用,保证了一人一坑,解决了线程安全问题,但也带来了一些效率问题。锁的分类有不少,但都是为了在线程之间更高效共享数据,达到高效并发的效果。

    分类和状态

    自旋锁

    原理

    假如持有锁的线程很开就能完事并释放资源,那么正在等待竞争锁的线程就无需在内核态和用户态之间进行切换进入阻塞状态,只需要原地转一转等一等(自旋),等持有锁的线程释放锁后就可以立即获取锁。

    优点

    自旋锁能够在锁竞争不激烈的情况下,减少线程的阻塞,避免用户线程和内核切换的消耗,因为自旋消耗小于阻塞消耗,所以对部分代码来讲可以带来性能提升。

    缺点

    自旋本身是占用cpu资源,但是不做功,如果竞争激烈的话,或持有锁的线程需要较长时间,此时自旋的消耗就远大于线程阻塞挂起的消耗,造成cpu浪费。

    阈值

    自旋锁自旋时间需要控制,时间长了占用cpu资源,时间短了自旋没有意义。以前JDK1.5的时候默认是10次;JDK1.6的时候由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,也就是自适应自旋锁;JDK1.7之后就有JVM控制。

    偏向锁

    偏向锁总会偏向于第一个获得它的线程,如果在运行过程中,该锁没有被其他线程获取,也就是不存在多线程竞争的情况,则该线程不需要再进行同步,减少不必要的CAS操作。如果运行过程中遇到了其他线程抢锁的情况,则持有偏向锁的线程就会被挂起,JVM就会消除它身上的偏向锁,将锁恢复到未锁定或轻量锁状态。
    在这里插入图片描述
    使用偏向锁是为了消除资源无竞争的情况下的同步原语,进一步提高程序运行性能。获取过程如下:

    1. 访问Mark Work中偏向锁的标识是否置为1,如果置为01,则为可偏向状态。
    2. 若为可偏向状态,则测试线程ID是否指向当前线程,若是是则执行同步代码;如果不是,则竞争。
    3. 线程id并未指向当前线程,通过CAS操作竞争锁,成功,则将Mark Work中线程id设为当前线程id,然后执行同步代码;如果失败,则进行步骤4。
    4. CAS获取偏向锁失败,所有有线程竞争偏向锁,当达到全局安全点后,已获取偏向锁的线程被挂起,偏向锁升为轻量级锁,之后被阻塞在安全点的线程执行同步代码。

    CAS
    更有效,更灵活的一种原子操作。基本思路:如果该地址上的值和期望的值相等,则将新值赋值给该地址,否则啥也不做。就是如果想要修改某一地址上的值,得先告诉我这个地址上的值是多少,回答正确了才可以修改,否则回去。
    Mark Word
    存储对象的hashcode或锁信息等。

    锁状态25bit4bit是否是偏向锁1bit锁标志位2bit
    锁当前状态对象的hashcode对象的分代年龄age001

    轻量级锁

    轻量级锁是由偏向锁升级而来的,偏向锁运行在一个线程进入同步块的情况下,当出现其他线程争用锁的情况,偏向锁就会升级为轻量级锁。
    当代码进入同步块时,如果同步对象锁是无状态的且不允许进行偏向,虚拟机会先在当前线程的栈帧中建立一个锁记录空间Lock Record,用于存储锁对象当前Mark Work的拷贝,官方的叫法是Displaced Mark Work。
    拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。若更新成功,当前的线程就拥有了该对象的锁,并将对象Mark Word的锁标志置为00,表示此对象处于轻量级锁定状态。若更新失败了,虚拟机会首先检查对象的Mark Word是否指向当前线程的栈帧,如果是则说明当前对象已经拥有了该对象的锁,就可以直接进入同步块执行,否则说明这个锁对象已经被其他的线程抢占了。

    重量级锁

    在轻量级锁的状态下,如果有两条以上的线程争用同一个锁,那么轻量级锁就不再有效,就会膨胀为重量级锁,锁标志也会被置为10,Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。

    转换

    锁的状态一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。锁状态会随着竞争情况逐渐升级,只能升级但是不能降级,这样才能提高获得锁和释放锁的效率。
    请添加图片描述

    比较

    优点缺点场景
    偏向锁加解锁无需额外的消耗,而且可执行同步方法相比基本一样快若线程之间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块的场景
    轻量级锁竞争的线程不会阻塞,提高了程序响应速度若始终得不到锁,线程会使用自旋,会消耗CPU同步块执行速度非常块,追求时间
    重量级锁线程竞争不会使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较慢

    死锁

    描述

    两个或两个以上的线程在执行的过程中,由于竞争资源或由于彼此通信而造成的一种阻塞现象,如果没有外力,该阻塞情况会一直持续下去,此时就进入死锁状态。

    产生

    死锁产生也不是随随便便就能产生的,需要几个条件。

    1. 互斥条件。给线程分配的资源具有排他性,也就是同一时间段内只能有一个线程占用,如果还有其他线程需要使用该资源,只能等着当前线程使用完毕释放后才能有机会使用。
    2. 请求和保持条件。线程已经占用了至少一个资源,再想占用其他资源的时候,该资源被其他线程阻塞,此时请求占用的线程发生阻塞,并且对自己持有的资源也不释放。
    3. 不剥夺条件。线程已经获取的资源在没有使用完毕之前,其他线程不能强行占用,只能由正在占用的线程在使用完后进行释放。
    4. 环路等待条件。发生死锁时,多线程之间必然存在一个资源占用链,也就是线程1等待线程2的资源,线程2等待线程3的资源,线程3等待线程4的资源…。线程1等待线程2的资源,线程2也在等待线程1的资源,这个不叫死锁。

    解决

    知道了死锁的产生条件,只要打破其中一个条件就可以预防或解除死锁。

    1. 打破互斥条件。将资源的独有改成虚拟资源,这个不容易改造。
    2. 打破请求和保持条件。当线程独占资源且又请求占有其他资源时可先退出原有资源的占用。
    3. 打破不剥夺条件。资源分配采用预先分配的策略,运行前申请全部资源,若成功就运行,若失败则等待。
    4. 打破环路等待条件。实现资源有序分配,按顺序来就不会出现相互等待的情况。

    活锁

    描述

    两个或多个线程在竞争锁的过程中,出现过度谦让的情况,总是出现一个线程总拿到同一把锁,想拿其他的锁总拿不到,而将自己原本持有的锁又释放了。

    解决

    线程之间错开拿锁时间,比如线程休眠随机时间。

  • 相关阅读:
    邢福有老师 找准公文写作要点,避开这3种毛病
    傻瓜式制作产品图册,一秒就能学会
    Ubuntu20.04软件安装顺序
    Linux网络配置解析:连接世界的关键步骤
    倒计时15天!百度世界2023抢先看
    基于粒子群算法改进小波神经网络的时间序列预测,基于pso-ann的回归分析
    01 python编码语法
    IB数学、物理、生物、化学复习指导书分享
    Python数据分析
    服务器之Apollo单机部署(快速安装)
  • 原文地址:https://blog.csdn.net/qq_34202054/article/details/123667807