Java并发编程-共享模型之Monitor管程悲观锁

文章目录

目录

文章目录

前言

一、线程安全问题

二、线程安全类的不安全问题

三、synchronize

四、线程活跃性

五、ReentrantLock

六、park/unpark

七、Java六状态转移关系

总结


前言

主要介绍了Java并发编制中的共享模型之Monitor管程悲观锁。


一、线程安全问题

  • 出现线程安全问题的情况
    • 出现条件(两个一起)
      • 临界资源的共享
      • 多线程同一时刻竞争某个临界资源
    • 线程不安全定义
      • 多线程因时间片轮转而发生上下文切换时,同时写或同时读写(同时读没事)的指令乱序导致临界资源出错的现象
  • 成员变量、静态变量
    • 成员变量属于对象实例数据存在线程共享的堆中
    • 静态变量属于类数据存于线程共享的方法区中
      方法区小扩展
      JDK8以后方法区中的常量池和静态变量都移动到堆中
      方法区={ 常量池 + 静态变量 + 编译后的方法指令字节流 + 类型数据(父类、接口等) }
    • 两者都是线程共享的数据,出现多线程同时访问会出现线程安全问题
  • 局部变量
    • 局部变量是方法参数和方法内部变量这类数据,存于线程独占的栈帧中局部变量表类,作用域只在方法内部
      局部变量必须初始化才能使用,成员变量可以在准备阶段零初始化直接使用
    • 局部变量引用的暴露问题
      • 局部变量引用存在栈帧中,指向存在堆中的实例
      • 若某子类重写父类方法(父类方法以局部变量引用作为参数,会传递给子类重写方法(同名同参同返回)),在子类的重写方法中创建了新线程,并在新线程中对传入的局部变量引用参数指向的堆中实例进行修改,这样除了调用父类方法的线程可以实现对其方法内部的局部变量引用访问,子类重写方法中创建的新线程也能得到局部变量引用并对堆中实例修改,造成了局部变量引用的多线程共享泄露问题
      • 用final修饰类(无法实现继承),用private修饰类中方法(对子类不可见不可重写),防止子类方法重写覆盖父类方法导致局部变量引用暴露问题

二、线程安全类的不安全问题

  • 常见线程安全类
    • Vector、HashTable,java.util.concurrent包下的类
    • 这些类的方法单独使用时都是线程安全的(内部有synchronize锁住),但是方法组合在一起就可能出现线程不安全状况
  • 不可变类
    • String、Integer
    • 每次写操作都不会作用在原对象上,而是操作在一个新的复制值上,多线程写写、读写并发不会导致共享同一个对象
    • 这些类的内部状态不可改变,只会出现线程共享读而不会出现共享写或者共享读写问题,不会出现线程安全问题,但是需要用final修饰String和Integer类(所有基本数据类型包装类都有final修饰),防止被子类继承重写其内部线程安全方法造成局部变量引用暴露的线程不安全问题

