JUC(共享模型之管程、synchronized、wait、park、活跃性、renetrantlock、条件变量)

临界区

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

竞态条件:多个线程在临界区内执行,由于代码执行序列不同导致结果无法预测,称之为竞态条件

synchronized解决方案

实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码对外界是不可分割的,不会被线程切换打断

方法上的synchronized:

public方法上,相对于锁对象是this(当前对象)

static方法上,相当于锁当前类对象this.class

方法的访问修饰符是有意义的,private,final防止子类重写,可以一定程度避免线程安全问题

常见的线程安全类

String

Integer

StringBuffer

Random

Vector

Hashtable

JUC包下的类

线程安全是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

不可变类线程的安全性:String、Integer等都是不可变类,内部的状态不可改变

外星方法:抽象方法(实现可能是并发不安全的)

String为什么设置为final,因为String是线程安全的,但是如果我有子类,子类可能重写方法等,破坏我的安全性

Monitor概念

对象头(32位为例)

普通对象:Object Header(64字节):Mark Word(32位)+ Klass Word(32位)

状态 Mark Word 内容 锁标志位
无锁(Normal) `hashcode:25 age:4 biased_lock:0 01` 01
偏向锁(Biased) `thread:23 epoch:2 age:4 biased_lock:1 01` 01
轻量级锁(Lightweight) ptr_to_lock_record:30 00
重量级锁(Heavyweight) ptr_to_monitor:30 10
GC标记(Marked for GC) ... 11

Monitor:监视器、管程

每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向monitor对象的指针

Monitor对象包含:owner(获取锁对象)、EntryList(阻塞队列)、WaitSet(与wait/notify相关)

synchronized

轻量级锁

一个对象虽然有多线程访问,但多线程访问的时间是错开的,那么可以使用轻量级锁来优化

过程:

一个栈帧可以有多个 Lock Record,用来支持多个锁和可重入,本质是一个"栈式结构"

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结果,内部可以存储锁定对象的Mark Word
  • 让锁记录中Object reference 指向锁对象,并尝试用CAS替换Mark Word,将Mark Word的值存入锁记录
  • 如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示该线程给对象加锁
  • 加锁失败
    • 如果是其他线程已经持有了该Object的轻量级锁,这是表示有竞争,进入锁膨胀过程
    • 如果是自己执行了synchronized锁重入,那么再添加一条LockRecord作为重入计数
  • 当退出synchronized代码块,如果锁记录值为null,表示重入,重置锁记录,表示重入计数减一
  • 当退出synchronized代码块时锁记录不为null,这时使用CAS将mark word的值恢复给对象头
    • 成功:解锁成功
    • 失败,进入重量级锁解锁流程

锁膨胀

如果在尝试加锁的过程中,CAS操作失败,这时一种情况就是有其他线程对此线程加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当thread1进行轻量级加锁时,thread0已经对该对象加了轻量级锁
  • 这时thread1加轻量级锁失败,进入锁膨胀流程
    • 即为object对象申请monitor锁,让object指向重量级锁的地址
    • 然后自己进入monitor的entrylist(Blocked状态)
  • 当thread0退出同步块解锁时,使用CAS将mark word的值恢复给对象头,失败。这时会进入重量级锁解锁流程,按照monitor地址找到monitor对象,设置owner为null,唤醒entrylist中的blocked线程

自旋优化

自旋只会发生在"轻量级锁竞争失败"阶段,并不是所有锁竞争失败都会自旋。

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

偏向锁

轻量级锁没有竞争时,每次重入仍然需要执行CAS操作

java6引入偏向锁来进一步优化,只有第一次使用CAS将线程ID设置到对象的mark word头,之后发现这个线程id是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归线程所有

偏向状态

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword值为ox05即最后3位为101,这时它的thread/epoch/age都为0
  • 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加vm参数 -XX:BiasedLockingStartpDelay=0来禁止延迟
  • 如果没有开启偏向锁,那么对象创建后,markword值为0x01即后三位为001,这时它的hashcode/age都为0,第一次用到hashcode时才会赋值

偏向锁撤销

  • 调用对象hashcode
  • 其他线程使用锁对象
  • 调用wait/notify(重量级锁才有)

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程t1的对象仍然有可能重新偏向t2,重偏向会重置对象的thredID

当撤销偏向锁阈值超过20次后,jvm会觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程

批量重偏向(Bulk Rebias)是"针对同一类(Class)的多个对象"进行的,而不是单个对象。

批量撤销

当撤销偏向锁阈值超过40次后,jvm会觉得,自己确实偏向错了,根本就不应该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

锁消除

JIT发现加不加锁一样,直接消除锁(比如用局部变量作为锁对象)

