Synchronized 分析

等待-通知机制:

类比现实世界中的就医流程:

1. 我们先去挂号,然后到就诊门口,排队等待叫号
2. 当我们的号码被叫时,我们进门诊看医生
3. 医生说我们需要验血,于是我们去抽血验血,他叫下一位排队的病人
4. 我们拿到验血报告后,重新分诊后在门诊口排队,等待叫号
5. 当医生再次叫到我们的号时,我们再进去看医生。
6. 看完后,我们告别医生,接着回家。

就医流程结合并发编程会是什么样的那?

1. 我们到门诊就诊,类似于线程去获取互斥锁,在门诊口排队等待叫号,类似于互斥锁被其他线程占有,当前线程被放到锁池队列,监听锁的状态;
2. 我们进门诊看医生,类似于通过竞争,线程获取到了互斥锁,进入临界区;
3. 医生需要验血报告,让我们去抽血,类似于线程要求的条件没有满足(已经获取锁了)
4. 我们告别医生,去抽血,类似于线程进入阻塞状态,互斥锁被释放,且线程被放到等待队列中;医生叫下一个病人,类似于锁池队列中的线程可以竞争锁了。
5. 我们拿到验血报告,类似于线程要求条件得到满足,系统把该线程从等待队列中提到锁池队列;我们到门诊重新分诊,类似于线程重新去获取互斥锁。
6. 看完后,我们告别医生回家。类似于临界区代码执行后,系统执行解锁,线程进入死亡状态。

总结:线程首先去获取互斥锁,如果锁被其他线程占有,则排队等待,当线程获取到锁后,进入临界区。如果线程要求的条件不满足时,释放锁并进入等待状态;当要求的条件满足时,通知等待的线程,去获取互斥锁。这就是完整的等待-通知机制,也就是wait-notify机制。

Java中的等待-通知机制

synchronized配合wait(),notify()

````
Object lock = new Object();
int count = 0;
boolean condition = false;
public void test(){
	synchronized(lock){ // 尝试加锁
		while(!condition){
			lock.wait()
		}
		lock.notify();
		count++;
	} // 解锁
}
public void changeCondition(){
	condition = true;
}
````

代码分析:synchronized可以保护临界区的代码在同一时刻只被一个线程访问。它是Java内置的互斥锁。当线程必须等待某一条件成立,才能访问共享资源时,可以在 synchronized{}内部调用 wait() 方法,释放占有的锁,进入等待状态,让出CPU时间片,阻塞线程。当条件满足时,调用 notifyAll() 唤醒等待队列中所有等待状态的线程,被唤醒的线程进入就绪状态,开始竞争互斥锁。

wait()和sleep()的区别:

  1. wait()释放线程持有的锁,sleep()不会;
  2. wait()必须在 synchronized{} 保护的临界区内调用,sleep则没有限制;
  3. wait() 是 Object的方法,sleep() 是 Thread 的方法。

notify()和notifyAll()的区别:

  1. notify只能随机唤醒等待队列中的一个线程,natifyAll能唤醒等待队列中所有的线程。唤醒的线程不一定能立即获取锁,它需要先改变等待状态,进入就绪状态,才能开始竞争锁。

Synchronized

为什么 synchronized(lock){临界区} 就可以实现临界区代码互斥的功能?

synchronized 关键字的底层实现是两个CPU指令: monitorentermonitorexit,lock只是一个普通的 java 对象。

Java对象在内存长什么样子?

Java对象在内存中的分布为:对象头,实例数据,对齐方式。其中,对象头描述了对象运行过程中的信息,如:锁状态(无锁,偏向锁,轻量级锁,重量级锁),哈希值,GC分代回收的年龄,偏向锁标志,锁标志位,计数器,当前线程指针…
当我们使用 new 关键字创建一个对象时,JVM会在堆中创建一个 instanceOopDesc 对象,这个对象包含了对象头(MarkWord)和实例数据。

MarkWord 的存储结构图

注意:在Java6之前,并没有轻量级锁和偏向锁,只有重量级锁,也就是我们常用的 synchronized的对象锁 。Java6之后,对 synchronized 进行了优化,添加了轻量级锁,偏向锁,以及锁自旋。目的是:避免 ObjectMonitor 的访问,减少"重量级锁"的使用,并最终减少线程上下文切换的频率。

图中,当锁的标志位为 10 时,锁的状态是重量级锁,对象头中用 30bit 来指向一个互斥量 Monitor。

什么是 Monitor ?怎么创建的?

Monitor 可以理解为一个同步工具,也可以描述为同步机制,它是保存在 MarkWorld 中的一个对象。通过下面方法创建:

bool has_monitor() const {
		return ((value) $monitor_value)!=0);
	}
	ObjectMonitor* monitor() const {
		assert(has_monitor(),"check");
		return (ObjectMonitor*) (value() ^ monitor_value);
}

上述代码告诉我们,Monitor其实是 ObejctMonitor 类型的实例对象。而且 Java中每个对象都会有一个对应的 ObjectMonitor 对象,所以,Java 中的所有 Object 都可以作为锁对象。这也解释了为什么 Obejct 中会有 wait 和 notify 方法。

ObjectMonitor如何实现同步机制那?

ObjectMonitor(){
	_count = 0; // 记录该线程获取锁的次数
	_owner = NULL; // 持有对象锁的线程
	_WaitSet = NULL; // 存储处于wait状态的线程的队列
	_EntryList = NULL; // 存储处于block状态(等待锁)的线程的队列
	_recursions = 0; //记录锁的重入次数
	....
}
  1. 当多个线程同时访问同步代码时,会先进入 _EntryList 线程队列中;
  2. 当某个线程通过竞争获取到对象锁后,_owner 设置为当前线程,_count 加1;_EntryList 中的线程进入阻塞状态(blocking)
  3. 当持有对象锁的线程,遇到 wait 方法后,该线程释放锁,_owner 重置为 NULL,_count减1,同时该线程进入 _WaitSet 中等待被唤醒,当前线程处于等待状态(wait);同时,_EntryList中阻塞状态的线程开始竞争锁对象。假设其中一个获取到锁,又会走第 2 步;
  4. 当持有对象锁的线程,调用 notify 时,处于_WaitSet集合中的线程被唤醒,加入_ENtryList队列中,同时将状态改为阻塞状态。注意:此时,当前线程依然持有锁。

总结:上诉过程,每次获取锁,释放锁,都会阻塞和唤醒线程,而线程切换需要CPU从用户态转入内核态,性能消耗比较严重。所以,JVM使用 自旋锁,偏向锁,轻量级锁,优化 synchronized。

synchronized修饰普通方法,静态方法和代码段,有什么区别?

  1. 修饰静态方法时,锁对象是当前类的 Class 对象,而 Class 对象在程序运行过程中只有一个,所以,访问该方法时会自动加锁与解锁,即执行 monitorentermonitorexit
  2. 修饰普通方法时,锁对象是当前类的 this 对象,而 this 对象是通过 new 关键字创建在堆内存中的,不同的对象就代表不同的锁。而且该方法会被标记为 ACC_SYNCHRONIZED,自动在调用与退出时执行 monitorentermonitorexit

参考自

  1. https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1863
  2. https://mp.weixin.qq.com/s/fyvoraVu9yjgqX-xhn6EHQ
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值