三、synchronize

  • 关键
    • JVM实现,C++代码编写,关键字级别用于锁对象(为对象关联的Monitor是锁),解决多线程互斥(共享资源访问原子性)(Owner+EntryList)同步通信(条件变量)(WaitSet)问题
  • 编程人员实现使用
    synchronize只能锁对象,锁成员方法时等同于锁调用方法的实例;锁static静态方法时等同于锁类对象(即.class对象)
    • 对对象上锁
      • 对含有访问共享成员变量、静态变量的临界区代码块上锁,实现多线程安全访问共享资源
      • 想实现多线程互斥访问临界区,每个线程必须锁同一个对象,将被锁的对象设置为final常量,保证被锁的对象引用不可变,永远锁住同一个对象
      • 面向过程
        • 创建一个final常量的Object对象lock,每个线程访问临界代码区前对lock上锁synchronize(lock),对象关联Monitor线程变成Owner,若某线程访问时发现已经有线程成为lock锁Owner则会进入EntryList阻塞队列等待,缺少必要条件会wait被锁的对象lock,放弃锁进入WaitSet等待,待必要条件满足且获取锁的线程释放锁后,所有EntryList中需要锁的线程会竞争锁的使用权
      • 面向对象
        • 将临界资源作为类的成员变量,访问临界资源的临界区代码块封装在类的方法中,并在方法中synchronize锁上方法的this指针(即调用方法的实例对象),在主线程中创建类的实例对象,并在不同线程中通过该实例对象调用其方法互斥访问临界资源
    • 对方法上锁
      • 面向对象时,也可以直接对类中的成员方法上锁,此时synchronize的还是方法的this指针(即调用方法的实例对象)
      • 对静态方法上锁时,锁的是类对象(.class对象)而不是实例对象
        直接用类名或者实例对象都可以调用静态方法
      • 两个线程一个用实例对象调用其成员方法(锁实例对象),一个用实例对象调用其静态方法(锁.class类对象)不会陷入阻塞,因为他们锁的不是一个对象
  • 原理
    • 实现互斥(共享资源访问原子性)(Owner+EntryList)
      • synchronized的底层实现过程
        获得锁的线程,就算CPU执行时间片已经到达,让出CPU也不会让出锁
        ​线程会一直持有锁直到临界区代码执行完成,或者对象调用wait方法等待必要条件满足时才释放锁
        • 先上偏向锁(可偏向时),只允许第一次访问临界区的线程a获得锁,若线程解锁后出现线程b请求锁访问临界资源
        • 撤销偏向锁,锁升级为轻量级锁,对象头中Mark Word与线程b栈中栈帧记录的Lock Record地址交换位置,使被锁对象与线程锁记录相关联,若线程b持有锁的同时出现线程c请求锁访问临界资源
        • 发生锁膨胀(即多线程同时竞争锁访问临界资源),撤销轻量级锁,锁升级为重量级锁,使对象关联一个Monitor管程(对象头Mark Word记录Monitor地址,Monitor中会存Mark Word原内容),线程b置为Monitor的Owner,线程c进入Monitor的EntryList阻塞等待线程b执行完成释放Owner
        • 线程b执行完成后,Monitor的Owner变为Null并唤醒EntryList中的线程,这些线程竞争得到锁成为Monitor的Owner
      • 重量级锁
        锁对象头Mark Word记录Monitor地址
        多个线程同时竞争锁对象
        • 使用条件
          • 有多线程同时访问一个共享资源的竞争现象时,启动重量级锁,为每个对象关联一个Monitor管程
        • Monitor
          • 字节码内容大概为
            • 拿到Monitor的Owner为Null则将当前线程与Owner联系起来(不为Null则进入EntryList阻塞等待),执行临界代码块,将Owner置为Null并唤醒EntryList中线程,若在执行期间出现任何异常会被捕获,异常处理代码会实现锁释放(将Owner置为Null)并报异常,获得锁后一定会释放锁不会永远占用锁
          • 组成
            • Owner
              • 记录当前获得锁的线程
              • Owner为null时,线程可以获得锁
            • EntryList
              • 记录等待锁释放的线程
              • Owner不为null时,线程进入EntryList
            • WaitSet
              • 记录获得锁后因为缺少某个必要条件无法顺利执行,调用wait方法放弃锁并阻塞等待必要条件的线程
              • 获得必要条件后,线程会离开WaitSet进入EntryList,在占用锁线程释放锁、Owner为null时与其他线程一起竞争锁
        • 自旋优化
          • 线程b持有锁时线程c请求锁失败后不会立即进入EntryList阻塞,因为阻塞需要放弃CPU执行需要十分耗费时间的线程上下文切换过程(保存当前线程执行状态并将下一个线程的状态写入CPU),会让线程c多次自旋请求锁,若线程b在线程c自旋请求过程中释放了锁,则线程c会立即获得锁而节省了线程上下文切换的时间损耗
          • 必须在多核条件下,单核CPU只能用于获得锁的线程b的执行,线程c自旋也是浪费时间
          • 自旋优化是自适应的,开发人员无法控制,JVM根据之前自旋成功的概率自行调整自旋时间
      • 轻量级锁
        锁对象头Mark Word记录锁记录Lock Record地址
        多个线程交替获得锁对象
        • 使用条件
          • 多线程交错不同时间访问同一个共享资源(但不竞争)时,启动轻量级锁,无需为对象关联Monitor,只记录一个Lock Record
        • Lock Record
          • 线程栈的栈帧中记录Lock Record
          • 组成
            • Lock Record地址
              • 与对象头中的原始Mark Word运行时数据(hashcode,age,bias)交换
            • Object Reference
              • 指向锁对象
        • 可重入
          • 同一个线程可以反复获得锁,第一次获得锁会创建一个Lock Record记录对象地址和对象Mark Word原内容,后来获得锁会创建一个为null的Lock Record
          • 一个锁一直被一个线程使用会频繁创建Lock Record很浪费资源,可以直接将锁置为仅供该线程使用,即在被锁对象的对象头Mark Word位置记录线程ID(=设为偏向锁),有别的线程也来获得锁时(不同时间使用锁不构成竞争)再升级为轻量锁,同时获得锁(竞争)时再升级为重量级锁
        • 锁膨胀
          • 线程b获得锁执行过程中线程c请求锁(多线程同时请求锁而产生的竞争)发生锁膨胀,会撤销轻量级锁升级为重量级锁,为对象关联一个Monitor对象并将线程b与Owner关联,线程c进入EntryList
      • 偏向锁
        锁对象头Mark Word中hashcode部分记录第一次获得锁的线程ID或者在批量重偏向过程中第二次获得锁的线程ID
        一个线程独占锁对象
        • 使用条件
          • 只有一个线程访问共享资源,启动偏向锁,使这个锁只给第一次使用它的线程使用,锁对象头的Mark Word(hashcode,age,bias)中hashcode部分记录线程ID
        • 对象头中hashcode部分记录线程ID
          • 只在对象未计算hashcode(对象使用hashcode时才会计算它)时才可以启用偏向锁,计算hashcode之后锁对象头的Mard Word没有空间记录线程ID
        • 偏向锁的优化
          • 撤销
            • 调用hashcode
              • 计算hashcode之后对象头的Mard Word没有空间记录线程ID,被迫撤销偏向锁升级为轻量级锁
            • 其他线程c请求锁
              • 与线程b交替在不同时间使用锁
                • 升级为轻量级锁(Lock Record)
              • 与线程b同时使用锁
                • 升级为重量级锁(EntryList)
            • wait/notify
              • 线程缺少必要条件时,会用对象调用wait放弃锁进入Monitor的WaitSet等待,这时候必须升级为重量级锁才能有WaitSet,而撤销偏向锁
              • 该线程无法再占用锁,在必要条件满足前,锁只能被其他线程占用,所以必须撤销偏向锁,让锁不只能被该线程访问
            • 批量撤销
              • 撤销和重偏向总次数太多超过第二阈值(40)时,JVM会停止使用偏向锁(太多线程想获得锁,不适合再让锁偏向于被某个特定线程使用),对象锁升级为轻量级锁
          • 批量重偏向
            • 撤销偏向锁、重置轻量级锁次数太多,超过第一阈值(20)时,JVM会把接下来的撤销升级操作变为重偏向操作,直接将后面的锁对象头重偏向,记录新请求锁的线程ID
      • 锁撤销
        • java语言同时进行编译和解释,对经常使用的代码会在及时优化编译器中优化编译后执行,若代码中存在没有实际效用的锁(锁住的代码实际没有造成线程安全问题,没有被多线程同时访问)会将其撤销以提高效率,因为获得锁、释放锁会降低效率
    • 实现同步通信(条件变量)
      • wait/notify
        • 作用在锁对象上(lock.wait()/lock.notify)
        • 过程&原理
          • 线程获得锁后,缺少条件时会让锁对象调用wait方法,使线程释放锁进入Monitor的Waitset中阻塞等待
          • 等线程满足条件时,锁对象调用notify方法(随机唤醒WaitSet中某个阻塞线程),notifyAll方法(唤醒所有WaitSet中阻塞线程),线程将从Waitset中退出,进入EntryList中与其他线程竞争锁
          • 竞争成功状态变为Runable成为Monitor的Owner,竞争失败状态为Block依旧在EntryList中等待Owner为null时再次被唤醒进行竞争
        • 使用模板
        • 与sleep的区别
          • sleep
            • sleep作用在线程上(thread.sleep)、sleep不需要先执行synchronize获得锁,若线程synchronize获得锁后再sleep,线程不会释放锁
            • 线程sleep后,状态变为Waiting,睡眠时间到、或被interrupt打断捕获异常可以结束阻塞被唤醒
            • 唤醒后线程状态变为Runnable
          • wait
            • wait作用在对象上(object.wait)、对象必须先上锁关联Monitor才可以执行wait进入Monitor的WaitSet中阻塞等待,上锁的对象调用wait后线程会释放锁
            • 对象wait后,等待时间到、或被interrupt打断捕获异常、或对象调用notify/notifyAll可以结束阻塞被唤醒
            • 唤醒后线程状态变为Runnable(竞争锁成功)、Block(竞争锁失败)