wait/notify

  • owner线程发现条件不满足,调用wait方法,即可进入waitset变为waiting状态
  • blocked和waiting到的线程都处于阻塞状态,不占用CPU时间片
  • blocked线程会在owner线程释放锁时唤醒
  • waiting线程会在owner线程调用notify或notifyall时唤醒,但唤醒后并不意味着立即获得锁,仍需进入entrylist重新竞争

相关API

  • wait():让进入object监视器的线程到waitset等待
  • notify():在object上正在waitset等待的线程中挑一个唤醒
  • notifyAll():让object上正在waitset等待的线程全部唤醒
  • wait(long timeout):设置最长等待时间

都是线程之间进行协作的手段,属于object对象的方法,必须后的此对象的锁,才能调用

sleep和wait的区别

  • sleep是Thread方法,而wait是object的方法
  • sleep不需要强制和synchronized配合使用,但wait需要
  • sleep在睡眠的同时,不会释放对象锁,而wait会释放对象锁

模式之保护性暂停

即guarded suspension,用在一个线程等待另一个线程的执行结果

要点:

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个guardedobject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(生产者-消费者)
  • JDK中,join的实现、future的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

解耦等待和生产

通过中间类解耦

异步模式之生产者/消费者

  • 与前面的保护性暂停中的guardobject不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费线程资源
  • 生产者仅负责生产结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK中各种阻塞队列,采用的就是这种模式

park/unpark

与wait相比

  • wait必须配合object monitor一起使用
  • park是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyall是唤醒所有等待线程,就不那么精确
  • park,可以先unpark

每个线程都会关联一个parker对象,由_counter,_cond,_mutex组成

多把锁

给互不相关的共享资源加不同锁

活跃性

死锁

一个线程需要同时获取多把锁,这时就容易发生死锁

定位:可以使用jconsole,或者使用jsp定位线程,然后用jstack

哲学家就餐问题

活锁

两个线程互相改变对方的结束条件,最后谁也无法结束

饥饿

教程定义:一个线程优先级太低,始终得不到CPU调度执行,也不能够结束

ReentrantLock

相比synchronized具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

都支持可重入

基本语法

对象级别

通过try-finally保证释放锁

获取锁:lock()

释放锁:unlock()

可重入

同一个线程如果首次获得这把锁,那么他就是这把锁的拥有者,因此有权利再次获取这把锁。

如果是不可重入锁,那么第二次获得锁时,自己也会被挡住

可打断

其他线程可以使用interrupt打断阻塞

lock.lockInterruptibly()

其实就是加入一种机制,可以停止无限制等待

锁超时

tryLock():获得锁返回true,获取锁失败返回false,直接尝试获取,不等待

tryLock(timeout,TimeUnit):在指定时间内尝试获取锁

tryLock可以被打断

锁超时解决银行家算法

通过tryLock(),获取一把锁后,没有获得另一把锁,我会释放第一把锁,重新尝试

公平锁

reentrantlock(boolean):传参数true,改为公平锁

公平锁一般没有必要,会降低并发度,通过tryLock就可以

条件变量

synchronized中也有条件变量,就是waitset休息室,当条件不满足进入waitset等待

reentrantlock的条件变量比synchronized强大在于,他是支持多条件变量

  • synchronized是那些不满足条件的线程都在一间休息室等消息
  • reentrantlock支持多休息室,有专门等各种资源的休息室,唤醒也可以按休息室来唤醒
  • await前需要获得锁
  • await执行后,会释放锁,进入conditionObject等待
  • await的线程被唤醒(或打断,或超时)需重新竞争锁
  • 竞争lock锁成功,从await后继续执行

创建条件变量(休息室):对象.newCondition();

线程进入条件变量:condition.await();

唤醒条件变量中线程:condition.signal()

同步模式之顺序控制

交替打印(见上一篇文章)

相关推荐
kongba0072 小时前
学习COZE编程 / LangGraph 通用工作流项目 提示词模板
java·网络·学习
水云桐程序员2 小时前
一个GCC编译C语言命令的执行过程和错误输出:目录不存在:当前目录下没有output子目录|C语言编译的解决办法|Visual Studio Code
c语言·开发语言·vscode
程序员阿明2 小时前
spring boot3识别PDF图纸
java·spring boot·后端·pdf
小樱花的樱花2 小时前
4 文件选择对话框 QFileDialog
开发语言·c++·ui
blxr_2 小时前
Spring AI自定义Advisor
java·spring
xyq20242 小时前
Python File 方法详解
开发语言
kisloy2 小时前
【反爬虫】极验4 W参数逆向分析
java·javascript·爬虫
-Rane2 小时前
【C++】红黑树
java·开发语言
leaves falling2 小时前
深入浅出 C++ STL list:从入门到精通
开发语言·c++