您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

从C源码看Java同步锁机制的演变

时间:2023-11-07 14:00:28  来源:今日头条  作者:代码小人物

什么是重量级锁?

重量级锁是一种同步机制,通常与在多线程环境中使用synchronized关键字实现同步相关。

由于其实现的开销和复杂性较高,因此被称为“重量级”,适合需要更严格的同步和并发控制的场景。

private synchronized void oneLock() {
    //doSomething();
}

两个线程t1和t2正在同时访问该oneLock()方法。如果t1先获取锁并执行其中的同步代码块,并且 t2 也尝试访问oneLock() 方法,则它将被阻止,因为锁由t1 持有。

在这种情况下,锁处于称为重量级锁的状态。

从上面的例子可以看出,t2由于无法获取锁,因此被挂起,等待t1释放锁后再被唤醒。

线程的挂起和唤醒涉及CPU内的上下文切换,这会产生很大的开销。

由于这个过程的成本相对较高,具有这种行为的锁被称为重量级锁。

什么是轻量级锁

轻量级锁是一种同步机制,旨在减轻与传统重量级锁(例如 JAVA synchronized关键字提供的锁)相关的性能开销。

继续前面的示例,让我们现在考虑t1和t2交替执行oneLock()方法。

在这种情况下,t1和t2不需要阻塞,因为它们之间没有争用。换句话说,不需要重量级的锁。

当线程交替执行临界区而不发生争用时,这种场景下使用的锁被称为轻量级锁。

轻量级锁相对于重量级锁的优点:

1、每次加锁只需要一次CAS操作。
2. 无需分配ObjectMonitor对象。
3、线程不需要被挂起或唤醒。

什么是偏向锁?

在只有一个线程(假设 t1)一致执行oneLock()方法的情况下,使用轻量级锁t1在每次获取锁时执行 CAS 操作。这可能会导致一些性能开销。

于是,偏向锁的概念就出现了。

当锁偏向特定线程时,该线程可以再次获取锁,而无需进行 CAS 操作。相反,简单的比较就足以获得锁。这个过程非常高效。

偏向锁相比轻量级锁的优点:

  • 当同一个线程多次获取锁时,不需要再次执行 CAS 操作。简单的比较就足够了。

怎样加锁?

让我们从源代码的角度深入研究一下 Java 中这些锁是如何实现的。

锁的本质在于共享变量,所以问题的关键是如何访问这些共享变量。了解这一点就了解了这三种锁的演变过程的一半。

接下来我将从源码分析的角度重点介绍一下这些信息。

既然我们处理的是锁,自然就涉及到锁的获取和释放操作,而在偏向锁的情况下,还有锁撤销操作。

对象头是Java对象在内存中布局的一部分,用于存储对象的元数据信息和锁定状态。

从C源码看Java同步锁机制的演变

 

在深入源码之前,我们先推测一下线程 t1 获取偏向锁的过程:

  1. 首先检查Mark word中的线程ID是否有值。
  2. 如果没有,则意味着还没有线程获得锁。本例中,直接将t1的线程ID记录到Mark Word中。多个线程可能会尝试同时修改Mark Word,因此需要CAS操作来修改Mark Word。
  3. 如果已经有一个 ID 值,那么有两种可能性:
  • 如果该ID是t1的ID,那么本次锁获取就是一个可重入的过程,t1可以直接获取锁。
  • 如果该ID不是t1的ID,则意味着另一个线程已经获取了锁。这种情况下,t1需要经过撤销过程来获取锁。
CASE(_monitorenter): {
// 1. 获取对象头,表示为“oop”(普通对象指针)。
  oop lockee = STACK_OBJECT(-1);
  CHECK_NULL(lockee);
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
  BasicObjectLock* entry = NULL;
  while (most_recent != limit ) {
// 2. 遍历线程栈找到对应的可用BasicObjectLock。  
    if (most_recent->obj() == NULL) entry = most_recent;
    else if (most_recent->obj() == lockee) break;
    most_recent++;
  }
  if (entry != NULL) {
// 3. BasicObjectLock 的 _obj 字段指向 oop。
    entry->set_obj(lockee);
    int success = false;
    uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;

// 从对象头中检索标记
    markOop mark = lockee->mark();
    intptr_t hash = (intptr_t) markOopDesc::no_hash;
// 检查是否支持偏向锁定。
    if (mark->has_bias_pattern()) {
      uintptr_t thread_ident;
      uintptr_t anticipated_bias_locking_value;
      thread_ident = (uintptr_t)istate->thread();
// 4. 获取异或运算的结果。
      anticipated_bias_locking_value =
        (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
        ~((uintptr_t) markOopDesc::age_mask_in_place);

      if  (anticipated_bias_locking_value == 0) {
// 5. 如果相等,则认为是可重入获取锁。
        if (PrintBiasedLockingStatistics) {
          (* BiasedLocking::biased_lock_entry_count_addr())++;
        }
        success = true;
      }
      else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
// 6. 如果不支持偏向锁
        markOop header = lockee->klass()->prototype_header();
        if (hash != markOopDesc::no_hash) {
          header = header->copy_set_hash(hash);
        }
// 执行CAS操作,将Mark Word修改为解锁状态。
        if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
          if (PrintBiasedLockingStatistics)
            (*BiasedLocking::revoked_lock_entry_count_addr())++;
        }
      }
      else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// 7. 如果epoch已过期,则使用当前线程的ID构造偏向锁
        markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
        if (hash != markOopDesc::no_hash) {
          new_header = new_header->copy_set_hash(hash);
        }
        if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
          if (PrintBiasedLockingStatistics)
            (* BiasedLocking::rebiased_lock_entry_count_addr())++;
        } else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
        success = true;
      }
      else {
// 8. 构造一个匿名偏向锁。
        markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
                                                        (uintptr_t)markOopDesc::age_mask_in_place |
                                                        epoch_mask_in_place));
        if (hash != markOopDesc::no_hash) {
          header = header->copy_set_hash(hash);
        }