四、线程活跃性

  • 死锁
    • 细密度锁
      • 线程需要访问多个共享资源,若将多个共享资源封装在一个锁内,会降低线程并发度
      • 需要所有共享资源的线程获得锁后,其它只需要访问其中某个共享资源的线程必须等待它执行完,导致它们之间无法并发执行
      • 将每个共享资源单独封装为一个锁,提高锁的细密度,多个只需要访问某个共享资源的线程可以并发执行,需要所有共享资源的线程获得多把锁后可以顺利执行,极大的提高并发度
    • 出现死锁的情况
      • 多个线程“请求并保持”,保持自己持有的锁 + 请求对方手里的锁,导致所有线程都无法得到顺利执行所需要的所有锁,而陷入一起长时间阻塞等待的现象
    • 解决办法
      • 规定获得锁的顺序(会出现饥饿问题)
      • 线程1得到a锁请求b锁,线程2得到b锁请求a锁,让线程1得到a但是长时间得不到b时先放弃a锁让给线程2,等线程2执行完成后再尝试请求锁
  • 饥饿
    • 出现饥饿的情况
      • 某些线程因为优先级低,长时间无法分配到时间片的现象
      • 规定获得锁的顺序解决死锁问题时,可能出现饥饿
    • 解决方法
      • 公平锁,EntryList中的线程争抢CPU时遵从先到先得,而不是随机的(一般不开启,浪费资源)
  • 活锁
    • 多个线程互相改变对方退出循环的条件,导致一起无法正常退出,长时间占用CPU进行无效执行的现象

