• 【共享模型-----管程】


    0. 口述线程安全问题

    口述静态变量、实例变量、局部变量的线程安全问题
    静态变量像用static修饰的这种变量,其实在加载class文件后,会把静态变量加载到方法区,在多线程的环境下我们访问的都是同一个,会有线程安全问题。
    实例变量它是属于具体的创建的对象的,如果这个对象是单例的话,也是有线程安全问题的,比如说我们开了两个线程,这两个线程对同一个new出来的对象操作的话,是有线程安全问题的,多线程环境下访问的都是同一个,如果不是单例的话,每开一个线程就创建一个对象的话,再去操作的话,那就不存在线程安全问题,其实主要是看我们线程操作的是这个线程new的还是说操作的是同一个。
    局部变量的话,它是线程安全的,即使在多线程下,每个线程都有自己的虚拟机栈,而对于局部变量都记录在栈帧中的局部变量表中,它们不是共享的,每个线程都有自己的,不存在线程安全问题。

    1. 为什么会出现线程安全问题

    一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

    共享资源:

    • 多个线程读共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

    2. synchronized 解决方案

    为了避免临界区的竞态条件发生,有多种手段可以达到目的:

    • 阻塞式的解决方案:synchronized,Lock
    • 非阻塞式的解决方案:原子变量

    synchronized,俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁
    的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

    如果多个线程要实现对共享资源的访问,用synchronized锁对这个资源的操作,synchronized 必须锁同一个对象或者同一个类。

    2.1 线程八锁

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    3. 变量的线程安全分析

    成员变量静态变量是否线程安全?

    • 如果它们没有共享,则线程安全
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

    局部变量是否线程安全?

    • 局部变量是线程安全的
    • 但局部变量引用的对象则未必
      • 如果该对象没有逃离方法的作用范围,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全

    3.1 局部变量线程安全分析

    变量有两种类型,值引用和对象引用。
    1. 值引用
    在这里插入图片描述
    每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
    在这里插入图片描述

    2. 对象引用:list在本案例中不是局部变量,而是全局共享的。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    分析:

    • 无论哪个线程中的 method2 和method3 引用的都是同一个对象中的 list ,多个线程访问共享变量又有读和写操作会出现线程安全问题。

    在这里插入图片描述

    解决线程安全问题:将 list 修改为局部变量
    在这里插入图片描述
    在这里插入图片描述

    思考:为 ThreadSafe 类添加子类,当 method2 或 method3 方法用public修饰,会不会出现线程安全问题呢?:会
    在这里插入图片描述
    在这里插入图片描述

    上面图片为正确的写法不会出现线程安全问题,但是当我们将method2和method3方法用public修饰的话,那么当一个子类继承时如果重写了method2和method3,也有可能导致线程安全的问题,比如重写方法中又开了一个线程然后对同一个共享资源进行操作,这时旧线程和这个新线程就会因为对同一个共享资源进行操作而产生线程安全问题。(但是用private修饰则不会重写,不会覆盖父类中的方法,这时是子类定义的一个新的方法)所以用private修饰,final修饰也是防止公共方法避免受到子类的影响(避免子类重写覆盖)。 private 或 final 提供【安全】的意义,开闭原则中的闭。

    3.2 常见线程安全类

    • String
    • Integer
    • StringBuffer
    • Random
    • Vector
    • Hashtable
    • java.util.concurrent 包下的类(JUC)

    这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
    比如下面的例子线程安全:
    在这里插入图片描述
    ==但是:==单个方法线程安全,但是组合不安全,原因是Hashtable()中的get和put方法都用了synchronized修饰,但只能保证get操作的原子性或者put方法的原子性,但是不同的针对不同的对象,synchronized只能约束同一个,导致线程安全问题。
    在这里插入图片描述
    在这里插入图片描述

    不可变类线程安全性
    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的:只能读不能改,改的话都是创建一个新的对象,包含了改后的值。

    3.3 案例

    卖票
    有一个卖票方法sell,多线程环境下会出现超卖情况:
    当同一时刻多个线程都拿到当前的余票,此时拿到的是同一个余票,但是多个线程在卖票逻辑执行完之后只会保留最后一次跟新的余票数量,即前面线程卖完跟新的余票被覆盖了,卖了,但是没有跟新余票,出现超卖。
    原因分析:本质原因是因为多个线程访问sell,而sell中即包含读取当前余票然后卖票(操作余票数量),即又有读又有写操作,为共享资源
    解决:对sell方法加锁,保证原子操作。

    转账:
    一个账户类中包含转账transfer方法,转账的逻辑为:A给B转账,先判断A账户的余额,余额大于转账金额时,再读取B账户的余额,B的余额加上转账金额,完成转账。
    分析:transfer中既有读取A账户余额,又有读取B账户余额,又有写操作(A余额减,B余额+)
    解决:对transfer方法上锁,但是此时注意:不像卖票一样对synchronized(this)上锁,转账中A账户B账户我们完成转账逻辑是相联系的,读A,A够的话,读B,A-,B+。所以应该对A和B账户做一个锁的制约,A和B都是new出来的账户对象(account),加锁用类锁,锁同一个类,即AB拿到的都是同一吧锁synchronized(Account.class)

    4. synchronized 原理

    Java 对象头
    在这里插入图片描述
    在这里插入图片描述
    每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
    在这里插入图片描述

    • 刚开始 Monitor 中 Owner 为 null
    • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner,在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList阻塞
    • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的
    • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析

    4.1 轻量级锁

    轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。语法仍然是 synchronized

    假设有两个方法同步块,利用同一个对象加锁:
    在这里插入图片描述

    1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word在这里插入图片描述

    2. 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录在这里插入图片描述

    3. 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁在这里插入图片描述

    4. 如果 cas 失败,有两种情况

      • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
      • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

    1)锁重入:还是增加一个锁记录,将第一部分结构设置为null
    在这里插入图片描述

    2)锁膨胀:如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
    当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁在这里插入图片描述

    这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED阻塞
      在这里插入图片描述
      当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

    4.2 自旋优化

    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
    在这里插入图片描述

    4.3 偏向锁

    轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS (交换markword)操作。偏向锁:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
    例子:在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    重偏向

    • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
    • 当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

    锁消除:JIT及时编译器经过判断后发现你不需要加锁,将锁消除了。

    4.4 小结

    口述:重量级锁是在不同线程在同一时刻竞争锁资源时用的,底层是用到的monitor,将锁对象的对象头的markword指向monitor对象,monitor中包含了owner表示谁可以拥有锁,当后续线程也想获得锁时会进入entrylist阻塞,轻量级锁是在同一时刻没有线程竞争时会采取轻量级锁的方式,轻量级锁其实在线程的栈帧中会有锁记录的这样一个结构,这个结构中包含两部分内容,一部分记录锁对象的地址指向锁对象,一部分是与锁对象的对象头中的markword交换用的,这个cas交换操作就是将当前线程的地址和oo与对象头的markword交换,交换成功的话当前线程就拿到了锁,交换失败的话其实有两种情况,第一种重入操作,同一个线程又要获得锁,这时也会在栈帧中创建锁记录,只不过将其设置 为null,表示重入,第二种情况就是不是我这个线程获取锁的而是另一个线程对锁竞争,此时就得升级锁为重量锁,应为出现了锁的竞争,此时会创建monitor对象,将竞争的这个新线程放入entrylist中阻塞等待
    。偏向锁也是一种锁,它其实是对轻量级锁的改进,其实就是对cas交换操作改进,不需要交换了,而是直接在锁对象的对象头中记录当前线程的id,那后续重入的时候就不需要进行 cas交换了,而是直接判断markword是否是自己的线程id,会更高效,但是,偏向锁会覆盖原来对象头中的markword的信息,比如hashcode,所以当再次调用锁对象的hashcode方法时会导致偏向锁失效。

  • 相关阅读:
    Facebook宣布关闭面部识别系统,删除超过10亿用户的数据
    Linux crontab命令
    报错:npm ERR code EPERM
    为什么 NGINX 的 reload 不是热加载?
    Linux基本命令之修改主机名、用户名、密码
    pandas数据离散化
    电脑上的文件怎么自动备份到网盘?
    【34】理解虚拟机:你在云上拿到的计算机是什么样的?
    数据结构-作业5
    AD360荣获2023 Fortress奖:卓越的身份验证和身份管理解决方案
  • 原文地址:https://blog.csdn.net/qq_51240148/article/details/133547723