最简单的东西,往往包含了最复杂的实现,因为需要为上层的存在提供一个稳定的基础,Object作为java中所有对象的基类,其存在的价值不言而喻,其中wait和notify方法的实现多线程协作提供了保证。

案例

  1. public class WaitTestDemo {
  2. public static void main(String[] args) {
  3. Message msg = new Message("process it");
  4. Waiter waiter = new Waiter(msg);
  5. new Thread(waiter,"waiterThread").start();
  6. Waiter waiter1 = new Waiter(msg);
  7. new Thread(waiter1, "waiter1Thread").start();
  8. Notifier notifier = new Notifier(msg);
  9. new Thread(notifier, "notifierThread").start();
  10. System.out.println("All the threads are started");
  11. }
  12. public static class Message {
  13. private String msg;
  14. public Message(String str){
  15. this.msg=str;
  16. }
  17. public String getMsg() {
  18. return msg;
  19. }
  20. public void setMsg(String str) {
  21. this.msg=str;
  22. }
  23. }
  24. public static class Waiter implements Runnable{
  25. private Message msg;
  26. public Waiter(Message m){
  27. this.msg=m;
  28. }
  29. @Override
  30. public void run() {
  31. String name = Thread.currentThread().getName();
  32. synchronized (msg) {
  33. try{
  34. System.out.println(name+" waiting to get notified at time:"+System.currentTimeMillis());
  35. msg.wait();
  36. }catch(InterruptedException e){
  37. e.printStackTrace();
  38. }
  39. System.out.println(name+" waiter thread got notified at time:"+System.currentTimeMillis());
  40. //process the message now
  41. System.out.println(name+" processed: "+msg.getMsg());
  42. }
  43. }
  44. }
  45. public static class Notifier implements Runnable {
  46. private Message msg;
  47. public Notifier(Message msg) {
  48. this.msg = msg;
  49. }
  50. @Override
  51. public void run() {
  52. String name = Thread.currentThread().getName();
  53. System.out.println(name+" started");
  54. try {
  55. Thread.sleep(1000);
  56. synchronized (msg) {
  57. msg.setMsg(name+" Notifier work done");
  58. msg.notify();
  59. msg.notify();
  60. //msg.notifyAll();
  61. }
  62. } catch (InterruptedException e) {
  63. e.printStackTrace();
  64. }
  65. }
  66. }
  67. }

Output:

  1. All the threads are started
  2. waiterThread waiting to get notified at time:1572344152693
  3. waiter1Thread waiting to get notified at time:1572344152693
  4. notifierThread started
  5. waiterThread waiter thread got notified at time:1572344153705
  6. waiterThread processed: notifierThread Notifier work done
  7. waiter1Thread waiter thread got notified at time:1572344153706
  8. waiter1Thread processed: notifierThread Notifier work done

也可以使用notifyAll,输出为:

  1. All the threads are started
  2. waiterThread waiting to get notified at time:1572344222162
  3. waiter1Thread waiting to get notified at time:1572344222162
  4. notifierThread started
  5. waiter1Thread waiter thread got notified at time:1572344223175
  6. waiter1Thread processed: notifierThread Notifier work done
  7. waiterThread waiter thread got notified at time:1572344223177
  8. waiterThread processed: notifierThread Notifier work done

发现最后唤醒的顺序颠倒了

执行完notify方法,并不会立马唤醒等待线程,在notify方法后面加一段sleep代码就可以看到效果,如果线程执行完notify方法之后sleep 5s,在这段时间内,线程waiterThread1依旧持有monitor,线程waiterThread只能继续等待;

为什么要使用synchronized?

在Java中,synchronized有两种使用形式,同步方法和同步代码块。代码如下:

  1. public class SynchronizedTest {
  2. public synchronized void doSth(){
  3. System.out.println("Hello World");
  4. }
  5. public void doSth1(){
  6. synchronized (SynchronizedTest.class){
  7. System.out.println("Hello World");
  8. }
  9. }
  10. }

我们先来使用Javap来反编译以上代码,结果如下(部分无用信息过滤掉了):

  1. public synchronized void doSth();
  2. descriptor: ()V
  3. flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  4. Code:
  5. stack=2, locals=1, args_size=1
  6. 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  7. 3: ldc #3 // String Hello World
  8. 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  9. 8: return
  10. public void doSth1();
  11. descriptor: ()V
  12. flags: ACC_PUBLIC
  13. Code:
  14. stack=2, locals=3, args_size=1
  15. 0: ldc #5 // class com/hollis/SynchronizedTest
  16. 2: dup
  17. 3: astore_1
  18. 4: monitorenter
  19. 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
  20. 8: ldc #3 // String Hello World
  21. 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  22. 13: aload_1
  23. 14: monitorexit
  24. 15: goto 23
  25. 18: astore_2
  26. 19: aload_1
  27. 20: monitorexit
  28. 21: aload_2
  29. 22: athrow
  30. 23: return

反编译后,我们可以看到Java编译器为我们生成的字节码。在对于doSthdoSth1的处理上稍有不同。也就是说。JVM对于同步方法和同步代码块的处理方式不同。