// 构造一个指向当前线程的偏向锁。
        markOop new_header = (markOop) ((uintptr_t) header | thread_ident);

        DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
// 执行CAS操作将锁修改为与当前线程关联的偏向锁。      
        if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
          if (PrintBiasedLockingStatistics)
            (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
        }
        else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
        success = true;
      }
    }

    if (!success) {
// 如果尝试使用偏向锁不成功,系统会尝试将锁升级为轻量级锁。
      markOop displaced = lockee->mark()->set_unlocked();
      entry->lock()->set_displaced_header(displaced);
      bool call_vm = UseHeavyMonitors;
      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
          entry->lock()->set_displaced_header(NULL);
        } else {
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    }
    UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
  } else {
    istate->set_msg(more_monitors);
    UPDATE_PC_AND_RETURN(0); // Re-execute
  }
}

代码比较多,下面我将对代码注释中注释1-8标注的内容进行详细解释。

# 1.oop代表对象头,包含Mark Word和Klass Word。

# 2.BasicObjectLock的结构如下:

#basicLock.hpp
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                  
  oop       _obj;                                   
  ...
};

class BasicLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  volatile markOop _displaced_header;
  ...
};

BasicObjectLock是著名的Lock Record的实现,它包括两个元素:

  1. 存储Mark Word的移位头_displaced_header。
  2. 指向对象头的指针:_obj。

# 3、将Lock Record中的_obj字段赋值给lockee,代表对象头。

# 4. 从对象头lockee中,检索Klass Word,它是指向Klass类型的指针。在Klass类内部,有一个名为_prototype_header的字段,它也代表Mark Word。它存储偏向锁定标志之类的信息。

在此步骤中,提取此信息并将其与当前线程 ID 连接起来。

然后与对象头中的Mark Word 执行XOR 运算。目标是识别不同的位。

后续步骤涉及确定Mark Word的哪些特定部分不相等,从而导致不同的处理逻辑。

# 5. 如果上面的异或运算结果相等,则表明Mark Word中包含当前线程ID,并且epoch和偏向锁标志一致。

这表明该锁已经被当前线程持有,表明是可重入的。由于线程已经拥有锁,因此不需要采取进一步的操作。

# 6. 观察Mark Word中的偏向锁标志与Klass中的偏向锁标志不一致,并且考虑到Mark Word已经被识别为具有偏向锁,因此可以推断Klass不再支持偏向锁。

鉴于不支持偏向锁定,标记字被修改以反映解锁状态。这为进一步升级到轻量级锁定或重量级锁定做好了准备。

# 7. 在识别出 Mark Word 中的纪元与 Klass 中的标记之间的差异后,可以推断发生了批量重新偏置。这种情况下,直接修改Mark Word,使其偏向当前线程。

# 8、如果以上条件都不满足,则表明是匿名偏向锁(不偏向任何线程的偏向锁)。在这种情况下,会尝试直接修改Mark Word以偏向当前线程

总结

  • 每次线程尝试获取锁时,都需要关联一个锁记录,并将“_obj”指针设置为对象头。这在锁定记录和对象头之间建立了连接。
  • 一旦线程成功将自己的线程ID写入Mark Word,就表明该线程已经获得了偏向锁。

在偏向锁状态下,锁记录和对象头之间建立了关系。这种关系由指向对象头的锁定记录的 _obj 字段表示。

从C源码看Java同步锁机制的演变

 

我们回顾一下线程t1和t2获取偏向锁的过程:

  1. 线程 t1 尝试获取锁。最初,锁处于匿名偏向状态, T1成功获取锁。
  2. 线程 t1 尝试再次获取锁。由于它已经持有锁,所以他会来获取可重入锁。
  3. 同时,线程 t2 尝试获取锁。由于 t1 当前持有锁定,因此 t2 会锁撤销。

锁撤销

如果尝试获取偏向锁不成功,锁将恢复为未锁定状态,然后升级为轻量级锁。此过程称为偏向锁撤销。

