首页 > 其他分享 >面试官:为什么 wait/notify 必须与 synchronized 一起使用??

面试官:为什么 wait/notify 必须与 synchronized 一起使用??

时间:2023-01-03 14:25:26浏览次数:58  
标签:面试官 obj synchronized lock 线程 notify wait

来源:blog.csdn.net/randompeople/article/details/114917087

为什么 java wait/notify 必须与 synchronized 一起使用

这个问题就是书本上没怎么讲解,就是告诉我们这样处理,但没有解释为什么这么处理?我也是基于这样的困惑去了解原因。

synchronized是什么

Java中提供了两种实现同步的基础语义:synchronized方法和synchronized块, 看个demo:

public class SyncTest {

   \\ 1、synchronized方法

 public synchronized void syncMethod(){
        System.out.println("hello method");
    }

    \\ 2、synchronized块

    public void syncBlock(){
        synchronized (this){
            System.out.println("hello block");
        }
    }
}

具体还要区分:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。不同实例对象的访问,是不会形成锁的。
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

它具有的特性:

  • 原子性
  • 可见性
  • 有序性
  • 可重入性

synchronized如何实现锁

这样看来synchronized实现的锁是基于class对象来实现的,我们来看看如何实现的,它其实是跟class对象的对象头一起起作用的,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

其中对象头中有一个Mark Word,这里主要存储对象的hashCode、锁信息或分代年龄或GC标志等信息,把可能的情况列出来大概如下:

其中synchronized就与锁标志位一起作用实现锁。主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。

每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

上面有2个字段很重要:

  • _WaitSet队列处于wait状态的线程,会被加入到_WaitSet。
  • _EntryList队列处于等待锁block状态的线程,会被加入到该列表。
  • _owner_owner指向持有ObjectMonitor对象的线程

我们来模拟一下进入锁的流程:

1、当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合

2、当线程获取到对象的monitor 后进入 _Owner 区域,并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1

3、若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。

4、若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)

wait/notify

这两个是Java对象都有的属性,表示这个对象的一个等待和通知机制。

推荐一个开源免费的 Spring Boot 最全教程:

https://github.com/javastacks/spring-boot-best-practice

不用synchronized 会怎么样

参考其他博客,我们来看看不使用synchronized会怎么样,假设有2个线程,分别做2件事情,T1线程代码逻辑:

while(!条件满足) // line 1
{
    obj.wait(); // line 2
}
doSomething();

T2线程的代码逻辑:

更改条件为满足; // line 1
obj.notify(); // line 2

多线程环境下没有synchronized,没有锁的情况下可能会出现如下执行顺序情况:

  • T1 line1 满足while 条件
  • T2 line1 执行
  • T2 line2 执行,notify发出去了
  • T1 line2 执行,wait再执行

这样的执行顺序导致了notify通知发出去了,但没有用,已经wait是在之后执行,所以有人说没有保证原子性,就是line1 和line2 是一起执行结束,这个也被称作lost wake up问题。解决方法就是可以利用synchronized来加锁,于是有人就写了这样的代码:

synchronized(lock)
{
    while(!条件满足)
    {
        obj.wait();
    }
    doSomething();
}
synchronized(lock)
{
   更改条件为满足;
   obj.notify();
}

这样靠锁来做达到目的。但这代码会造成死锁,因为先T1 wait(),再T2 notify();而问题在于T1持有lock后block住了,T2一直无法获得lock,从而永无可能notify()并将T1的block状态解除,就与T1形成了死锁。

所以JVM在实现wait()方法时,一定需要先隐式的释放lock,再block,并且被notify()后从wait()方法返回前,隐式的重新获得了lock后才能继续user code的执行。要做到这点,就需要提供lock引用给obj.wait()方法,否则obj.wait()不知道该隐形释放哪个lock,于是调整之后的结果如下:

synchronized(lock)
{
    while(!条件满足)
    {
        obj.wait(lock);
        // obj.wait(lock)伪实现
        //   [1] unlock(lock)
        //   [2] block住自己,等待notify()
        //   [3] 已被notify(),重新lock(lock)
        //   [4] obj.wait(lock)方法成功返回
    }
    doSomething();
}

[最终形态] 把lock和obj合一

其它线程API如PThread提供wait()函数的签名是类似cond_wait(obj, lock)的,因为同一个lock可以管多个obj条件队列。而Java内置的锁与条件队列的关系是1:1,所以就直接把obj当成lock来用了。因此此处就不需要额外提供lock,而直接使用obj即可,代码也更简洁:

synchronized(obj)
{
    while(!条件满足)
    {
        obj.wait();
    }
    doSomething();
}
synchronized(lock)
{
   更改条件为满足;
   obj.notify();
}

lost wake up

wait/notify 如果不跟synchronized结合就会造成lost wake up,难以唤醒wait的线程,所以单独使用会有问题。

近期热文推荐:

1.1,000+ 道 Java面试题及答案整理(2022最新版)

2.劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

标签:面试官,obj,synchronized,lock,线程,notify,wait
From: https://www.cnblogs.com/javastack/p/17021997.html

相关文章

  • 面试官:说说React-SSR的原理
    前言所谓同构,简而言之就是,第一次访问后台服务时,后台直接把前端要显示的界面全部返回,而不是像SPA项目只渲染一个<divid="root"></div>剩下的都是靠JavaScript脚本去......
  • 面试官:你是怎样进行react组件代码复用的
    mixinMixin设计模式Mixin(混入)是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上面去,可以拷贝多个属性到一个对象上,为了解决代码复用问题。常......
  • 面试官:React怎么做性能优化
    前言最近一直在学习关于React方面的知识,并有幸正好得到一个机会将其用在了实际的项目中。所以我打算以博客的形式,将我在学习和开发(React)过程中遇到的问题记录下来。这两......
  • 面试官让你说说react状态管理?
    开发者普遍认为状态是组件的一部分,但是同时却又在剥离状态上不停的造轮子,这不是很矛盾么?对于一个最简单的文本组件而言functionText(){const[text,setText]=......
  • 面试官:系统需求多变时如何设计?
    面试官:我想问个问题哈,项目里比较常见的问题面试官:我现在有个系统会根据请求的入参,做出不同动作。但是,这块不同的动作很有可能是会发生需求变动的,这块系统你会怎么样设计?面试......
  • 面试官问我JVM调优,我忍不住了,哈哈哈哈
    面试官:今天要不来聊聊JVM调优相关的吧?面试官:你曾经在生产环境下有过调优JVM的经历吗?候选者:没有面试官:…候选者:嗯…是这样的,我们一般优化系统的思路是这样的候选者:1.一般来......
  • 【面试题】面试官:为什么Promise中的错误不能被try/catch?
    大厂面试题分享面试题库前端面试题库(面试必备) 地址:​​前端面试题库​​前言之前我写过一篇文章,讨论了为什么​​asyncawait​​​中的错误可以被​​trycatch​​​......
  • 面试官:ui组件可以自动加载,那么业务组件可以吗?
    大厂面试题分享面试题库前端面试题库(面试必备) 地址:​​前端面试题库​​背景笔者在最近在公司接手了一个老的对内使用的项目,接手后体验了下发现首屏加载比较慢。分析......
  • Java关键词synchronized解读
    目录1引入Synchronized2Synchronized的使用2.1对象锁2.1.1Synchronized修饰实例方法2.1.2Synchronized修饰代码块2.2类锁2.2.1synchronize修饰静态方法2.2.2synchr......
  • 面试官:你说说 js 中实现继承有哪几种方法?
    前言面试官:“你说说JavaScript中实现继承有哪几种方法?”紧张的萌新:“额,class中用extends实现继承,然后...没了...”面试官:“...”······大家好,我是CoderBin,......