对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorentermonitorexit两个指令来实现同步。

关于这部分内容,在JVM规范中也可以找到相关的描述。

同步方法

方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块

同步代码块使用monitorentermonitorexit两个指令实现。 The Java® Virtual Machine Specification 中有关于这两个指令的介绍:

大致内容如下: 可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

归总

同步方法通过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。

同步代码块通过monitorentermonitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得所锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。

每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。

底层原理

对象头和内置锁(ObjectMonitor)

每个对象分为三块区域:对象头、实例数据和对齐填充

  • 对象头包含两部分,第一部分是Mark Word,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,这一部分占一个字节。第二部分是Klass Pointer(类型指针),是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,这部分也占一个字节。(如果对象是数组类型的,则需要3个字节来存储对象头,因为还需要一个字节存储数组的长度)
  • 实例数据存放的是类属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐
  • 填充数据是因为虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

Synchronized通常被称为重量级锁,但是1.6之后对其进行优化,新增了轻量级锁和偏向锁,这里重点说下重量级锁,随后对Synchronized的优化简单介绍下。

从对象头的存储内容可以看出锁的状态都保存在对象头中,Synchronized也不例外,当其从轻量级锁膨胀为重量级锁时,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。

关于Synchronized的实现在java对象头里较为简单,只是改变一下标识位,并将指针指向monitor对象的起始地址,其实现的重点是monitor对象。

在HotSpot虚拟机中,monitor采用ObjectMonitor实现。

内置锁(ObjectMonitor)

通常所说的对象的内置锁,是对象头Mark Word中的重量级锁指针指向的monitor对象,该对象是在HotSpot底层C++语言编写的(openjdk里面看),简单看一下代码:

  1. //结构体如下
  2. ObjectMonitor::ObjectMonitor() {
  3. _header = NULL;
  4. _count = 0;
  5. _waiters = 0,
  6. _recursions = 0; //线程的重入次数
  7. _object = NULL;
  8. _owner = NULL; //标识拥有该monitor的线程
  9. _WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
  10. _WaitSetLock = 0 ;
  11. _Responsible = NULL ;
  12. _succ = NULL ;
  13. _cxq = NULL ; //多线程竞争锁进入时的单向链表
  14. FreeNext = NULL ;
  15. _EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
  16. _SpinFreq = 0 ;
  17. _SpinClock = 0 ;
  18. OwnerIsThread = 0 ;
  19. }

ObjectMonitor队列之间的关系转换可以用下图表示:

既然提到了_waitSet和_EntryList(_cxq队列后面会说),那就看一下底层的wait和notify方法

wait方法的实现过程:

  1. //1.调用ObjectSynchronizer::wait方法
  2. void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  3. /*省略 */
  4. //2.获得Object的monitor对象(即内置锁)
  5. ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  6. DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  7. //3.调用monitor的wait方法
  8. monitor->wait(millis, true, THREAD);
  9. /*省略*/
  10. }
  11. //4.在wait方法中调用addWaiter方法
  12. inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
  13. /*省略*/
  14. if (_WaitSet == NULL) {
  15. //_WaitSet为null,就初始化_waitSet
  16. _WaitSet = node;
  17. node->_prev = node;
  18. node->_next = node;
  19. } else {
  20. //否则就尾插
  21. ObjectWaiter* head = _WaitSet ;
  22. ObjectWaiter* tail = head->_prev;
  23. assert(tail->_next == head, "invariant check");
  24. tail->_next = node;
  25. head->_prev = node;
  26. node->_next = head;
  27. node->_prev = tail;
  28. }
  29. }
  30. //5.然后在ObjectMonitor::exit释放锁,接着 thread_ParkEvent->park 也就是wait

总结:通过object获得内置锁(objectMonitor),通过内置锁将Thread封装成OjectWaiter对象,然后addWaiter将它插入以_waitSet为首结点的等待线程链表中去,最后释放锁。

notify方法的底层实现

  1. //1.调用ObjectSynchronizer::notify方法
  2. void ObjectSynchronizer::notify(Handle obj, TRAPS) {
  3. /*省略*/
  4. //2.调用ObjectSynchronizer::inflate方法
  5. ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
  6. }
  7. //3.通过inflate方法得到ObjectMonitor对象
  8. ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  9. /*省略*/
  10. if (mark->has_monitor()) {
  11. ObjectMonitor * inf = mark->monitor() ;
  12. assert (inf->header()->is_neutral(), "invariant");
  13. assert (inf->object() == object, "invariant") ;
  14. assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is inva;lid");
  15. return inf
  16. }
  17. /*省略*/
  18. }
  19. //4.调用ObjectMonitor的notify方法
  20. void ObjectMonitor::notify(TRAPS) {
  21. /*省略*/
  22. //5.调用DequeueWaiter方法移出_waiterSet第一个结点
  23. ObjectWaiter * iterator = DequeueWaiter() ;
  24. //6.后面省略是将上面DequeueWaiter尾插入_EntrySet的操作
  25. /**省略*/
  26. }

