JUC并发编程(三)

ThreadLocal(线程局部变量)

概念

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每个线程在访问ThreaLocal实例的时候都有自己的、独立初始化的副本。ThreadLocal实例通常时类中的私有静态变量,使用的目的是希望状态(例如:用户ID或事物ID)与线程关联起来

ThreadLocalMap

JVM内部维护了一个线程版的Map<ThreadLocal,value>通过ThreadLocal对象的set方法,结果把ThreadLocal对象本身当作key,放进了ThreadLocalMap中,每个线程要用到这个T的时候,用当前的线程去Map里面去取,通过这样每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底清除,在并发模式下是绝对安全的变量

内存泄漏问题

不再被使用的对象或者变量占用的内存无法回收,就是内存泄漏

为什么要用弱引用

在这里插入图片描述

  • 这块举一个例子,其实Thread,ThreadLocal,ThreadLocalMap三者的关系就像是人、身份证、身份证具体的信息,当我们人正常生老病死了以后,身份证信息也会跟着销毁,如果不进行销毁,那会产生原来越多的无效信息(计算机中的产生越来越多的无法回收的内存),所以这块采用弱引用。
  • ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用它,那么在gc的时候,这个ThreadLocal势必会被回收,这样一来ThreadLocalMap中就会出现键为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程迟迟不结束的话(比如正好采用线程池),这些key为null的Entry的value会存在一条强引用链,还会有内存泄露的隐患。解决办法:如果某个ThreadLocal对象不使用的话,手动采用remove方法来删除它。
  • 使用注意事项
    • 使用ThreadLocal.withInitial()进行初始化
    • 尽量采用static修饰
    • 使用完手动remove
总结
  • ThreadLocal不解决线程间共享变量的问题
  • ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程都有一个独立的属于自己的Map并维护了ThreadLocal对象与具体实例映射,该Map只有它的持有线程才可以访问,所以不存在线程安全及所的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

对象内存布局(会涉及一定的JVM知识,最好有JVM基础)

在HotSpot虚拟机中,对象在堆内存的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(padding)。

  • 对象头(普通对象无实例数据16字节)

    • 对象标记MarkWord

      存储内容标志位状态
      对象哈希码、对象分代年龄01未锁定
      指向锁记录指针00轻量级锁定
      指向重量级锁的指针10重量级锁定
      空,不需要记录信息11GC标记
      偏向线程ID、偏向时间戳、对象分代年龄01可偏向
      • 哈希码
      • GC标记
      • GC次数
      • 同步锁标记
      • 偏向锁持有者
    • 类元信息(类型指针):就是它是由哪个类生成的,例如User user = new User() 就是左边User

    • 数组长度(只有数组对象会存在该属性)

  • 实例数据:类中的一些属性数据信息以及父类数据信息等

  • 对齐填充(保证8字节的倍数)

Synchronized与锁升级

