- 浏览: 741422 次
- 性别:
- 来自: 大连
文章分类
最新评论
-
xuelj:
我的Android进阶之旅------>Android使用9Patch图片作为不失真背景 -
lBovinl:
,浩瀚~
我的Android进阶之旅------>经典的大客推荐(排名不分先后)!! -
xyy_zero:
看着听犀利的,大神,有demo吗?可以分享下不。我最近需要开发 ...
给android设备增加串口功能
我的Java开发学习之旅------>Java双重检查锁定及单例模式详解(转)
简介:
所有的编程语言都有一些共用的习语。了解和使用一些习语很有用,程序员们花费宝贵的时间来创建、学习和实现这些习语。问题是,稍后经过证明,一些习语并不完全如其所声称的那样,或者仅仅是与描述的功能不符。在 Java 编程语言中,双重检查锁定就是这样的一个绝不应该使用的习语。在本文中,Peter Haggar 介绍了双重检查锁定习语的渊源,开发它的原因和它失效的原因。
单例创建模式是一个通用的编程习语。和多线程一起使用时,必需使用某种类型的同步。在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效。它偶尔会失败,而不是总失败。此外,它失败的原因并不明显,还包含 Java 内存模型的一些隐秘细节。这些事实将导致代码失败,原因是双重检查锁定难于跟踪。在本文余下的部分里,我们将详细介绍双重检查锁定习语,从而理解它在何处失效。
要理解双重检查锁定习语是从哪里起源的,就必须理解通用单例创建习语,如清单 1 中的阐释:
清单1:单例创建
import java.util.*;
class Singleton
{
private static Singleton instance;
private Vector v;
private boolean inUse;
private Singleton()
{
v = new Vector();
v.addElement(new Object());
inUse = true;
}
public static Singleton getInstance()
{
if (instance == null) //1
instance = new Singleton(); //2
return instance; //3
}
}
此类的设计确保只创建一个Singleton
对象。构造函数被声明为private
,getInstance()
方法只创建一个对象。这个实现适合于单线程程序。然而,当引入多线程时,就必须通过同步来保护getInstance()
方法。如果不保护getInstance()
方法,则可能返回Singleton
对象的两个不同的实例。假设两个线程并发调用getInstance()
方法并且按以下顺序执行调用:
-
线程 1 调用
getInstance()
方法并决定instance
在 //1 处为null
。
-
线程 1 进入
if
代码块,但在执行 //2 处的代码行时被线程 2 预占。
-
线程 2 调用
getInstance()
方法并在 //1 处决定instance
为null
。
-
线程 2 进入
if
代码块并创建一个新的Singleton
对象并在 //2 处将变量instance
分配给这个新对象。
-
线程 2 在 //3 处返回
Singleton
对象引用。
-
线程 2 被线程 1 预占。
-
线程 1 在它停止的地方启动,并执行 //2 代码行,这导致创建另一个
Singleton
对象。
- 线程 1 在 //3 处返回这个对象。
结果是getInstance()
方法创建了两个Singleton
对象,而它本该只创建一个对象。通过同步getInstance()
方法从而在同一时间只允许一个线程执行代码,这个问题得以改正,如清单 2 所示:
清单 2 中线程安全的getInstance()
方法
public static synchronized Singleton getInstance() { if (instance == null) //1 instance = new Singleton(); //2 return instance; //3 }
清单 2 中的代码针对多线程访问getInstance()
方法运行得很好。然而,当分析这段代码时,您会意识到只有在第一次调用方法时才需要同步。由于只有第一次调用执行了 //2 处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。所有其他调用用于决定instance
是非null
的,并将其返回。多线程能够安全并发地执行除第一次调用外的所有调用。尽管如此,由于该方法是synchronized
的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。
为使此方法更为有效,一个被称为双重检查锁定的习语就应运而生了。这个想法是为了避免对除第一次调用外的所有调用都实行同步的昂贵代价。同步的代价在不同的 JVM 间是不同的。在早期,代价相当高。随着更高级的 JVM 的出现,同步的代价降低了,但出入synchronized
方法或块仍然有性能损失。不考虑 JVM 技术的进步,程序员们绝不想不必要地浪费处理时间。
因为只有清单 2 中的 //2 行需要同步,我们可以只将其包装到一个同步块中,如清单 3 所示:
清单 3getInstance()
方法public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
清单 3 中的代码展示了用多线程加以说明的和清单 1 相同的问题。当
instance
为null
时,两个线程可以并发地进入if
语句内部。然后,一个线程进入synchronized
块来初始化instance
,而另一个线程则被阻断。当第一个线程退出synchronized
块时,等待着的线程进入并创建另一个Singleton
对象。注意:当第二个线程进入synchronized
块时,它并没有检查instance
是否非null
。
双重锁定检查
为处理清单 3 中的问题,我们需要对instance
进行第二次检查。这就是“双重检查锁定”名称的由来。将双重检查锁定习语应用到清单
3 的结果就是清单 4 。
清单 4. 双重检查锁定示例
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
if (instance == null) //2
instance = new Singleton(); //3
}
}
return instance;
}
双重检查锁定背后的理论是:在 //2 处的第二次检查使(如清单 3 中那样)创建两个不同的Singleton
对象成为不可能。假设有下列事件序列:
-
线程 1 进入
getInstance()
方法。
-
由于
instance
为null
,线程 1 在 //1 处进入synchronized
块。
-
线程 1 被线程 2 预占。
-
线程 2 进入
getInstance()
方法。
-
由于
instance
仍旧为null
,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 在 //1 处阻塞。
-
线程 2 被线程 1 预占。
-
线程 1 执行,由于在 //2 处实例仍旧为
null
,线程 1 还创建一个Singleton
对象并将其引用赋值给instance
。
-
线程 1 退出
synchronized
块并从getInstance()
方法返回实例。
-
线程 1 被线程 2 预占。
-
线程 2 获取 //1 处的锁并检查
instance
是否为null
。
-
由于
instance
是非null
的,并没有创建第二个Singleton
对象,由线程 1 创建的对象被返回。
双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。
无序写入
为解释该问题,需要重新考察上述清单 4 中的 //3 行。此行代码创建了一个Singleton
对象并初始化变量instance
来引用此对象。这行代码的问题是:在Singleton
构造函数体执行之前,变量instance
可能成为非null
的。
什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设清单 4 中代码执行以下事件序列:
-
线程 1 进入
getInstance()
方法。
-
由于
instance
为null
,线程 1 在 //1 处进入synchronized
块。
-
线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非
null
。
-
线程 1 被线程 2 预占。
-
线程 2 检查实例是否为
null
。因为实例不为 null,线程 2 将instance
引用返回给一个构造完整但部分初始化了的Singleton
对象。
-
线程 2 被线程 1 预占。
-
线程 1 通过运行
Singleton
对象的构造函数并将引用返回给它,来完成对该对象的初始化。
此事件序列发生在线程 2 返回一个尚未执行构造函数的对象的时候。
为展示此事件的发生情况,假设为代码行instance =new Singleton();
执行了下列伪代码:instance =new Singleton();
mem = allocate(); //Allocate memory for Singleton object.
instance = mem; //Note that instance is now non-null, but
//has not been initialized.
ctorSingleton(instance); //Invoke constructor for Singleton passing
//instance.
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。
为说明这一情况,假设有清单 5 中的代码。它包含一个剥离版的getInstance()
方法。我已经删除了“双重检查性”以简化我们对生成的汇编代码(清单 6)的回顾。我们只关心 JIT 编译器如何编译instance=new Singleton();
代码。此外,我提供了一个简单的构造函数来明确说明汇编代码中该构造函数的运行情况。
清单5. 用于演示无序写入的单例类
class Singleton
{
private static Singleton instance;
private boolean inUse;
private int val;
private Singleton()
{
inUse = true;
val = 5;
}
public static Singleton getInstance()
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
清单 6 包含由 Sun JDK 1.2.1 JIT 编译器为清单 5 中的
getInstance()
方法体生成的汇编代码。
清单 6. 由清单 5 中的代码生成的汇编代码
;asm code generated for getInstance
054D20B0 mov eax,[049388C8] ;load instance ref
054D20B5 test eax,eax ;test for null
054D20B7 jne 054D20D7
054D20B9 mov eax,14C0988h
054D20BE call 503EF8F0 ;allocate memory
054D20C3 mov [049388C8],eax ;store pointer in
;instance ref. instance
;non-null and ctor
;has not run
054D20C8 mov ecx,dword ptr [eax]
054D20CA mov dword ptr [ecx],1 ;inline ctor - inUse=true;
054D20D0 mov dword ptr [ecx+4],5 ;inline ctor - val=5;
054D20D7 mov ebx,dword ptr ds:[49388C8h]
054D20DD jmp 054D20B0
注:为引用下列说明中的汇编代码行,我将引用指令地址的最后两个值,因为它们都以054D20
开头。例如,B5
代表test eax,eax
。
汇编代码是通过运行一个在无限循环中调用getInstance()
方法的测试程序来生成的。程序运行时,请运行 Microsoft Visual C++ 调试器并将其附到表示测试程序的 Java 进程中。然后,中断执行并找到表示该无限循环的汇编代码。
B0
和B5
处的前两行汇编代码将instance
引用从内存位置049388C8
加载至eax
中,并进行null
检查。这跟清单 5 中的getInstance()
方法的第一行代码相对应。第一次调用此方法时,instance
为null
,代码执行到B9
。BE
处的代码为Singleton
对象从堆中分配内存,并将一个指向该块内存的指针存储到eax
中。下一行代码,C3
,获取eax
中的指针并将其存储回内存位置为049388C8
的实例引用。结果是,instance
现在为非null
并引用一个有效的Singleton
对象。然而,此对象的构造函数尚未运行,这恰是破坏双重检查锁定的情况。然后,在C8
行处,instance
指针被解除引用并存储到ecx
。CA
和D0
行表示内联的构造函数,该构造函数将值true
和5
存储到Singleton
对象。如果此代码在执行C3
行后且在完成该构造函数前被另一个线程中断,则双重检查锁定就会失败。
不是所有的 JIT 编译器都生成如上代码。一些生成了代码,从而只在构造函数执行后使instance
成为非null
。针对 Java 技术的 IBM SDK 1.3 版和 Sun JDK 1.3 都生成这样的代码。然而,这并不意味着应该在这些实例中使用双重检查锁定。该习语失败还有一些其他原因。此外,您并不总能知道代码会在哪些 JVM 上运行,而 JIT 编译器总是会发生变化,从而生成破坏此习语的代码。
考虑到当前的双重检查锁定不起作用,我加入了另一个版本的代码,如清单 7
所示,从而防止您刚才看到的无序写入问题。
清单 7. 解决无序写入问题的尝试
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null)
{
synchronized(Singleton.class) { //3
inst = new Singleton(); //4
}
instance = inst; //5
}
}
}
return instance;
}
看着清单 7 中的代码,您应该意识到事情变得有点荒谬。请记住,创建双重检查锁定是为了避免对简单的三行getInstance()
方法实现同步。清单 7 中的代码变得难于控制。另外,该代码没有解决问题。仔细检查可获悉原因。
此代码试图避免无序写入问题。它试图通过引入局部变量inst
和第二个synchronized
块来解决这一问题。该理论实现如下:
-
线程 1 进入
getInstance()
方法。
-
由于
instance
为null
,线程 1 在 //1 处进入第一个synchronized
块。
-
局部变量
inst
获取instance
的值,该值在 //2 处为null
。
-
由于
inst
为null
,线程 1 在 //3 处进入第二个synchronized
块。
-
线程 1 然后开始执行 //4 处的代码,同时使
inst
为非null
,但在Singleton
的构造函数执行前。(这就是我们刚才看到的无序写入问题。)
-
线程 1 被线程 2 预占。
-
线程 2 进入
getInstance()
方法。
-
由于
instance
为null
,线程 2 试图在 //1 处进入第一个synchronized
块。由于线程 1 目前持有此锁,线程 2 被阻断。
-
线程 1 然后完成 //4 处的执行。
-
线程 1 然后将一个构造完整的
Singleton
对象在 //5 处赋值给变量instance
,并退出这两个synchronized
块。
-
线程 1 返回
instance
。
-
然后执行线程 2 并在 //2 处将
instance
赋值给inst
。
-
线程 2 发现
instance
为非null
,将其返回。
这里的关键行是 //5。此行应该确保instance
只为null
或引用一个构造完整的Singleton
对象。该问题发生在理论和实际彼此背道而驰的情况下。
由于当前内存模型的定义,清单 7 中的代码无效。Java 语言规范(Java Language Specification,JLS)要求不能将synchronized
块中的代码移出来。但是,并没有说不能将synchronized
块外面的代码移入synchronized
块中。
JIT 编译器会在这里看到一个优化的机会。此优化会删除 //4 和 //5 处的代码,组合并且生成清单 8 中所示的代码。
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null)
{
synchronized(Singleton.class) { //3
//inst = new Singleton(); //4
instance = new Singleton();
}
//instance = inst; //5
}
}
}
return instance;
}
如果进行此项优化,您将同样遇到我们之前讨论过的无序写入问题。
用 volatile 声明每一个变量怎么样?
另一个想法是针对变量inst
以及instance
使用关键字volatile
。根据 JLS(参见参考资料),声明成volatile
的变量被认为是顺序一致的,即,不是重新排序的。但是试图使用volatile
来修正双重检查锁定的问题,会产生以下两个问题:
-
这里的问题不是有关顺序一致性的,而是代码被移动了,不是重新排序。
-
即使考虑了顺序一致性,大多数的 JVM 也没有正确地实现
volatile
。
第二点值得展开讨论。假设有清单 9 中的代码:
清单 9. 使用了 volatile 的顺序一致性
class test
{
private volatile boolean stop = false;
private volatile int num = 0;
public void foo()
{
num = 100; //This can happen second
stop = true; //This can happen first
//...
}
public void bar()
{
if (stop)
num += num; //num can == 0!
}
//...
}
根据 JLS,由于stop
和num
被声明为volatile
,它们应该顺序一致。这意味着如果stop
曾经是true
,num
一定曾被设置成100
。尽管如此,因为许多
JVM 没有实现volatile
的顺序一致性功能,您就不能依赖此行为。因此,如果线程 1 调用foo
并且线程 2 并发地调用bar
,则线程
1 可能在num
被设置成为100
之前将stop
设置成true
。这将导致线程见到stop
是true
,而num
仍被设置成0
。使用volatile
和
64 位变量的原子数还有另外一些问题,但这已超出了本文的讨论范围。有关此主题的更多信息,请参阅下面的参考资料。解决方案
底线就是:无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。JSR-133 是有关内存模型寻址问题的,尽管如此,新的内存模型也不会支持双重检查锁定。因此,您有两种选择:
-
接受如清单 2 中所示的
getInstance()
方法的同步。
-
放弃同步,而使用一个
static
字段。
选择项 2 如清单 10 中所示
清单 10. 使用 static 字段的单例实现
class Singleton
{
private Vector v;
private boolean inUse;
private static Singleton instance = new Singleton();
private Singleton()
{
v = new Vector();
inUse = true;
//...
}
public static Singleton getInstance()
{
return instance;
}
}
清单 10 的代码没有使用同步,并且确保调用static getInstance()
方法时才创建Singleton
。如果您的目标是消除同步,则这将是一个很好的选择。
String 不是不变的
鉴于无序写入和引用在构造函数执行前变成非null
的问题,您可能会考虑String
类。假设有下列代码:
private String str;
//...
str = new String("hello");
String
类应该是不变的。尽管如此,鉴于我们之前讨论的无序写入问题,那会在这里导致问题吗?答案是肯定的。考虑两个线程访问String str
。一个线程能看见str
引用一个String
对象,在该对象中构造函数尚未运行。事实上,清单
11 包含展示这种情况发生的代码。注意,这个代码仅在我测试用的旧版 JVM 上会失败。IBM 1.3 和 Sun 1.3 JVM 都会如期生成不变的String
。
清单 11. 可变 String 的例子
class StringCreator extends Thread
{
MutableString ms;
public StringCreator(MutableString muts)
{
ms = muts;
}
public void run()
{
while(true)
ms.str = new String("hello"); //1
}
}
class StringReader extends Thread
{
MutableString ms;
public StringReader(MutableString muts)
{
ms = muts;
}
public void run()
{
while(true)
{
if (!(ms.str.equals("hello"))) //2
{
System.out.println("String is not immutable!");
break;
}
}
}
}
class MutableString
{
public String str; //3
public static void main(String args[])
{
MutableString ms = new MutableString(); //4
new StringCreator(ms).start(); //5
new StringReader(ms).start(); //6
}
}
此代码在 //4 处创建一个MutableString
类,它包含了一个String
引用,此引用由 //3 处的两个线程共享。在行 //5 和 //6 处,在两个分开的线程上创建了两个对象StringCreator
和StringReader
。传入一个MutableString
对象的引用。StringCreator
类进入到一个无限循环中并且使用值“hello”在
//1 处创建String
对象。StringReader
也进入到一个无限循环中,并且在 //2 处检查当前的String
对象的值是不是 “hello”。如果不行,StringReader
线程打印出一条消息并停止。如果String
类是不变的,则从此程序应当看不到任何输出。如果发生了无序写入问题,则使StringReader
看到str
引用的惟一方法绝不是值为“hello”的String
对象。
在旧版的 JVM 如 Sun JDK 1.2.1 上运行此代码会导致无序写入问题。并因此导致一个非不变的String
。
结束语
为避免单例中代价高昂的同步,程序员非常聪明地发明了双重检查锁定习语。不幸的是,鉴于当前的内存模型的原因,该习语尚未得到广泛使用,就明显成为了一种不安全的编程结构。重定义脆弱的内存模型这一领域的工作正在进行中。尽管如此,即使是在新提议的内存模型中,双重检查锁定也是无效的。对此问题最佳的解决方案是接受同步或者使用一个static
field
。
参考资料
volatile
和 64 位变量的信息,请参阅 Peter Haggar 的文章“Does Java Guarantee Thread Safety?”,发表在 2002 年 6 月那期的Dr. Dobb's Journal之上。ThreadLocal
,并提供了一些发掘它的能力的小提示。关于作者
Peter Haggar 是 IBM 在北卡罗来纳州的 Research Triangle Park 的一名高级软件工程师,他还是Practical
Java Programming Language Guide(Addison-Wesley 出版)一书的作者。此外,他还发表了很多篇关于 Java 编程的文章。他有着广泛的编程经验,曾致力于开发工具、类库和操作系统相关的工作。Peter 在 IBM 致力于研究新兴 Internet 技术,目前主要从事高性能 Web 服务方面的工作。Peter 经常在很多行业会议上作为技术发言人就 Java
技术发表言论。他已经为 IBM 工作了 14 年多,并获得了 Clarkson University 的计算机科学学士学位。您可以通过haggar@us.ibm.com与他联系。
附注:本文转载于http://www.ibm.com/developerworks/cn/java/j-dcl.html
==================================================================================================
作者:欧阳鹏 欢迎转载,与人分享是进步的源泉!
转载请保留原文地址:http://blog.csdn.net/ouyang_peng
==================================================================================================
相关推荐
"Java双重检查加锁单例模式的详解" Java双重检查加锁单例模式是一种常用的单例模式实现方法,但是在多线程环境下,它存在一些问题。在这篇文章中,我们将探讨Java双重检查加锁单例模式的详解,包括它的优点和缺点,...
单例模式是一种设计模式,它的主要目标是确保一个类只有一个实例,并提供一个全局访问点。在软件工程中,单例模式常用于控制资源的共享,比如数据库连接池、线程池或者日志系统等,这些资源通常需要全局唯一且高效地...
Java设计模式之单例模式详解 一、单例模式概览 单例模式(Singleton Pattern)是面向对象设计模式中的一种,属于创建型模式。它确保一个类仅有一个实例,并提供一个全局访问点来访问该实例。单例模式的核心在于控制...
### Java设计模式——单例模式详解 #### 一、单例模式概述 单例模式是设计模式中的一个重要组成部分,属于创建型模式之一。其主要作用是确保某个类仅有一个实例存在,并提供一个全局访问该实例的方法。这在很多场景...
### Java 单例模式详解 #### 一、什么是单例模式? 单例模式是一种常用的软件设计模式,在这种模式中,一个类只能拥有一个实例,并且该类必须自行创建并提供这个实例。通常,单例模式用于确保某个类在整个应用程序...
为了避免同步带来的性能问题,可以采用双重检查锁定(Double-Check Locking,DCL)优化懒汉式: ```java public class Singleton { private volatile static Singleton INSTANCE; private Singleton() {} ...
在软件工程中,单例模式是创建型设计模式之一,其目的是确保一个类仅有一个实例,并提供一个全局访问点。Java语言中的单例模式实现有多种方式,包括饿汉式、懒汉式和登记式单例。不同的实现方式适应不同的场景和需求...
Java中的单例模式是一种设计模式,它用于控制类的实例化过程,确保在整个应用程序中,一个类只有一个实例存在。这种模式常用于配置管理、缓存、日志记录、对话框、注册表设置、线程池等场景,以提高性能、减少资源...
单例模式是软件开发中最基本的设计模式之一,尤其在Java编程中应用广泛。它的核心思想是确保一个类仅有一个实例,并提供一个全局访问点来获取该实例。这种模式特别适用于那些只需要一个实例就可以满足系统需求的情况...
【单例模式详解】 单例模式是设计模式中的一种,它的核心思想是限制类的实例化,确保在程序运行过程中,对于某个类,全局只存在一个实例。这种模式常用于资源管理器,如打印服务、数据库连接池等,因为这些资源通常...
在Java中,有多种实现单例的方法,如饿汉式(静态常量)、懒汉式(线程不安全)、双重检查锁定(DCL)和枚举方式。其中,DCL和枚举是推荐的实现方式,因为它们既保证了线程安全,又避免了性能开销。 2. 工厂模式:...
### Java多线程单例模式详解 #### 一、单例模式概述 单例模式(Singleton Pattern)是一种常用的软件设计模式,它确保一个类仅有一个实例,并提供一个全局访问点。这种模式通常用于那些需要频繁实例化然后销毁的...
Tomcat与java web 开发技术详解-孙卫琴-第二版 这是271-540 Tomcat与java web 开发技术详解-孙卫琴-第二版 内容详细,清晰, javaweb必备
### 单例模式详解 #### 一、单例模式简介 单例模式(Singleton Pattern)是一种常用的软件设计模式,属于创建型模式之一。其目的是确保某个类只有一个实例,并提供一个全局访问点。单例模式的核心在于确保在系统...
JAVA TCP-IP应用开发详解JAVA TCP-IP应用开发详解JAVA TCP-IP应用开发详解JAVA TCP-IP应用开发详解JAVA TCP-IP应用开发详解JAVA TCP-IP应用开发详解
《Tomcat与Java Web开发技术详解》是由孙卫琴编著的第二版书籍,该书深入探讨了在Java Web开发中使用Tomcat服务器的核心技术和实践方法。Tomcat是一款开源、轻量级的Java Servlet容器,它是Java EE Web应用程序部署...
根据给定的信息,我们可以深入探讨Java...综上所述,通过本案例的学习,我们不仅了解了如何在Java中实现单例模式,还掌握了基于Swing框架开发简单图形界面的基本方法,这对于后续学习Java GUI编程具有很好的参考价值。
张孝祥Java邮件开发详解-高清扫描版-part1 绝对是精品 张孝祥的好书 。很大,分为2部分。
在安装完成后,只需双击“eclipse”目录中的可执行文件,即可启动Eclipse IDE,开始Java开发之旅。 总之,“eclipse-java-2021-09-R-linux-gtk-x86_64.tar.gz”是针对Linux 64位系统的Eclipse IDE for Java开发者...