美文网首页JVM
Java中的synchronized关键字(一)

Java中的synchronized关键字(一)

作者: buzzerrookie | 来源:发表于2019-03-03 10:24 被阅读0次

    在Java中,synchronized关键字既可修饰方法,又可修饰对象,在用于对象时既可修饰普通对象也可用于类对象。

    synchronized关键字

    用法

    • 修饰方法:根据JVM规范2.11.10节,方法级同步是隐式的,运行时常量池method_info结构用ACC_SYNCHRONIZED标志区分synchronized方法,当调用ACC_SYNCHRONIZED修饰的方法时,执行线程会进入监视器,然后执行该方法,最后退出监视器。
    • 修饰对象:用synchronized修饰对象时通常使用synchronized块语句,JVM提供了monitorenter指令monitorexit指令支持这种特性。使用javap命令反编译字节码文件后会看到在synchronized块语句的入口和出口分别有monitorenter和monitorexit指令。

    monitorenter指令

    根据先前文章分析的模板解释器的初始化过程,为monitorenter指令生成汇编代码的生成函数如下:

    void TemplateTable::monitorenter() {
      transition(atos, vtos);
    
      // 省略一些代码
    
      // store object
      __ movptr(Address(c_rarg1, BasicObjectLock::obj_offset_in_bytes()), rax);
      __ lock_object(c_rarg1);
    
      // 省略一些代码
    }
    

    lock_object函数在InterpreterMacroAssembler类中实现如下:

    void InterpreterMacroAssembler::lock_object(Register lock_reg) {
      assert(lock_reg == c_rarg1, "The argument is only for looks. It must be c_rarg1");
    
      if (UseHeavyMonitors) {
        call_VM(noreg,
                CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
                lock_reg);
      } else {
        // 省略一些代码
        if (UseBiasedLocking) {
          biased_locking_enter(lock_reg, obj_reg, swap_reg, rscratch1, false, done, &slow_case);
        }
        // 省略一些代码
        bind(slow_case);
        // Call the runtime routine for slow case
        call_VM(noreg,
                CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
                lock_reg);
        bind(done);
      }
    }
    

    该函数对三种情况做了分别的处理:

    • 如果明确使用重量级的监视器(虚拟机参数-XX:+UseHeavyMonitors),那么UseHeavyMonitors的值是true,而UseBiasedLocking的值是false,会直接执行标准的加锁流程(见下文);
    • 如果使用偏向锁(默认启用),那么UseHeavyMonitors的值是false,而UseBiasedLocking的值是true。当某个线程想锁定一个可偏向的对象时首先会通过CAS操作尝试将自己的线程指针安装到对象的mark word,如果CAS操作成功则该对象被锁定并偏向了该线程,该线程在后续的加锁与解锁操作时如果对象仍然可偏向,那么只需检查mark word中的线程指针。CAS操作失败意味着该对象已经偏向了另一线程,因此该对象的偏向锁需要被撤销,偏向锁升级成轻量级锁(仍由另一线程持有);
    • 如果不使用偏向锁(虚拟机参数-XX:-UseBiasedLocking),则UseHeavyMonitors与UseBiasedLocking的值都是false,执行标准的加锁流程。

    monitorexit指令

    为monitorexit指令生成汇编代码的函数如下:

    void TemplateTable::monitorexit() {
      // 省略一些代码
      __ unlock_object(c_rarg1);
      __ pop_ptr(rax); // discard object
    }
    

    所调用的InterpreterMacroAssembler类的unlock_object函数实现如下,与上文的lock_object函数相似。

    void InterpreterMacroAssembler::unlock_object(Register lock_reg) {
      assert(lock_reg == c_rarg1, "The argument is only for looks. It must be rarg1");
    
      if (UseHeavyMonitors) {
        call_VM(noreg,
                CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorexit),
                lock_reg);
      } else {
        // 省略一些代码
    
        // Call the runtime routine for slow case.
        movptr(Address(lock_reg, BasicObjectLock::obj_offset_in_bytes()),
             obj_reg); // restore obj
        call_VM(noreg,
                CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorexit),
                lock_reg);
        bind(done);
        restore_bcp();
      }
    }
    

    synchronized方法

    模板解释器初始化时generate系列函数会为Java方法调用生成汇编代码,以调用普通Java方法为例,generate_normal_entry函数只有一个用来表示方法是否是同步方法的布尔型参数。如果是同步方法那么在调用真正的Java方法之前会调用lock_method函数,注意lock_method函数在内部也会调用InterpreterMacroAssembler类的lock_object函数。

    void InterpreterGenerator::lock_method(void) {
      // synchronize method
      const Address access_flags(rbx, Method::access_flags_offset());
      const Address monitor_block_top(
            rbp,
            frame::interpreter_frame_monitor_block_top_offset * wordSize);
      const int entry_size = frame::interpreter_frame_monitor_size() * wordSize;
    
      // get synchronization object
      {
        const int mirror_offset = in_bytes(Klass::java_mirror_offset());
        Label done;
        __ movl(rax, access_flags);
        __ testl(rax, JVM_ACC_STATIC);
        // get receiver (assume this is frequent case)
        __ movptr(rax, Address(r14, Interpreter::local_offset_in_bytes(0)));
        __ jcc(Assembler::zero, done);
        __ movptr(rax, Address(rbx, Method::const_offset()));
        __ movptr(rax, Address(rax, ConstMethod::constants_offset()));
        __ movptr(rax, Address(rax,
                               ConstantPool::pool_holder_offset_in_bytes()));
        __ movptr(rax, Address(rax, mirror_offset));
    
        __ bind(done);
      }
    
      // add space for monitor & lock
      __ subptr(rsp, entry_size); // add space for a monitor entry
      __ movptr(monitor_block_top, rsp);  // set new monitor block top
      // store object
      __ movptr(Address(rsp, BasicObjectLock::obj_offset_in_bytes()), rax);
      __ movptr(c_rarg1, rsp); // object address
      __ lock_object(c_rarg1);
    }
    

    在深入InterpreterRuntime类的monitorenter和monitorexit静态函数之前,先看一下OpenJDK的Wiki对同步的解释。

    同步流程

    OpenJDK给出了同步加锁的流程。

    Synchronization.png
    1. 右边展示了标准的加锁过程。当对象未被加锁时,mark word最低两位的值是01。当方法在对象上同步时,mark word和对象指针会保存在当前栈桢的锁记录(lock record)里。然后虚拟机尝试利用CAS操作将锁记录的指针安装到对象的mark word上,如果成功,当前线程就拥有了锁。锁记录总是在字边界对齐的,mark word的最低两位会变成00以标识该对象被锁定。
    2. 如果因为该对象之前被锁定而导致CAS操作失败,虚拟机首先会测试mark word是否指向当前线程的方法栈。如果是则这个线程已经拥有了该对象的锁(锁记录已经在当前栈桢中了),可以继续执行。对这种递归锁定的对象,锁记录会被初始化成0而不是对象的mark word。只有当两个不同线程并发地在同一对象上同步时,轻量级锁才必须膨胀成重量级的监视器(monitor)以管理等待的线程。
    3. 轻量级锁比膨胀的监视器要轻量得多,但性能也受每次CAS操作的影响,尽管大多数对象都是被同一线程锁定和解锁。在Java 6,这个不足被无存储的偏向锁技术解决,只有第一次获取锁时才执行CAS操作用以将线程ID安装到mark word上。对象偏向了这个线程,后续被同一线程加锁与解锁都不需要任何的原子操作或者去更新mark word,栈上的锁记录会一直不被初始化。
    4. 当一个线程在偏向另一线程的对象上同步时,必须撤回偏向以使该对象被正常地加锁。遍历偏向持有者的栈,与该对象有关的锁记录会被调整成轻量级锁模式,最老的锁记录的指针会被安装到对象的mark word,所有线程必须等待此操作。偏向被撤回的另一种情况是访问对象的hashcode时,因为hash与线程ID在字段上是共享的。
    5. 显式需要在线程间共享的对象,比如生产者/消费者模式中,不适用偏向锁。因此,在过去如果经常发生撤回那么某个类就会禁用偏向锁,这叫做批量撤回(bulk revocation)。如果加锁代码在被禁用偏向锁的类实例上被调用,那么会执行标准的轻量级锁操作。
    6. 相似地,批量重偏向(bulk rebiasing)使类实例的偏向无效而不禁用偏向锁,epoch值扮演着偏向有效期的时间戳。这个值会在对象分配的时候拷贝到mark word。

    InterpreterRuntime类

    InterpreterRuntime类的实现在文件hotspot/src/share/vm/interpreter/interpreterRuntime.cpp中,monitorenter函数代码经过预处理后如下,UseBiasedLocking是运行时参数,默认是开启的。如果使用偏向锁,则调用ObjectSynchronizer类的fast_enter静态函数,否则调用slow_enter静态函数,BasicObjectLock就是上面所说的锁记录实现。

    void InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)
    {
        ThreadInVMfromJavaNoAsyncException __tiv(thread);
        HandleMarkCleaner __hm(thread);
        Thread* __the_thread__ = thread;
        os::verify_stack_alignment();
        if (PrintBiasedLockingStatistics) {
            Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
        }
        Handle h_obj(thread, elem->obj());
        if (UseBiasedLocking) {
            // Retry fast entry if bias is revoked to avoid unnecessary inflation
            ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, __the_thread__);
            if ((((ThreadShadow*)__the_thread__)->has_pending_exception())) return ; (void)(0);
        } else {
            ObjectSynchronizer::slow_enter(h_obj, elem->lock(), __the_thread__);
            if ((((ThreadShadow*)__the_thread__)->has_pending_exception())) return ; (void)(0);
        }
    }
    

    monitorexit函数代码经过预处理后如下,只调用ObjectSynchronizer类的slow_exit静态函数。

    void InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem)
    {
        ThreadInVMfromJavaNoAsyncException __tiv(thread);
        HandleMarkCleaner __hm(thread);
        Thread* __the_thread__ = thread;
        os::verify_stack_alignment();
        Handle h_obj(thread, elem->obj());
    
        if (elem == __null || h_obj()->is_unlocked()) {
            { Exceptions::_throw_msg(__the_thread__, vmSymbols::java_lang_IllegalMonitorStateException(), __null); return; };
        }
        ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
        elem->set_obj(__null);
    }
    

    ObjectSynchronizer类

    fast_enter函数

    在偏向锁启用的情况下,fast_enter函数首先再次尝试撤回偏向并重新偏向至参数线程,若成功则返回,否则执行标准的加锁过程。

    void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
     if (UseBiasedLocking) {
        if (!SafepointSynchronize::is_at_safepoint()) {
          BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
          if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
            return;
          }
        } else {
          assert(!attempt_rebias, "can not rebias toward VM thread");
          BiasedLocking::revoke_at_safepoint(obj);
        }
        assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
     }
    
     slow_enter (obj, lock, THREAD) ;
    }
    

    slow_enter函数

    slow_enter函数即对应着标准的加锁过程,其代码如下所示:

    void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
      markOop mark = obj->mark();
      assert(!mark->has_bias_pattern(), "should not see bias pattern here");
    
      if (mark->is_neutral()) { // 检查是否为无锁状态,即obj的mark word的后两位是否为01
        // Anticipate successful CAS -- the ST of the displaced mark must
        // be visible <= the ST performed by the CAS.
        lock->set_displaced_header(mark); // 栈桢锁记录的displaced header会保存obj的mark word
        if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { // 利用CAS操作将锁记录指针安装到obj的mark word上,若操作成功则拥有了锁
          TEVENT (slow_enter: release stacklock) ;
          return ;
        }
        // Fall through to inflate() ...
      } else// 若obj已被锁定,即obj的mark word的后两位已经是00,那么看它是否由参数线程锁定,即检查mark word是否指向参数线程的方法栈
      if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { // 参数线程重入获得了锁
        assert(lock != mark->locker(), "must not re-lock the same lock");
        assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
        lock->set_displaced_header(NULL); // 锁记录的displaced header会被置为0而不会是对象的mark word
        return;
      }
      // The object header will never be displaced to this lock,
      // so it does not matter what the value is, except that it
      // must be non-zero to avoid looking like a re-entrant lock,
      // and must not look locked either.
      lock->set_displaced_header(markOopDesc::unused_mark());
      ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); // 膨胀成重量级的监视器
    }
    

    slow_exit函数

    slow_exit与fast_exit函数几乎相同,都是用于monitorexit字节码指令的执行。与slow_enter函数相似,fast_exit函数同样对锁的不同情况做了分类讨论。

    void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
      fast_exit (object, lock, THREAD) ;
    }
    
    void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
      assert(!object->mark()->has_bias_pattern(), "should not see bias pattern here");
      // if displaced header is null, the previous enter is recursive enter, no-op
      markOop dhw = lock->displaced_header();
      markOop mark ;
      if (dhw == NULL) { // 在slow_enter函数里,递归重入时锁记录的displaced header会被初始化成0
         // Recursive stack-lock.
         // Diagnostics -- Could be: stack-locked, inflating, inflated.
         mark = object->mark() ; // 对象自己的mark word
         assert (!mark->is_neutral(), "invariant") ; // 对象肯定是被锁定的状态
         if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
            assert(THREAD->is_lock_owned((address)mark->locker()), "invariant") ;
         }
         if (mark->has_monitor()) {
            ObjectMonitor * m = mark->monitor() ;
            assert(((oop)(m->object()))->mark() == mark, "invariant") ;
            assert(m->is_entered(THREAD), "invariant") ;
         }
         return ;
      }
    
      mark = object->mark() ; // 对象自己的mark word
    
      // If the object is stack-locked by the current thread, try to
      // swing the displaced header from the box back to the mark.
      if (mark == (markOop) lock) { // 如果是从无锁转变到轻量级锁,那么锁记录的displaced header保存的是对象加锁之前的mark word
         assert (dhw->is_neutral(), "invariant") ; // 对象加锁之前的mark word后两位一定是01
         if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) { // 将对象的mark word恢复到加锁之前的状态
            TEVENT (fast_exit: release stacklock) ;
            return;
         }
      }
      // 如果不是上述两种情况,那么该释放重量级监视器
      ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
    }
    

    inflate函数

    inflate静态函数用于返回对象的监视器,其代码如下所示:

    ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
      // Inflate mutates the heap ...
      // Relaxing assertion for bug 6320749.
      assert (Universe::verify_in_progress() ||
              !SafepointSynchronize::is_at_safepoint(), "invariant") ;
    
      for (;;) {
          const markOop mark = object->mark() ; // 对象的mark word
          assert (!mark->has_bias_pattern(), "invariant") ; // 一定不是偏向锁
    
          // The mark can be in one of the following states:
          // *  Inflated     - just return
          // *  Stack-locked - coerce it to inflated
          // *  INFLATING    - busy wait for conversion to complete
          // *  Neutral      - aggressively inflate the object.
          // *  BIASED       - Illegal.  We should never see this
    
          // CASE: inflated
          if (mark->has_monitor()) { // 已经是监视器锁状态,mark word已经指向了监视器,即最后两位是10
              ObjectMonitor * inf = mark->monitor() ; // 先前填充过的监视器
              assert (inf->header()->is_neutral(), "invariant");
              assert (inf->object() == object, "invariant") ; // 先前填充过的监视器属于参数object
              assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
              return inf ;
          }
    
          // CASE: inflation in progress - inflating over a stack-lock.
          // Some other thread is converting from stack-locked to inflated.
          // Only that thread can complete inflation -- other threads must wait.
          // The INFLATING value is transient.
          // Currently, we spin/yield/park and poll the markword, waiting for inflation to finish.
          // We could always eliminate polling by parking the thread on some auxiliary list.
          if (mark == markOopDesc::INFLATING()) { // 正在填充,需要重试,markOopDesc::INFLATING()返回0
             TEVENT (Inflate: spin while INFLATING) ;
             ReadStableMark(object) ;
             continue ;
          }
    
          // CASE: stack-locked
          // Could be stack-locked either by this thread or by some other thread.
          //
          // Note that we allocate the objectmonitor speculatively, _before_ attempting
          // to install INFLATING into the mark word.  We originally installed INFLATING,
          // allocated the objectmonitor, and then finally STed the address of the
          // objectmonitor into the mark.  This was correct, but artificially lengthened
          // the interval in which INFLATED appeared in the mark, thus increasing
          // the odds of inflation contention.
          //
          // We now use per-thread private objectmonitor free lists.
          // These list are reprovisioned from the global free list outside the
          // critical INFLATING...ST interval.  A thread can transfer
          // multiple objectmonitors en-mass from the global free list to its local free list.
          // This reduces coherency traffic and lock contention on the global free list.
          // Using such local free lists, it doesn't matter if the omAlloc() call appears
          // before or after the CAS(INFLATING) operation.
          // See the comments in omAlloc().
    
          if (mark->has_locker()) { // 参数object被轻量级锁锁定,这里不需要管是被谁锁定的,因为这个函数只是将轻量锁膨胀成监视器锁
              ObjectMonitor * m = omAlloc (Self) ;
              // Optimistically prepare the objectmonitor - anticipate successful CAS
              // We do this before the CAS in order to minimize the length of time
              // in which INFLATING appears in the mark.
              m->Recycle();
              m->_Responsible  = NULL ;
              m->OwnerIsThread = 0 ; // _owner是锁记录指针,见1312行
              m->_recursions   = 0 ;
              m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class
    
              markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
              if (cmp != mark) {
                 omRelease (Self, m, true) ;
                 continue ;       // Interference -- just retry
              }
    
              // We've successfully installed INFLATING (0) into the mark-word.
              // This is the only case where 0 will appear in a mark-work.
              // Only the singular thread that successfully swings the mark-word
              // to 0 can perform (or more precisely, complete) inflation.
              //
              // Why do we CAS a 0 into the mark-word instead of just CASing the
              // mark-word from the stack-locked value directly to the new inflated state?
              // Consider what happens when a thread unlocks a stack-locked object.
              // It attempts to use CAS to swing the displaced header value from the
              // on-stack basiclock back into the object header.  Recall also that the
              // header value (hashcode, etc) can reside in (a) the object header, or
              // (b) a displaced header associated with the stack-lock, or (c) a displaced
              // header in an objectMonitor.  The inflate() routine must copy the header
              // value from the basiclock on the owner's stack to the objectMonitor, all
              // the while preserving the hashCode stability invariants.  If the owner
              // decides to release the lock while the value is 0, the unlock will fail
              // and control will eventually pass from slow_exit() to inflate.  The owner
              // will then spin, waiting for the 0 value to disappear.   Put another way,
              // the 0 causes the owner to stall if the owner happens to try to
              // drop the lock (restoring the header from the basiclock to the object)
              // while inflation is in-progress.  This protocol avoids races that might
              // would otherwise permit hashCode values to change or "flicker" for an object.
              // Critically, while object->mark is 0 mark->displaced_mark_helper() is stable.
              // 0 serves as a "BUSY" inflate-in-progress indicator.
    
    
              // fetch the displaced mark from the owner's stack.
              // The owner can't die or unwind past the lock while our INFLATING
              // object is in the mark.  Furthermore the owner can't complete
              // an unlock on the object, either.
              markOop dmw = mark->displaced_mark_helper() ; // 从无锁到轻量级锁时栈桢锁记录的displaced header所保存的参数object的原mark word
              assert (dmw->is_neutral(), "invariant") ; // 上面一行的原mark word一定是无锁的
    
              // Setup monitor fields to proper values -- prepare the monitor
              m->set_header(dmw) ;
    
              // Optimization: if the mark->locker stack address is associated
              // with this thread we could simply set m->_owner = Self and
              // m->OwnerIsThread = 1. Note that a thread can inflate an object
              // that it has stack-locked -- as might happen in wait() -- directly
              // with CAS.  That is, we can avoid the xchg-NULL .... ST idiom.
              m->set_owner(mark->locker()); // _owner是参数object的mark word里的锁记录指针
              m->set_object(object);
              // TODO-FIXME: assert BasicLock->dhw != 0.
    
              // Must preserve store ordering. The monitor state must
              // be stable at the time of publishing the monitor address.
              guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
              object->release_set_mark(markOopDesc::encode(m));
    
              // 省略一些代码
              return m ;
          }
    
          // CASE: neutral
          // TODO-FIXME: for entry we currently inflate and then try to CAS _owner.
          // If we know we're inflating for entry it's better to inflate by swinging a
          // pre-locked objectMonitor pointer into the object header.   A successful
          // CAS inflates the object *and* confers ownership to the inflating thread.
          // In the current implementation we use a 2-step mechanism where we CAS()
          // to inflate and then CAS() again to try to swing _owner from NULL to Self.
          // An inflateTry() method that we could call from fast_enter() and slow_enter()
          // would be useful.
    
          assert (mark->is_neutral(), "invariant"); // 无锁状态,尚未给参数object分配监视器
          ObjectMonitor * m = omAlloc (Self) ;
          // prepare m for installation - set monitor to initial state
          m->Recycle();
          m->set_header(mark); // 新建的监视器_header字段保存的是无锁时参数object的mark word
          m->set_owner(NULL); // 初值为NULL
          m->set_object(object);
          m->OwnerIsThread = 1 ; // _owner是线程指针
          m->_recursions   = 0 ;
          m->_Responsible  = NULL ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class
          // 使用CAS操作将监视器指针安装到参数object的mark word,若失败则重试
          if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
              m->set_object (NULL) ;
              m->set_owner  (NULL) ;
              m->OwnerIsThread = 0 ;
              m->Recycle() ;
              omRelease (Self, m, true) ;
              m = NULL ;
              continue ;
              // interference - the markword changed - just retry.
              // The state-transitions are one-way, so there's no chance of
              // live-lock -- "Inflated" is an absorbing state.
          }
    
          // 省略一些代码
          return m ;
      }
    }
    

    根据对象mark word的不同状态,inflate函数有不同的处理方式:

    • 已经是监视器锁状态:直接返回先前的监视器;
    • 正在填充(值为0):重试;
    • 已经被轻量锁锁定:将轻量锁膨胀成监视器;
    • 无锁状态:新建监视器并关联该对象。

    获得与释放监视器请见后续文章。

    参考文献

    [1] David Dice. Implementing Fast Java Monitors with Relaxed-Locks. 2001
    [2] JVM同步方法之偏向锁 https://zhuanlan.zhihu.com/p/34662715

    相关文章

      网友评论

        本文标题:Java中的synchronized关键字(一)

        本文链接:https://www.haomeiwen.com/subject/hijjeqtx.html