锁升级过程
  • 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

    img

  • Synchronized在Java5以前是操作系统级别的重量级操作,如果锁的竞争比较激烈的话,性能会下降,因为监视器锁是依赖于底层操作系统的Mutex Lock(系统互斥量)来实现,java的线程是映射与操作系统原生线程之上的,如果要阻塞或唤醒线程的话,会涉及到用户态和内核态的一个转化,这种消耗会耗费大量的资源,所以就引入了偏向锁和轻量级锁

  • 轻量级锁:MarkWord存储的是指向线程栈中的Lock Record的指针

  • 重量级锁:MarkWord存储的是指向堆中monitor对象的指针

  • 无锁:在这里插入图片描述

    偏向锁
    • MarkWord存储的偏向的线程ID
    • 单线程竞争,当线程A第一次竞争到锁时,通过修改MarkWord中的偏向线程ID、偏向模式
    • 当一段同步代码块一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问会自动获得锁
    • 会偏向第一个获得锁的线程,如果后续没有其它线程访问的话,则持有偏向锁的线程永远不会触发同步。也即偏向锁在没有现成竞争的情况下消除了同步语句,连CAS操作都不做,直接提高程序的性能
    • 锁被第一个线程拥有以后,这个线程就是偏向线程,在MarkWord中会存储偏向线程的ID,后续偏向线程在进入或者退出同步代码块的话,不需要再去加锁和释放锁,而是直接去检查锁中的MarkWord偏向线程ID是否等于自己,如果相等就不需要再去获得锁,无需再去CAS去更新对象头中的偏向锁ID。如果不相等就说明产生了线程的竞争,就会尝试使用CAS对对象头的偏向锁ID进行更新,如果竞争成功,偏向锁ID为新线程的ID,如果竞争失败,可能会变为轻量级锁,才能保证线程公平竞争锁
    • 偏向线程只遇到其它线程竞争才会释放偏向锁,否则线程不会释放偏向锁
    • 不会涉及到系统级别的操作,用户态向内核态的转换
    偏向锁的撤销
    • 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由之前的偏向线程持有,继续执行同步代码块,而正在竞争的线程会自旋等待获得该轻量级锁
    • 第一个线程已经执行完synchronized方法(退出同步块),则将对象头设置为无锁状态并撤销偏向锁,重新偏向
    轻量级锁
    • 多线程竞争,但是任意时刻最多只能有一个线程竞争,即不存在锁竞争太激烈的情况,也就没有线程阻塞
    • 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,先自旋,不行再阻塞
    • 当关闭偏向锁功能或者多线程竞争偏向锁会导致偏向锁升级为轻量级锁
    • 自适应自旋锁
      • 原理:线程如果自旋成功了,那么下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也会大概率成功。反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转
      • 自适应意味着自旋的次数不是固定不变的,而是根据同一个锁的上一次自旋时间和拥有锁线程的状态决定的
    • 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争时才释放锁
    重量级锁
    • 有大量的锁参与竞争,冲突性很高
    • Java中Synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。当线程执行至monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor
    Synchronized和hashcode的关系
    • 无锁:当一个对象已经计算过一致性哈希码之后,他就再也无法进入偏向锁的状态了
    • 偏向锁:而当一个对象在处于偏向锁的状态的话,有收到一致性哈希码请求时,他的偏向状态会被立即撤销,膨胀为重量锁
    • 轻量级锁:升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象MarkWord拷贝,该拷贝中可以包含一致性哈希码,所以轻量级锁可以和一致性哈希码共存,gc年龄等信息也在于此,释放后会将这些信息重写入对象头中
    • 重量级锁:升级为重量级锁时,MarkWord保存的重量级锁指针,代表着重量级锁的ObjectMonitor类里有字段记录非加锁状态的MarkWord,锁释放后也会将信息重写到对象头中
    锁的优缺点对比

    在这里插入图片描述

    锁消除和锁粗化
    • 锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要 加锁,就会进行锁消除。

    • 锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

      public static void main(String[] args) {
          Object o = new Object();
          new Thread(()->{
              synchronized (o) {
                  System.out.println(1);
              }
              synchronized (o) {
                  System.out.println(2);
              }
              synchronized (o) {
                  System.out.println(3);
              }
              
              
              //JIT编译器优化后的结果
              synchronized (o) {
                  System.out.println(1);
                  System.out.println(2);
                  System.out.println(3);
              }
              
          },"t1").start();
      
      }
      

AbstractQueuedSycnchronizer之AQS(抽象队列同步器)

概念
  • 是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给谁的问题
  • 整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
原理
  • 抢到资源的线程直接处理业务,抢不到资源的必然涉及到一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能在等候区排队),但等待线程仍然保留获取锁的可能且获取锁的流程仍在继续(候客区的顾客也在等着叫号,轮到了再去办理窗口办理业务)
  • 如果共享资源被占用了,就需要一定的阻塞等待唤醒机制来保证锁的分配,这个机制主要用的是CLH队列的变体实现,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步器抽象的表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象(Node),通过CAS、自旋以及LockSupport.park()方式,维护state变量的状态,使并发达到同步的效果
AQS内部体系架构

在这里插入图片描述

AQS自身属性和Node节点介绍

在这里插入图片描述

源码解析可以看一下b站尚硅谷周阳老师的JUC并发编程(AQS之源码分析)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值