等待-通知机制:
类比现实世界中的就医流程:
1. 我们先去挂号,然后到就诊门口,排队等待叫号
2. 当我们的号码被叫时,我们进门诊看医生
3. 医生说我们需要验血,于是我们去抽血验血,他叫下一位排队的病人
4. 我们拿到验血报告后,重新分诊后在门诊口排队,等待叫号
5. 当医生再次叫到我们的号时,我们再进去看医生。
6. 看完后,我们就回家。
就医流程结合并发编程会是什么样的那?
1. 我们到门诊就诊,类似于线程去获取互斥锁,在门诊口排队等待叫号,类似于互斥锁被其他线程占有;
2. 我们进门诊看医生,类似于线程获取到了互斥锁;
3. 医生需要验血报告,让我们去抽血,类似于线程要求的条件没有满足(已经获取锁了)
4. 我们去抽血,类似于线程进入等待状态,医生叫下一个病人,类似于线程释放了持有的互斥锁
5. 我们拿到验血报告,类似于线程要求条件得到满足;我们到门诊重新分诊,类似于线程重新去获取互斥锁。
总结:线程首先去获取互斥锁,如果锁被其他线程占有,则排队等待,当线程获取到锁后,进入临界区。如果线程要求的条件不满足时,释放锁并进入等待状态;当要求的条件满足时,通知等待的线程,去获取互斥锁。这就是完整的等待-通知机制,也就是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() 方法,释放占有的锁,并进入等待状态,阻塞线程。当条件满足时,调用 notifyAll() 唤醒等待队列中所有等待状态的线程,被唤醒的线程进入就绪状态,开始竞争互斥锁。
注意:
wait()和sleep()的区别:1. wait()释放线程持有的锁,sleep()不会;2. wait()必须在 synchronized{} 保护的临界区内调用,sleep则没有限制;3. wait不能自动唤醒线程,必须通过 notify,sleep则在指定的时间后自动唤醒;4. wait() 是 Object的方法,sleep() 是 Thread 的方法。
notify()和notifyAll()的区别:notify只能随机唤醒等待队列中的一个线程,natifyAll能唤醒等待队列中所有的线程。唤醒的线程不一定能立即获取锁,它需要先改变等待状态,进入就绪状态,才能开始竞争锁。
Synchronized
为什么 synchronized(lock){临界区} 就可以实现临界区代码互斥的功能?
synchronized 关键字的底层实现是两个指令: monitorenter 和 monitorexit,而lock只是一个普通的 java 对象。
Java对象在内存长什么样子?
Java对象在内存中的分布为:对象头,实例数据,对齐方式。其中,对象头描述了对象运行过程中的信息,如:锁状态(无锁,偏向锁,轻量级锁,重量级锁),哈希值,GC分代年龄,偏向锁标志,锁标志位,计数器,当前线程指针...
当我们使用 new 关键字创建一个对象时,JVM会在堆中创建一个 instanceOopDesc 对象,这个对象包含了对象头(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; //记录锁的重入次数
....
}
- 当多个线程同时访问同步代码时,会先进入 _EntryList 线程队列中;
- 当某个线程通过竞争获取到对象锁后,_owner 设置为当前线程,_count 加1;_EntryList 中的线程进入阻塞状态(blocking)
- 当持有对象锁的线程,遇到 wait 方法后,该线程释放锁,_owner 重置为 NULL,_count减1,同时该线程进入 _WaitSet 中等待被唤醒,当前线程处于等待状态(wait);同时,_EntryList中阻塞状态的线程开始竞争锁对象。假设其中一个获取到锁,又会走第 2 步;
- 当持有对象锁的线程,调用 notify 时,处于_WaitSet集合中的线程被唤醒,加入_ENtryList队列中,同时将状态改为阻塞状态。注意:此时,当前线程依然持有锁。
总结:上诉过程,每次获取锁,释放锁,都会阻塞和唤醒线程,而线程切换需要CPU从用户态转入内核态,性能消耗比较严重。所以,JVM使用 自旋锁,偏向锁,轻量级锁,优化 synchronized。
synchronized修饰普通方法,静态方法和代码段氏,有什么区别?
- 修饰静态方法时,锁对象是当前类的 Class 对象,而 Class 对象在程序运行过程中只有一个,所以,访问该方法时会自动加锁与解锁,即执行 monitorenter 和 monitorexit
- 修饰普通方法时,锁对象是当前类的 this 对象,而 this 对象是通过 new 关键字创建在堆内存中的,不同的对象就代表不同的锁。而且该方法会被标记为 ACC_SYNCHRONIZED,自动在调用与退出时执行 monitorenter 和 monitorexit。
参考自