五、ReentrantLock

  • 关键
    • Java层面编写实现,对象级别调用lock方法(ReentrantLock实例对象本身就是锁),解决多线程互斥(共享资源访问原子性)同步通信(条件变量)(Condition)问题
    • 底层原理是继承同步器AQS,实现其方法,调用park、unpark结合使用cas管理锁状态state,实现互斥独占锁
  • 使用模板
  • 与synchronized比较的特点
    • 都可重入
      • 同一线程可以重复获得同一个锁
    • 可中断
      • reentrantLock.lockInterruptibly(),在请求锁的时候可以被其他线程用interrupt打断
      • 使用模板
    • 可设置超时时间
      • reentrantLock.tryLock(n,TimeUnit),尝试请求锁若超过规定时间则放弃请求
      • 使用模板
      • 解决死锁问题
        • chopsticks Extends ReentrantLock
        • 获得left后进入第一个try,尝试获得right,失败会执行第一个try对应的finally释放left;成功时进入第二个try去eat,然后执行第二个try对应的finally释放right,最后执行第一个try对应的finally释放left
        • 使用模板
    • 可设置公平锁
      • 阻塞队列中的线程争抢CPU时遵从先到先得,而不是随机的(一般不开启,浪费资源)
    • 支持多个条件变量(实现同步通信)
      • reentrantLock.newCondition();reentrantLock.lock()后,condition1.await(),condition1.signal(),condition1.signalAll(),可以让等待不同条件变量的线程进入不同conditionObject“房间”等待,再用这个条件变量唤醒,即可特定唤醒等待它的单个或所有线程(notifyAll可能会唤醒等待其他条件变量的线程)
      • 使用模板

六、park/unpark

  • 作用在LockSupport对象上(LockSupport.park/LockSupport.unpark(thread1)),可以指定唤醒某个特定线程
  • 应用JUC中各类锁上锁时,在底层被调用,用于阻塞线程
  • 过程&原理
    • 先park再unpark
      • 线程判断没有“干粮”counter=0,先进入mutex的condition“帐篷”中休息
      • 线程unpark补充“干粮”counter=1,线程被唤醒,离开“帐篷”继续执行,“干粮”被消耗counter=0
    • 先unpark再park
      • 线程unpark补充“干粮”counter=1(只可以unpark一次,counter只能=1/0)
      • 线程park时发现"干粮"还有counter=1,消耗掉它counter=0并继续执行,不会进入”帐篷“阻塞休息
  • 与wait、sleep区别
    • wait作用object对象,需要锁,notify唤醒时释放锁,不能指定唤醒的线程,必须先wait再notify,被interrupt后还可以继续wait(打断信号会被清空)
    • park作用LockSupport,不需要锁,unpark唤醒不会释放锁,可以指定唤醒的线程,可以先unpark再park,被interrupt一次后不会再被park了(打断信号不会被清空),但是若配合调用interrupted获取打断标记后,会清空打断标记,可以再次被park
    • sleep作用于thread线程,不需要锁,睡眠时间到或interrupt唤醒不会释放锁,唤醒的一定是调用它的线程,被interrupt后还可以继续sleep(打断信号会被清空)

七、Java六状态转移关系


总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值