#InterpreterRuntime.cpp
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ...
  if (UseBiasedLocking) {
// 当使用偏向锁时,进程进入快速路径执行。
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
// 升级为轻量级锁。
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  ...

#synchronizer.cpp
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) ;
}

可见,撤销分为安全点撤销和非安全点撤销。

非安全点撤销,也称为“revoke_and_rebias”,发生在未等待安全点而撤销偏向锁时。在这个过程中,偏向锁被直接撤销,并且对象的标记字被更新以反映新的状态,而不需要安全点来保证一致的状态转换。

当发生非安全点撤销时,偏向锁的状态从偏向变为正常或可重偏向。

如果它更改为可重偏向状态,则意味着如果另一个线程寻求该锁,该锁可以再次偏向。

这允许更快、更有效的锁定转换,因为如果另一个线程在撤销后不久获取该锁,则该锁可能会跳过中间状态并直接进入偏向状态。

从本质上讲,非安全点撤销减少了等待安全点的需要,并实现了更灵活、响应更灵敏的方法来撤销偏向锁,从而提高了性能并减少了某些场景下的锁争用。

批量重新偏向和批量撤销

经过以上分析,我们了解到以下几点:

  • 当一个线程持有偏向锁,而另一个线程试图获取该锁时,需要撤销该锁。
  • 撤销过程首先尝试使用CAS在非安全点将Mark Word更改为解锁状态。如果仍然无法实现撤销,则可以考虑在安全点执行撤销的选项,尽管在安全点执行撤销的效率相对较低。

因此,偏向锁引入了批量重偏向和批量撤销的概念。

当对象的锁被撤销的次数达到一定阈值时,例如20次,就会触发批量重偏逻辑。

这涉及到修改 Klass 中的标记以及当前使用的该类型锁的 Mark Word 中的标记。

当线程尝试获取偏向锁时,它会将当前对象的纪元值与 Klass 中的标记值进行比较。

如果不相等,则认为锁已过期。在这种情况下,允许线程直接CAS修改Mark Word以偏向当前线程,避免撤销逻辑。这对应于偏向锁进入最初讨论中的分析标签(7)。

同样,当撤销次数达到40次时,就认为该对象不再适合偏向锁。

因此,Klass 中的偏向锁标志发生更改,以指示不再支持偏向锁。

当线程尝试获取偏向锁时,它会检查 Klass 中的偏向锁标志。如果不再允许偏差,则表明批次撤销较早发生。

在这种情况下,允许线程直接CAS将Mark Word修改为解锁状态,避免了撤销逻辑。这对应于偏向锁进入最初讨论中的分析标签(6)。

批量重新偏向和批量撤销是旨在提高偏向锁定性能的优化。

锁释放


#bytecodeInterpreter.cpp
CASE(_monitorexit): {
  oop lockee = STACK_OBJECT(-1);
  CHECK_NULL(lockee);
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
// 遍历线程栈
  while (most_recent != limit ) {
// 查找对应的锁记录
    if ((most_recent)->obj() == lockee) {
      BasicLock* lock = most_recent->lock();
      markOop header = lock->displaced_header();
// 将锁定记录中的_obj字段设置为null
      most_recent->set_obj(NULL);
// 这是轻量级锁的释放。
      UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
    }
    most_recent++;
  }
  ...
}

您可能已经注意到,Mark Word 没有改变;它仍然偏向于前一个线程。然而,锁还没有被释放。事实上,当线程退出临界区时,它不会释放偏向锁。

原因是:

当再次需要锁时,简单的按位比较就可以快速判断是否是可重入获取。这意味着不需要每次都执行CAS操作就可以高效地获取锁。这种效率是偏向锁在只有一个线程访问锁的场景下的核心优势。

总结

  1. 偏向锁中的“锁”指的是Mark Word。修改Mark Word是获取锁所必需的,由于潜在的多线程争用,这可能会涉及CAS操作。
  2. 由于撤销操作在安全点执行时效率可能较低,并且多次撤销会进一步影响效率,因此引入了批量重偏和撤销机制。
  3. 偏向锁的可重入计数取决于线程堆栈中存在的锁记录的数量。
  4. 如果偏向锁撤销失败,锁最终会升级为轻量级锁。
  5. 退出时,偏向锁不会修改Mark Word,也就是说锁没有被释放。

未完待续。。。。。



Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  Search: Java  点击:(4)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践——如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  Search: Java  点击:(25)  评论:(0)  加入收藏
Oracle正式发布Java 22
Oracle 正式发布 Java 22,这是备受欢迎的编程语言和开发平台推出的全新版本。Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进,包括对Java 语言、其API...【详细内容】
2024-03-21  Search: Java  点击:(10)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  Search: Java  点击:(20)  评论:(0)  加入收藏
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  Search: Java  点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20  Search: Java  点击:(21)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18  Search: Java  点击:(24)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  Search: Java  点击:(2)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  Search: Java  点击:(2)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06  Search: Java  点击:(52)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(21)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(24)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(56)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(68)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(72)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(88)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(105)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(95)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(74)  评论:(0)  加入收藏
站内最新
站内热门
站内头条