管程模型与锁

先上一张图:
在这里插入图片描述
前面的文章中已经学习了Thread、Runnable/Callable、FutureTask和ThreadPool相关的知识;现在线程基本讲完了,该轮到“锁”了。

什么是锁?

学习多线程是为了提供任务执行效率,但多线程会导致线程安全的问题。所谓的线程安全问题,可以理解为多个线程操作同意资源,数据不一致的问题。

为什么加锁就能解决线程安全的问题呢?
加锁的核心就是互斥,就是让线程之间相互排斥。

又怎么实现线程的互斥呢?
引入中间人即可。

意,这是个非常简单且伟大的思想。在编程世界中,通过引入“中介”最终解决问题的案例不胜枚举,包括但不限于Spring、MQ。在码农之间,甚至流传着一句话:没有什么问题是引入中间层解决不了的。

锁就是线程与线程之间的中间人,多个线程操作加锁数据前都必须要征求中间人的意见。
锁在这里的角色就是守门员,是唯一的访问入口,在JDK中常见的两种锁是:

  • synchronized关键字
  • AQS(ReentrantLock)

Java对象内存结构

要想了解synchronized,首先要了解java对象的内存模型。
在这里插入图片描述
通过上图可以看到java的内存结构分为几块:

  • Mark Word (锁相关)
  • 元数据指针(class pointer 指向当前实例所属的类)
  • 实例数据(我们平常看到的仅仅是这一块)
  • 对齐(padding,和内存对齐有关)
    看到这些的时候是不是有点懵:
  • 为什么任何对象都可以作为锁?
  • Object对象锁和类锁有什么区别?
  • synchronized修饰的普通方法使用的锁是什么?
  • synchronized修饰的静态方法使用的锁是什么?

这一切的一切,其实都是java对象内存结构中的Mark Word:
在这里插入图片描述
Mark Word 可以理解为是记录锁信息的标记,正因为每个Java对象都有Mark Word,而Mark Word能标记锁状态(把自己当做锁),所以Java中任意对象都可以作为synchronized的锁:

synchronized(person){
}
synchronized(student){
}

this锁就是当前对象,而Class锁就是当前对象所属类的Class对象,本质上也是java对象。synchronized修饰的普通方法,底层使用的是当前对象作为锁;synchronized修饰静态方法,底层使用Class对象作为锁。

如果要保证多个线程互斥,最基本的条件是使用同一把锁:

synchronized(Person.class){
    // 操作count
}

synchronized(person){
    // 操作count
}

上面的代码应该要避免。

什么是管程?

首先可以明确的是,管程不是Java专有的概念,也不是某个编程语言独有的概念,而是一种通用的解决方案——一种用于解决并发问题的模型。

管程的定义

管程是由自身内部的若干公共变量和所有访问这些公共变量的过程所组成的软件模式。

管程的组成部分
  • 管程内部定义的共享变量
  • 对数据结构(共享变量)进行操作的一组过程(方法)
  • 对管程的数据进行初始化的语句
管程的属性
  • 共享性:管程可被系统范围内的进程互斥访问,属于共享资源。
  • 安全性:管程的局部变量只能由管程的过程访问,不允许进程或其他管程直接访问,管程也不能访问非局部于它的变量
  • 互斥性:多个进程对管程的访问是互斥的,任一时刻,管程中只能有一个活跃进程
  • 封装性:管程内的数据结构是私有的,只能在管程内使用,管程内的过程也只能使用管程内的数据结构,进程通过调用管程的过程使用临界资源。

管程在java中的实现

synchronized锁

看一张图:
在这里插入图片描述
java中的任意对象都可以作为锁,这是为什么呢?

实际上,每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象加上锁之后,该对象头的Mark Word就被设置为指向Monitor对象的指针,也就是说,这个java对象就关联了一个Monitor对象,而Monitor就是java的管程模型,所以真正实现锁机制的并不是java对象本身,而是背后的管程。
在这里插入图片描述
synchronized的管程模型大致分为EntryList、Owner和WaitSet。
抢到锁的线程记录为Owner,抢不锁的线程进入EntryList等待,而waitSet是抢到锁但在同步代码中调用wait()方法的线程,就进入了waitSet中。

总结一下synchronized相关的管程模型(Monitor):
● 抢到锁的线程是Owner
● 没抢到锁的进入EntryList
● 抢到锁,但运行过程中因为条件不满足,调用wait()方法后进入WaitSet,等待notify()/notifyAll()

对于synchronize而言:
● 共享变量大概就是锁对象
● 条件变量等待队列只有一个WaitSet
● 入口等待队列就是EntryList
● 对外暴露的方法就是:wait()、notify()和notifyAll()
由于synchronized是隐式锁,所以并没有提供lock()和unlock()操作,JVM底层帮我们自动完成加解锁操作。

ReentrantLock锁

同样是基于管程模型,但synchronized和ReentrantLock底层实现并不相同。synchronized直接依赖于JVM和操作系统,引入了Monitor对象,而ReentrantLock底层则是AQS。

之前模拟阻塞队列时,我们迭代了多个版本。原先用synchronized实现了一版阻塞队列,但由于出现了“生产者唤醒生产者”的乌龙事件,于是把notify()改成了notifyAll(),后面干脆使用ReentrantLock替代了synchronized,而ReentrantLock.Condition也完美了解决了等待队列的问题。

回顾上面对管程模型的定义:

  • 共享变量(synchronized基于对象内存的markword,ReentrantLock基于AQS的state变量)
  • 对外暴露的操作
  • 对外暴露的操作

你会发现synchronized的Monitor实现,只有一个等待队列,无论基于什么原因发生等待,统统进入WaitSet,这样一来,当A条件满足要去唤醒当初因为A条件而等待的线程时,不得不把其他条件的等待队列也一并唤醒…简而言之,对等待队列的控制不够精确。

Doug Lea通过抽取封装AQS,实现了自己的EntryList,同时支持不同条件的等待队列Condition。获锁的线程如果当前不满足A条件,可以进入A Condition队列等待,不满足B条件则进入B Condition队列等待。唤醒的过程同样清晰,当A条件满足时,去A Condition队列唤醒即可。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值