总结:通过object获得内置锁(objectMonitor),调用内置锁的notify方法,通过_waitset结点移出等待链表中的首结点,将它置于_EntrySet中去,等待获取锁。注意:notifyAll根据policy不同可能移入_EntryList或者_cxq队列中,此处不详谈。

参考

JVM源码分析之Object.wait/notify实现

[深入理解多线程(四)—— Moniter的实现原理]

从jvm源码看synchronized

并发编程之 wait notify 方法剖析

JAVA并发-对象方法wait的更多相关文章

  1. Java中对象方法的调用过程&动态绑定(Dynamic Binding)

    Java面向对象的最重要的一个特点就是多态, 而多态当中涉及到了一个重要的机制是动态绑定(Dynamic binding). 之前只有一个大概的概念, 没有深入去了解动态绑定的机理, 直到很多公司都问 ...

  2. Java 调用对象方法的执行过程

    弄清调用对象方法的执行过程十分重要.下面是调用过程的详细描述: 1) 编译器查看对象的声明类型和方法名.假设调用x.f(param),且隐式参数x声明为C类的对象.需要注意的是:有可能存在多个名为f, ...

  3. (转)java并发对象锁、类锁、私有锁

    转自:http://ifeve.com/java-locks/ 建议参考:http://www.zhihu.com/question/28113814 Java类锁和对象锁实践 感谢[jiehao]同 ...

  4. Java并发-对象共享

    我们不仅希望防止某个线程正在使用对象状态而其他的线程正在修改该状态,而且希望当一个线程修改了对象状态后,其他的线程能够看到发生的状态变化. 可见性:当读操作和写操作在不同的线程中进行时,他们的动作是共 ...

  5. 《Java并发编程实战》学习笔记 线程安全、共享对象和组合对象

    Java Concurrency in Practice,一本完美的Java并发参考手册. 查看豆瓣读书 推荐:InfoQ迷你书<Java并发编程的艺术> 第一章 介绍 线程的优势:充分利 ...

  6. 用Java实现一个通用并发对象池

    这篇文章里我们主要讨论下如何在Java里实现一个对象池.最近几年,Java虚拟机的性能在各方面都得到了极大的提升,因此对大多数对象而言,已经没有必要通过对象池来提高性能了.根本的原因是,创建一个新的对 ...

  7. 【Java并发编程一】线程安全和共享对象

    一.什么是线程安全 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用代码代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的 ...

  8. Java 进阶7 并发优化 5 并发控制板方法

    Java 进阶7 并发优化 5 并发控制板方法 20131114 前言:          Java 中多线程并发程序中存在线程安全的问题,之前学习 Java的同步机制,掌握的同步方法只有一种就是使用 ...

  9. Java并发编程(五):Java线程安全性中的对象发布和逸出

    发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外 ...

随机推荐

  1. monkey-api-encrypt 1.1.2版本发布啦

    时隔10多天,monkey-api-encrypt发布了第二个版本,还是要感谢一些正在使用的朋友们,提出了一些问题. GitHub主页:https://github.com/yinjihuan/mon ...

  2. AI语音验证码识别

    欢迎使用AI语音验证码识别v4.0程序程序调用方法:http://code.hbadmin.com/?url=http://code.hbadmin.com/demo/2118534.wav [试听] ...

  3. oracle--JOB任务

    1.创建一张测试表 -- Create table create table A8 ( a1 VARCHAR2(500) ) tablespace TT1 pctfree 10 initrans 1 ...

  4. SQLAIchemy 学习(一)Session 相关

    0. 前言 最近是使用 SQLAlchemy 框架作为一个 ORM 框架,现对其做简单整理 1. 创建 Session 说到数据库,就离不开 Session.Session 的主要目的是建立与数据库的 ...

  5. apache启动错误 AH00072

    错误描述: make_sock: could not bind to address [::]:443 G:\Apache24\bin>httpd.exe -w -n "Apache2 ...

  6. <T>泛型,广泛的类型

    其实早在1999年的JSR 14规范中就提到了泛型概念,知道jdk5泛型的使用才正式发布,在jdk7后,又对泛型做了优化,泛型的推断. 泛型类 public class Pair<T> { ...

  7. 【More Effective C++ 条款2】最好使用C++转型操作符

    C的转型方式存在以下两个缺点: 1)几乎允许你将任何类型转化为任何类型,不能精确的指明转型意图,这样很不安全 如将一个pointer-to-base-class-object转型为一个pointer- ...

  8. springmvc项目转为springboot

    说明 如果你的项目连maven项目都不是,请自行转为maven项目,在按照本教程进行. 本教程适用于spring+springmvc+mybatis+shiro的maven项目. 1.修改pom文件依 ...

  9. Springboot项目中pom.xml的Oracle配置错误问题

    这几天刚开始学习Springboot碰见各种坑啊,这里记录一个添加Oracle引用的解决方案. 前提:开发工具IDEA2019.2,SpringBoot,maven项目:Oracle版本是Oracle ...

  10. Spring MVC处理参数Convert

    Springmvc.xml 配置convert,xml中配置多个相同的泛型时,xml里配置的convert会从上到下挨个执行. <!-- 配置注解驱动,并配置convert --> < ...