admin 管理员组

文章数量: 887021


2024年1月13日发(作者:什么是常量表达式)

1 JAVA内存管理

1.1 java是如何管理内存的

Java的内存管理就是对象的分配和释放问题。(两部分)

分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。

释放 :对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC(Gabage Collection)为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

1.2什么叫java的内存泄露

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连(也就是说仍存在该内存对象的引用);其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

与C++内存泄露概念的区别:

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

1

1.3 JVM的内存区域组成

java把内存分两种:一种是栈内存,另一种是堆内存

1。在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配;

2。堆内存用来存放由new创建的对象和数组以及对象的实例变量

在函数(代码块)中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间;在堆中分配的内存由java虚拟机的自动垃圾回收器来管理。

堆和栈的优缺点

堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。缺点就是要在运行时动态分配内存,存取速度较慢;栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。另外,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

Java的内存管理实质上就是JVM的内存管理,JVM的内存分为两部分:stack和heap

Stack(栈)是指JVM的内存指令区。Java基本数据类型,Java指令代码,常量都存在stack中。

heap(堆)是JVM的内存数据区。heap专门用来保存对象的实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),对象实例在heap中分配好后需要在Stack中保存1个4字节的heap内存地址,用来定位该对象在heap中的位置,以便找到该对象实例。

Stack不存在内存管理问题,系统自动管理,heap中的对象由GC负责垃圾回收。

GC垃圾收集的规程:GC进程定期扫描heap,他根据stack中保存的4字节对象地址扫描heap,定位heap中的这些对象,进行一些优化,并且假设heap中的没有扫描到区域都是空闲的,统统refresh(实际上是把stack中丢失对象地址的无用对象清除了)。这就是垃圾回收的过程。

关于对象

1、方法本身是指令的操作码部分,保存在stack中;

2、方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在stack中(实际上是简单类型保存在stack中,对象实例在stack中保存地址,在heap中保存值)

2

上述的指令操作数和指令操作码构成完整的Java指令。

3、对象实例包括属性值作为数据,保存在heap中。

关于静态方法和静态属性

当一个ClassLoader load进入JVM后,方法指令保存在stack中,此时heap区没有数据,然后程序计数器开始执行指令,如果是一个静态方法,直接依次执行指令代码,当然此时指令代码无法访问heap数据区;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行之前,要先new对象,在heap中分配数据,并把stack中的地址指针交给非静态方法,这样程序计数器一次执行指令,而指令代码就能够访问到heap数据区。

由于上述的原因,静态属性是保存在stack中的(基本类型保存在stack,对象类型地址保存在stack中,值保存在heap中),并因此具有全局属性。

补充:字符串常量在stack分配,this在heap中分配,数组想对象一样既在stack中分配地址放数组名称,又在heap中分配数组实际大小的空间。

下面再说C/C++内存管理

在C++中,内存分为5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

栈:编译器自动管理,里面的变量是局部变量、函数参数等(可以用alloca函数分配)

堆:由new分配的内存块,用delete释放

自由存储区:由malloc分配,用free释放和堆很相似

全局/静态存储区:全局变量和静态变量

常量存储区:常量

1.3.1 JVM中的对象生命周期

在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了 JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。

3.1.1创建阶段

在对象创建阶段,系统要通过下面的步骤,完成对象的创建过程:

3

(1)为对象分配存储空间。 (2)开始构造对象。 (3)递归调用其超类的构造方法。

(4)进行对象实例初始化与变量初始化。(5)执行构造方法体。

上面的5个步骤中的第3步就是指递归地调用该类所扩展的所有父类的构造方法,一个Java类(除Object类外)至少有一个父类(Object),这个规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。实际上,在 Java程序设计中,任何一个Java类都直接或间接的是Object类的子类。例如下面的代码:

public class A {

}

这个声明等同于下面的声明:

public class A extends {

}

上面讲解了对象处于创建阶段时,系统所做的一些处理工作,其中有些过程与应用的性能密切相关,因此在创建对象时,我们应该遵循一些基本的规则,以提高应用的性能。

下面是在创建对象时的几个关键应用规则:

(1)避免在循环体中创建对象,即使该对象占用内存空间不大。

(2)尽量及时使对象符合垃圾回收标准。

(3)不要采用过深的继承层次。

(4)访问本地变量优于访问类中的变量。

关于规则(1)避免在循环体中创建对象,即使该对象占用内存空间不大,需要提示一下,这种情况在我们的实际应用中经常遇到,而且我们很容易犯类似的错误,例如下面的代码:

… …

for (int i = 0; i < 10000; ++i) {

Object obj = new Object();

n("obj= "+ obj);

}

… …

4

上面代码的书写方式相信对你来说不会陌生,也许在以前的应用开发中你也这样做过,尤其是在枚举一个Vector对象中的对象元素的操作中经常会这样书写,但这却违反了上述规则(1),因为这样会浪费较大的内存空间,正确的方法如下所示:

… …

Object obj = null;

for (int i = 0; i < 10000; ++i) {

obj = new Object();

n("obj= "+ obj);

}

… …

采用上面的第二种编写方式,仅在内存中保存一份对该对象的引用,而不像上面的第一种编写方式中代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。因此在循环体中声明创建对象的编写方式应该尽量避免。

另外,不要对一个对象进行多次初始化,这同样会带来较大的内存开销,降低系统性能,如:

public class A {

private Hashtable table = new Hashtable ();

public A() {

// 将Hashtable对象table初始化了两次

table = new Hashtable();

}

}

正确的方式为:

public class B {

private Hashtable table = new Hashtable ();

public B() {

}

}

不要小看这个差别,它却使应用软件的性能相差甚远,如图2-5所示。

5

图2-5 初始化对象多次所带来的性能差别

看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则我们开发出来的应用也是低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。因此,我们在日常的应用开发中,应该认真对待每一行代码,采用最优化的编写方式,不要忽视细节,不要忽视潜在的问题。

3.1.2应用阶段

当对象的创建阶段结束之后,该对象通常就会进入对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身“存在价值”的时期。在对象的应用阶段,对象具备下列特征:

◆系统至少维护着对象的一个强引用(Strong Reference);

◆所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。

上面提到了几种不同的引用类型。可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。在讲解这几种不同类型的引用之前,我们必须先了解一下Java中对象引用的结构层次。 Java对象引用的结构层次示意如图2-6所示。

6

图2-6 对象引用的结构层次示意

由图2-6我们不难看出,上面所提到的几种引用的层次关系,其中强引用处于顶端,而虚引用则处于底端。下面分别予以介绍。

1.强引用

强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。

2.软引用

软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。再者,软可到达对象的所有软引用都要保证在虚拟机抛出OutOfMemoryError之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。下面是软引用的实现代码:

… …

import ference;

A a = new A();

// 使用 a

7

// 使用完了a,将它设置为soft 引用类型,并且释放强引用;

SoftReference sr = new SoftReference(a);

a = null;

// 下次使用时

if (sr!=null) {

a = ();

}

else{

// GC由于内存资源不足,可能系统已回收了a的软引用,

// 因此需要重新装载。

a = new A();

sr=new SoftReference(a);

}

… …

软引用技术的引进,使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃(crash)。因此在处理一些占用内存较大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建(这里是指那些没有保留运行过程中状态的对象),提高应用对内存的使用效率,提高系统稳定性。但事物总是带有两面性的,有利亦有弊。在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻

烦,有些时候我们要权衡利弊择时应用。

3.弱引用

弱引用(Weak Reference)对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。虽然,GC在运行时一定回收Weak引用对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map数据结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null时,

8

对这个对象引用就不存在了,GC能够快速地回收该对象空间。与软引用类似我们也可以给出相应的应用代码:

… …

import ference;

A a = new A();

// 使用 a

// 使用完了a,将它设置为weak 引用类型,并且释放强引用;

WeakReference wr = new WeakReference (a);

a = null;

// 下次使用时

if (wr!=null) {

a = ();

}

else{

a = new A();

wr = new WeakReference (a);

}

… …

弱引用技术主要适用于实现无法防止其键(或值)被回收的规范化映射。另外,弱引用分为“短弱引用(Short Week Reference)”和“长弱引用(Long Week Reference)”,其区别是长弱引用在对象的Finalize方法被GC调用后依然追踪对象。基于安全考虑,不推荐使用长弱引用。因此建议使用下面的方式创建对象的弱引用。

… …

WeakReference wr = new WeakReference(obj);

9

WeakReference wr = new WeakReference(obj, false);

… …

4.虚引用

虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。虚引用主要适用于以某种比 java 终结机制更灵活的方式调度

pre-mortem 清除操作。

&注意 在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

3.1.3不可视阶段

在一个对象经历了应用阶段之后,那么该对象便处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。

… …

public void process () {

try {

Object obj = new Object();

thing();

} catch (Exception e) {

tackTrace();

}

while (isLoop) { // ... loops forever

// 这个区域对于obj对象来说已经是不可视的了

// 因此下面的代码在编译时会引发错误

thing();

}

10

}

… …

如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行thing();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。

3.1.4不可到达阶段

处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变

量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。

3.1.5 可收集阶段、终结阶段与释放阶段

对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:

(1)垃圾回收器发现该对象已经不可到达。

(2)finalize方法已经被执行。

(3)对象空间已被重用。

当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。

1.4 Java中数据在内存中是如何存储的

1.4.1 基本数据类型

Java的基本数据类型共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的。如int a = 3;这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值定义在某个程序块里面,程序块退出后,

11

字段值就消失了),出于追求速度的原因,就存在于栈中。

另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。

比如:我们同时定义:

int a = 3;

int b=3;

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

定义完a与b的值后,再令a = 4;那么,b不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

4.2对象

在Java中,创建一个对象包括对象的声明和实例化两步,下面用一个例题来说明对象的内存模型。假设有类Rectangle定义如下:

class Rectangle{

double width,height;

Rectangle(double w,double h){

width=w;

height=h;

}

}

(1) 声明对象时的内存模型

用Rectangle rect;声明一个对象rect时,将在栈内存为对象的引用变量rect分配内存空间,但Rectangle的值为空,称rect是一个空对象。空对象不能使用,因为它还没有引用任何“实体”。

(2) 对象实例化时的内存模型

当执行rect=new Rectangle(3,5);时,会做两件事:

12

在堆内存中为类的成员变量width,height分配内存,并将其初始化为各数据类型的默认值;接着进行显式初始化(类定义时的初始化值);最后调用构造方法,为成员变量赋值。

返回堆内存中对象的引用(相当于首地址)给引用变量rect,以后就可以通过rect来引用堆内存中的对象了。

1.5 创建多个不同的对象实例

一个类通过使用new运算符可以创建多个不同的对象实例,这些对象实例将在堆中被分配不同的内存空间,改变其中一个对象的状态不会影响其他对象的状态。例如:

Rectangle r1=new Rectangle(3,5);

Rectangle r2=new Rectangle(4,6);

此时,将在堆内存中分别为两个对象的成员变量width、height分配内存空间,两个对象在堆内存中占据的空间是互不相同的。如果有:

Rectangle r1=new Rectangle(3,5);

Rectangle r2=r1;

4.4 包装类

基本型别都有对应的包装类:如int对应Integer类,double对应Double类等,基本类型的定义都是直接在栈中,如果用包装类来创建对象,就和普通对象一样了。例如:int i=0;i直接存储在栈中。 Integer i(i此时是对象) = new Integer(5);这样,i对象数据存储在堆中,i的引用存储在栈中,通过栈中的引用来操作对象。

4.5 String

String是一个特殊的包装类数据。可以用用以下两种方式创建:

1. String str = new String("abc");

2. String str = "abc";

第一种创建方式,和普通对象的的创建过程一样;

第二种创建方式,Java内部将此语句转化为以下几个步骤:

(1) 先定义一个名为str的对String类的对象引用变量:String str;

13

(2) 在栈中查找有没有存放值为“abc”的地址,如果没有,则开辟一个存放字面值为“abc”的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为“abc”的地址,则查找对象o,并返回o的地址。

(3) 将str指向对象o的地址。

值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用。

为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。

String str1=“abc”;

String str2=“abc”;

n(s1==s2);//true

注意,这里并不用(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。

我们再接着看以下的代码。

String str1=new String(“abc”);

String str2=“abc”;

n(str1==str2);//false

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

4.6 数组

当定义一个数组,int x[];或int []x;时,在栈内存中创建一个数组引用,通过该引用(即数组名)来引用数组。x=new int[3];将在堆内存中分配3个保存int型数据的空间,堆内存的首地址放到栈内存中,每个数组元素被初始化为0。

4.7静态变量

用static的修饰的变量和方法,实际上是指定了这些变量和方法在内存中的“固定位

14

置”-static storage,可以理解为所有实例对象共有的内存空间。static变量有点类似于C中的全局变量的概念;静态表示的是内存的共享,就是它的每一个实例都指向同一个内存地址。把static拿来,就是告诉JVM它是静态的,它的引用(含间接引用)都是指向同一个位置,在那个地方,你把它改了,它就不会变成原样,你把它清理了,它就不会回来了。

那静态变量与方法是在什么时候初始化的呢?对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。

我们常可看到类似以下的例子来说明这个问题:

class Student {

static int numberOfStudents=0;

Student() {

numberOfStudents++;

}

}

每一次创建一个新的Student实例时,成员numberOfStudents都会不断的递增,并且所有的Student实例都访问同一个 numberOfStudents变量,实际上int numberOfStudents变量在内存中只存储在一个位置上。

15

1.5 Java的内存管理实例

Java程序的多个部分(方法,变量,对象)驻留在内存中以下两个位置:即堆和栈,现在我们只关心3类事物:实例变量,局部变量和对象:

实例变量和对象驻留在堆上

局部变量驻留在栈上

让我们查看一个java程序,看看他的各部分如何创建并且映射到栈和堆中:

public class Dog {

Collar c;

String name;

//1. main()方法位于栈上

public static void main(String[] args) {

//2. 在栈上创建引用变量d,但Dog对象尚未存在

Dog d;

//3. 创建新的Dog对象,并将其赋予d引用变量

d = new Dog();

//4. 将引用变量的一个副本传递给go()方法

(d);

}

//5. 将go()方法置于栈上,并将dog参数作为局部变量

void go(Dog dog){

//6. 在堆上创建新的Collar对象,并将其赋予Dog的实例变量

c =new Collar();

}

//7.将setName()添加到栈上,并将dogName参数作为其局部变量

void setName(String dogName){

//8. name的实例对象也引用String对象

name=dogName;

}

//9. 程序执行完成后,setName()将会完成并从栈中清除,此时,局部变量dogName也会

16

消失,尽管它所引用的String仍在堆上

}

1.6垃圾回收机制

(问题一:什么叫垃圾回收机制?) 垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露。

(问题二:java的垃圾回收有什么特点?) JAVA语言不允许程序员直接控制内存空间的使用。内存空间的分配和回收都是由JRE负责在后台自动进行的,尤其是无用内存空间的回收操作(garbagecollection,也称垃圾回收),只能由运行环境提供的一个超级线程进行监测和控制。

(问题三:垃圾回收器什么时候会运行?) 一般是在CPU空闲或空间不足时自动进行垃圾回收,而程序员无法精确控制垃圾回收的时机和顺序等。

(问题四:什么样的对象符合垃圾回收条件?) 当没有任何获得线程能访问一个对象时,该对象就符合垃圾回收条件。

(问题五:垃圾回收器是怎样工作的?) 垃圾回收器如发现一个对象不能被任何活线程访问时,他将认为该对象符合删除条件,就将其加入回收队列,但不是立即销毁对象,何时销毁并释放内存是无法预知的。垃圾回收不能强制执行,然而Java提供了一些方法(如:()方法),允许你请求JVM执行垃圾回收,而不是要求,虚拟机会尽其所能满足请求,但是不能保证JVM从内存中删除所有不用的对象。

(问题六:一个java程序能够耗尽内存吗?) 可以。垃圾收集系统尝试在对象不被使

17

用时把他们从内存中删除。然而,如果保持太多活的对象,系统则可能会耗尽内存。垃圾回收器不能保证有足够的内存,只能保证可用内存尽可能的得到高效的管理。

(问题七:如何显示的使对象符合垃圾回收条件?)

(1) 空引用 :当对象没有对他可到达引用时,他就符合垃圾回收的条件。也就是说如果没有对他的引用,删除对象的引用就可以达到目的,因此我们可以把引用变量设置为null,来符合垃圾回收的条件。

Java代码

StringBuffer sb = new StringBuffer("hello");

n(sb);

sb=null;

(2) 重新为引用变量赋值:可以通过设置引用变量引用另一个对象来解除该引用变量与一个对象间的引用关系。

Java代码

StringBuffer sb1 = new StringBuffer("hello");

StringBuffer sb2 = new StringBuffer("goodbye");

n(sb1);

sb1=sb2;//此时"hello"符合回收条件

(3) 方法内创建的对象:所创建的局部变量仅在该方法的作用期间内存在。一旦该方法返回,在这个方法内创建的对象就符合垃圾收集条件。有一种明显的例外情况,就是方法的返回对象。

Java代码

public static void main(String[] args) {

Date d = getDate();

n("d = " + d);

}

private static Date getDate() {

Date d2 = new Date();

StringBuffer now = new StringBuffer(ng());

n(now);

return d2;

18

}

(4) 隔离引用:这种情况中,被回收的对象仍具有引用,这种情况称作隔离岛。若存在这两个实例,他们互相引用,并且这两个对象的所有其他引用都删除,其他任何线程无法访问这两个对象中的任意一个。也可以符合垃圾回收条件。

Java代码

public class Island {

Island i;

public static void main(String[] args) {

Island i2 = new Island();

Island i3 = new Island();

Island i4 = new Island();

i2.i=i3;

;

i2=null;

i3=null;

i4=null;

}

}

(问题八:垃圾收集前进行清理------finalize()方法) java提供了一种机制,使你能够在对象刚要被垃圾回收之前运行一些代码。这段代码位于名为finalize()的方法内,所有类从Object类继承这个方法。由于不能保证垃圾回收器会删除某个对象。因此放在finalize()中的代码无法保证运行。因此建议不要重写finalize();

1.7 final问题

final使得被修饰的变量“不变”,但是由于对象型变量的本质是“引用”,使得“不变”也有了两种含义:引用本身的不变?,和引用指向的对象不变?

引用本身的不变:

Java代码

19

final StringBuffer a=new StringBuffer("immutable");

final StringBuffer b=new StringBuffer("not immutable");

a=b;//编译期错误

引用指向的对象不变:

Java代码

final StringBuffer a=new StringBuffer("immutable");

(" broken!"); //编译通过

可见,final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。

在举一个例子:

Java代码

public class Name {

private String firstname;

private String lastname;

public String getFirstname() {

return firstname;

}

public void setFirstname(String firstname) {

ame = firstname;

}

public String getLastname() {

return lastname;

}

public void setLastname(String lastname) {

me = lastname;

}

}

编写测试方法:

20

Java代码

public static void main(String[] args) {

final Name name = new Name();

stname("JIM");

tname("Green");

n(stname()+" "+tname());

}

理解final问题有很重要的含义。许多程序漏洞都基于此----final只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它“永远不变”。其实那是徒劳的。

Final还有一个值得注意的地方:

先看以下示例程序:

Java代码

class Something {

final int i;

public void doSomething() {

n("i = " + i);

}

}

对于类变量,Java虚拟机会自动进行初始化。如果给出了初始值,则初始化为该初始值。如果没有给出,则把它初始化为该类型变量的默认初始值。但是对于用final修饰的类变量,虚拟机不会为其赋予初值,必须在constructor (构造器)结束之前被赋予一个明确的值。可以修改为"final int i = 0;"。

1.8如何把程序写得更健壮

1、尽早释放无用对象的引用。 好的办法是使用临时变量的时候,让引用变量在退出活动域后,自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄露。对于仍然

21

有指针指向的实例,jvm就不会回收该资源,因为垃圾回收会将值为null的对象作为垃圾,提高GC回收机制效率;

2、定义字符串应该尽量使用 String str=“hello”; 的形式 ,避免使用String str = new

String(“hello”); 的形式。因为要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化,把它设置为初始值,应当这样做:

Java代码

public class Demo {

private String s;

...

public Demo {

s = "Initial Value";

}

...

}

而非

Java代码

s = new String("Initial Value");

后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。

3、我们的程序里不可避免大量使用字符串处理,避免使用String,应大量使用StringBuffer ,因为String被设计成不可变(immutable)类,所以它的所有对象都是不可变对象,请看下列代码;

Java代码

String s = "Hello";

s = s + " world!";

在这段代码中,s原先指向一个String对象,内容是 "Hello",然后我们对s进行了+操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,s不指向原来那个对

22

象了,而指向了另一个 String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。

通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的内存开销。因为 String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用StringBuffer类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。

4、尽量少用静态变量 ,因为静态变量是全局的,GC不会回收的;

5、尽量避免在类的构造函数里创建、初始化大量的对象 ,防止在调用其自身类的构造器时造成不必要的内存资源浪费,尤其是大对象,JVM会突然需要大量内存,这时必然会触发GC优化系统内存环境;显示的声明数组空间,而且申请数量还极大。

以下是初始化不同类型的对象需要消耗的时间:

运算操作

本地赋值

实例赋值

方法调用

新建对象

示例

i = n

this.i = n

Funct()

New Object()

标准化时间

1.0

1.2

5.9

980

3100

新建数组

New int[10]

从表1可以看出,新建一个对象需要980个单位的时间,是本地赋值时间的980倍,是方法调用时间的166倍,而若新建一个数组所花费的时间就更多了。这是一个案例想定供大家警戒 。使用jspsmartUpload作文件上传,运行过程中经常出现emoryError的错误,检查之后发现问题:组件里的代码

m_totalBytes = m_tentLength();

m_binArray = new byte[m_totalBytes];

问题原因是totalBytes这个变量得到的数极大,导致该数组分配了很多内存空间,而且该数组不能及时释放。解决办法只能换一种更合适的办法,至少是不会引发outofMemoryError的方式解决。

6、尽量在合适的场景下使用对象池技术以提高系统性能,缩减缩减开销,但是要注意对

23

象池的尺寸不宜过大,及时清除无效对象释放内存资源,综合考虑应用运行环境的内存资源限制,避免过高估计运行环境所提供内存资源的数量。

7、大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理 ,然后解决一块释放一块的策略。

8、不要在经常调用的方法中创建对象 ,尤其是忌讳在循环中创建对象。可以适当的使用hashtable,vector 创建一组对象容器,然后从容器中去取那些对象,而不用每次new之后又丢弃。

9、一般都是发生在开启大型文件或跟数据库一次拿了太多的数据,造成 Out Of Memory

Error 的状况,这时就大概要计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。

10、尽量少用finalize函数,因为finalize()会加大GC的工作量,而GC相当于耗费系统的计算能力。

11、尽量避免强制系统做垃圾内存的回收();

12、要过滥使用哈希表 ,有一定开发经验的开发人员经常会使用hash表(hash表在JDK中的一个实现就是HashMap)来缓存一些数据,从而提高系统的运行速度。比如使用HashMap缓存一些物料信息、人员信息等基础资料,这在提高系统速度的同时也加大了系统的内存占用,特别是当缓存的资料比较多的时候。其实我们可以使用操作系统中的缓存的概念来解决这个问题,也就是给被缓存的分配一个一定大小的缓存容器,按照一定的算法淘汰不需要继续缓存的对象,这样一方面会因为进行了对象缓存而提高了系统的运行效率,同时由于缓存容器不是无限制扩大,从而也减少了系统的内存占用。现在有很多开源的缓存实现项目,比如ehcache、oscache等,这些项目都实现了FIFO、MRU等常见的缓存算法。

2关于多态的学习

以前学java时,知道多态有这么一个定义,“一种类型,多种状态”,但是对多态的理解很模糊,只是知道这样写List list = new ArrayList(); (前面是接口与抽象类,后面是一个实现类)就是一个多态。近期做了几道SCJP的题,翻阅了相关的书籍,对其有了进一步的理解。

先看多态的一个例子:

Java代码

class Animal {

24

public void eat(){

n("eat method in Animal");

}

}

class Horse extends Animal{

public void eat(){

n("eat method in Horse");

}

public void buck(){}

}

public class TestAnimal{

public static void main(String[] args) {

Animal b = new Horse();

();

}

}

class Animal {

public void eat(){

n("eat method in Animal");

}

}

class Horse extends Animal{

public void eat(){

n("eat method in Horse");

}

public void buck(){}

}

public class TestAnimal{

public static void main(String[] args) {

Animal b = new Horse();

25

}

}

();

运行结果为:

Java代码

eat method in Animal

eat method in Horse

eat method in Animal

eat method in Horse

从测试结果可以得知()调用的是Horse中的eat方法,我们改变一下测试方法:

Java代码

Animal b =new Horse();

();

Animal b =new Horse();

();

执行这个测试会发现不能编译,而且编译器只允许调用Animal类中的方法。这时会出现一个问题: Animal b =new Horse(); 这个语句执行完后变量b到底是什么类型呢,是Animal类型,还是Horse类型。如果是Animal类型,为什么调用的却是Horse的方法。刚接触面向对象编程时,我一直以为b是Horse,最近才发现是错的。看看下面的代码,应该可以说明这个问题:

Java代码

class Animals{

}

class Horses extends Animals{

}

public class UseAnimals {

public void doStuff(Animals a){

26

n(" In the Animals version");

}

public void doStuff(Horses h){

n(" In the Horses version");

}

public static void main(String[] args) {

UseAnimals ua = new UseAnimals();

Animals animalsObject = new Animals();

Horses horseObject = new Horses();

f(animalsObject);

f(horseObject);

}

}

class Animals{

}

class Horses extends Animals{

}

public class UseAnimals {

public void doStuff(Animals a){

n(" In the Animals version");

}

public void doStuff(Horses h){

n(" In the Horses version");

}

public static void main(String[] args) {

UseAnimals ua = new UseAnimals();

Animals animalsObject = new Animals();

Horses horseObject = new Horses();

27

}

}

f(animalsObject);

f(horseObject);

输出结果应该能如你所料:

Java代码

In the Animals version

In the Horses version

In the Animals version

In the Horses version

但是如果使用对Horses对象的Animals引用,情况会如何?

Java代码

Animals animalRefToHorse=new Horses();

f(animalRefToHorse);

Animals animalRefToHorse=new Horses();

f(animalRefToHorse);

调用的是哪一个重载的版本?你可能说会调用Horses这个版本,因为在运行时传递给该方法的是Horse对象。但事实并非如此,上面的代码实际会输出:

Java代码

In the Animals version

In the Animals version

弄明白这个程序首先要明白一个问题,用方法doStuff()传递的不是一个对象,而是持有该对象的一个引用的一个副本。因此在这个程序中说明持有new Horses()这个对象的引用是一个Animals类型,所以那个animalRefToHorse它应该是一个Animals类型的。

同理:在第一个程序中的问题就可以得知,在第一个程序中的那个b是Animal类型的,但是为什么在前一个程序中();方法调用的不是Animal的方法,而是Horse的方法呢。如果你了解下面这个原理就可以明白这个问题了。

从内存的角度去考虑, b其实就是一个Animal类型引用变量,它存放在栈中,因为是一个变量,所以他可以引用放在堆中的对象。然而一个引用变量有如下的特性:引用变量可以引用具有与所声明引用相同类型的对象,更重要的一点----他可以引用所声明类型的任何

28

子类型!!!(这一点可以说是java能够实现多态的重要支持)

因此我们可以这样理解下面这个语句:

Animal b = new Horse(); 在栈中声明了一个Animal类型的引用变量b,在堆中创建一个Animal子类Horse类型的对象,b引用了这个对象。

(); 在运行过程中JVM发现b引用的实际对象是Horse对象,因此调用的是Horse中的方法。有一点需要记住,当使用指向Animal的引用时,编译器将只允许调用Animal类中的方法,因此 ()这个语句就不能通过编译了。

3重载与重写的总结

方法的重写和重载并不难,但是有些地方还是值得注意一下,特别是下文提到的重写规则的第二条和重载规则的第一条,在java认证考试和一些面试题中经常会考到。

方法的重写规则:

重写方法的规则如下:

1. 参数列表:必须与被重写方法的参数列表完全匹配。

2. 返回类型:必须与超类中被重写的方法中声明的返回类型或子类型完全相同

3. 访问级别:一定不能比被重写方法强,可以比被重写方法的弱。

4. 非检查异常:重写方法可以抛出任何非检查的异常,无论被重写方法是否声明了该异常。

5. 检查异常:重写方法一定不能抛出新的检查异常,或比被重写方法声明的检查异常更广的检查异常

6. 不能重写标志为final,static的方法

重载方法的规则:

1. 参数列表:被重载的方法必须改变参数列表。

2. 返回类型:可以改变返回类型。

3. 修饰符:可以改变修饰符

4. 异常:可以声明新的或者更广泛的异常。

通过实例充分理解重写与重载:

Java代码

class Animal {

public void eat(){

29

}

}

以下列出对于Animal的eat方法各种重写重载实例,根据以上列出的规则,判断其是否合法(以下的方法都属于Animal的子类Horse的方法):

1. private void eat(){} 不能通过编译,非法重写,访问修饰符限制性变强;不属于重载,因为参数列表没有发生改变

2. public void eat() throw RuntimeException{} 能通过编译,属于重写,重写方法可以抛出任何非检查异常。

3. public void eat() throw IOException{} 不能通过编译,非法重写,重写方法一定不能抛出新的检查异常,或比被重写方法声明的检查异常更广的检查异常;不属于重载,参数列表必须发生变化才属于重载

4. public void eat(String food){} 能通过编译,不是重写;合法重载,因为参数列表发生改变了。

5. public String eat(){} 不能通过编译,不是重写,因为其返回类型;不是重载,因为参数列表没有发生改变。

6. public String eat(int n){} 可以通过编译,不是重写;是重载,首先参数列表必须发生变化,返回类型可以发生改变。

对父类被重写的方法做一个变动:

Java代码

class Animal {

public Animal eat() throws IOException{

return null;

}

}

7. public Animal eat(){return null;} 可以通过编译,合法重写,可以不抛出异常,只要不抛出新的异常或更广泛的异常就可以。

8. public Animal eat() throws FileNotFoundException{return null;} 可以通过编译,是重写,属于子类的非检查异常。

9. public Animal eat() throws Exception{} 不可以通过编译,非法重写,抛出了更广泛的异常。

30

10. public Horse eat() {} 可以通过编译,是重写,因为返回类型可以是被重写的返回类型的子类。

4 Java线程知识深入解析(1)

一般来说,我们把正在计算机中执行的程序叫做"进程"(Process) ,而不将其称为程序(Program)。所谓"线程"(Thread),是"进程"中某个单一顺序的控制流。新兴的操作系统,如Mac,Windows NT,Windows 95等,大多采用多线程的概念,把线

程视为基本执行单位。线程也是Java中的相当重要的组成部分之一。

甚至最简单的Applet也是由多个线程来完成的。在Java中,任何一个Applet的paint()和update()方法都是由AWT(Abstract Window Toolkit)绘图与事件处理线程调用的,而Applet 主要的里程碑方法——init(),start(),stop()和destory() ——是由执行该Applet的应用调用的。

单线程的概念没有什么新的地方,真正有趣的是在一个程序中同时使用多个线程来完成不同的任务。某些地方用轻量进程(Lightweig ht Process)来代替线程,线程与真正进程的相似性在于它们都是单一顺序控制流。然而线程被认为轻量是由于它运行于整个程序的上下文内,能使用整个程序共有的资源和程序环境。

作为单一顺序控制流,在运行的程序内线程必须拥有一些资源作为必要的开销。例如,必须有执行堆栈和程序计数器在线程内执行的代码只在它的上下文中起作用,因此某些地方用"执行上下文"来代替"线程"。

4.1 线程属性

为了正确有效地使用线程,必须理解线程的各个方面并了解Java 实时系统。必须知道如何提供线程体、线程的生命周期、实时系统如何调度线程、线程组、什么是幽灵线程(Demo nThread)。

31

4.1.1 线程体

所有的操作都发生在线程体中,在Java中线程体是从Thread类继承的run()方法,或实现Runnable接口的类中的run()方法。当线程产生并初始化后,实时系统调用它的run()方法。run()方法内的代码实现所产生线程的行为,它是线程的主要部

分。

4.1.2 线程状态

附图表示了线程在它的生命周期内的任何时刻所能处的状态以及引起状态改变的方法。这图并不是完整的有限状态图,但基本概括了线程中比较感兴趣和普遍的方面。以下讨论有关线程生命周期以此为据。

●新线程态(New Thread)

产生一个Thread对象就生成一个新线程。当线程处于"新线程"状态时,仅仅是一个空线程对象,它还没有分配到系统资源。因此只能启动或终止它。任何其他操作都会引发异常。

●可运行态(Runnable)

start()方法产生运行线程所必须的资源,调度线程执行,并且调用线程的run ()方法。在这时线程处于可运行态。该状态不称为运行态是因为这时的线程并不总是一直占用处理机。特别是对于只有一个处理机的PC而言,任何时刻只能有一个处于可运行态的线程占用处理 机。Java通过调度来实现多线程对处理机的共享。

●非运行态(Not Runnable)

当以下事件发生时,线程进入非运行态。

①suspend()方法被调用;

②sleep()方法被调用;

③线程使用wait()来等待条件变量;

④线程处于I/O等待

●死亡态(Dead)

32

当run()方法返回,或别的线程调用stop()方法,线程进入死亡态 。通常Applet使用它的stop()方法来终止它产生的所有线程。

4.1.3 线程优先级

虽然我们说线程是并发运行的。然而事实常常并非如此。正如前面谈到的,当系统中只有一个CPU时,以某种顺序在单CPU情况下执行多线程被称为调度(scheduling)。Java采用的是一种简单、固定的调度法,即固定优先级调度。这种算法是根据处于可运行态线程的相对优先级来实行调度。当线程产生时,它继承原线程的优先级。在需要时可对优先级进行修改。在任何时刻,如果有多条线程等待运行, 系统选择优先级最高的可运行线程运行。只有当它停止、自动放弃、或由于某种原因成为非运行态低优先级的线程才能运行。如果两个线程具有相同的优先级,它们将被交替地运行。

Java实时系统的线程调度算法还是强制性的,在任何时刻,如果一个比其他线程优先级都高的线程的状态变为可运行态,实时系统将选择该线程来运行。

4.1.4 幽灵线程

任何一个Java线程都能成为幽灵线程。它是作为运行于同一个进程内的对象和线程的服务提供者。例如,HotJava浏览器有一个称为"后台图片阅读器"的幽灵线程,它为需要图片的对象和线程从文件系统或网络读入图片。幽灵线程是应用中典型的独立线程。它为同一应用中的其他对象和线程提供服务。幽灵线程的run()方法一般都是无限循环,等待服务请求。

4.1.5 线程组

每个Java线程都是某个线程组的成员。线程组提供一种机制,使得多个线程集于一个对象内,能对它们实行整体操作。譬如,你能用一个方法调用来启动或挂起组内的所有线程。Java线程组由ThreadGroup类实现。当线程产生时,可以指定线程组或由实时系统将其放入某个缺省的线程组内。线程只能属于一个线程组,并且当线程产生后不能改变它所属的线程组。

33

4.2 多线程程序

对于多线程的好处这就不多说了。但是,它同样也带来了某些新的麻烦。只要在设计程序时特别小心留意,克服这些麻烦并不算太困难。

4.2.1 同步线程

许多线程在执行中必须考虑与其他线程之间共享数据或协调执行状态。这就

需要同步机制。在Java中每个对象都有一把锁与之对应。但Java不提供单独的lock和unlock操作。它由高层的结构隐式实现, 来保证操作的对应。(然而,我们注意到Java虚拟机提供单独的monito renter和monitorexit指令来实现lock和unlock操作。)

Synchronized(synchronized关键字就可以轻松地解决多线程共享数据同步问题)语句计算一个对象引用,试图对该对象完成锁操作, 并且在完成锁操作前停止处理。当锁操作完成synchronized语句体得到执行。当语句体执行完毕(无论正常或异常),解锁操作自动完成。作为面向对象的语言,synchronized经常与方法连用。一种比较好的办法是,如果某个变量由一个线程赋值并由别的线程引用或赋值,那么所有对该变量的访问都必须在某个synchromized语句或synchronized方法内。

现在假设一种情况:线程1与线程2都要访问某个数据区,并且要求线程1的访问先于线程2, 则这时仅用synchronized是不能解决问题的。这在Unix或Windows NT中可用Simaphore来实现。而Java并不提供。在Java中提供的是wait()和notify()机制。使用如下:

synchronized method-1(…){ call by thread 1.

∥access data area;

available=true;

notify()

}

synchronized method-2(…){∥call by thread 2.

while(!available)

34

try{

wait();∥wait for notify().

}catch (Interrupted Exception e){

}

∥access data area

}

其中available是类成员变量,置初值为false。如果在method-2中检查available为假,则调用wait()。wait()的作用是使线 程2进入非运行态,并且解锁。在这种情况下,method-1可以被线程1调用。当执行 notify()后。线程2由非运行态转变为可运行态。当method-1调用返回后。线程2 可重新对该对象加锁,加锁成功后执行wait()返回后的指令。这种机制也能适用于 其他更复杂的情况。

4.2.2 死锁

如果程序中有几个竞争资源的并发线程,那么保证均衡是很重要的。系统均衡是指每个线程在执行过程中都能充分访问有限的资源。系统中没有饿死和死锁的线程。Java并不提供对死锁的检测机制。对大多数的Java程序员来说防止死锁是一种较好的选择。最简单的防止死锁的方法是对竞争的资源引入序号,如果一个线程需要几个资源,那么它必须先得到小序号的资源,再申请大序号的资源。

所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。

产生死锁的原因主要是:

(1) 因为系统资源不足。

(2) 进程运行推进的顺序不合适。

(3) 资源分配不当等。

产生死锁的四个必要条件:

35

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁问题的解决方法

1预防死锁

2避免死锁

3检测与解除死锁

一、 预防死锁

预防: 通过设置某些限定条件,破坏导致死锁的四个必要条件之一。

“ 互斥条件”—— 由资源的性质决定。

摒弃“ 占有并请求” 条件: 运行前(创建时),一次性分配给进程它所需的“ 全部” 资源。简单易实现,安全性高;资源浪费。

二、 避免死锁

在资源的动态分配过程中,用某种方法防止系统进入不安全状态。

安全状态:现有的进程资源占有情况下,各进程按照某种推进顺序仍然可以使每个进程得到其对资源的最大需求,从而都可以顺利地完成。

避免死锁的经典算法—银行家算法详细介绍

小结

线程是Java中的重要内容,多线程是Java的一个特点。虽然Java的同步互斥不如某些系统那么丰富,但适当地使用它们也能收到满意的效果。

5 Java多线程的相关机制

5.1 线程的基本概念

线程是一个程序内部的顺序控制流.一个进程相当于一个任务,一个线程相当于一个任务中的一条执行路径.;多进程:在操作系统中能同时运行多个任务

36

(程序);多线程:在同一个应用程序中有多个顺序流同时执行;Java的线程是通过类来实现的;JVM启动时会有一个由主方法(public static void

main(){})所定义的线程;可以通过创建Thread的实例来创建新的线程;每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体,通过调用Thread类的start()方法来启动一个线程。

5.2 线程的创建和启动

可以有两种方式创建新的线程:

第一种:

1.定义线程类实现Runnable接口

myThread = new Thread(target); //target为Runnable接口类型

le中只有一个方法:public void run();用以定义线程运行体

4.使用Runnable接口可以为多个线程提供共享的数据

5.在实现Runnable接口的类的run()方法定义中可以使用Thread的静态方法public static Thread currentThread();获取当前线程的引用

第二种:

1.可以定义一个Thread的子类并重写其run方法如:

class MyThread extends Thread {

public void run() {...}

}

2.然后生成该类的对象:

MyThread myThread = new MyThread();

5.3 线程控制的基本方法

isAlive():判断线程是否还"活"着

getPriority():获得线程的优先级数值

setPriority():设置线程的优先级数值

37

():将当前线程睡眠指定毫秒数

join():调用某线程的该方法,将当前线程与该线程"合并",即等待该线程结束,再恢复当前线程的运行

yield():让出cpu,当前线程进入就绪队列等待调度

wait():当前线程进入对象的wait pool

notify()/notifyAll():唤醒对象的wait pool中的一个/所有等待线程

5.4 线程编程方面

60、java中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?

答:有两种实现方法,分别是继承Thread类与实现Runnable接口

用synchronized关键字修饰同步方法,反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。

61、sleep() 和 wait() 有什么区别?

答:sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

62、同步和异步有何异同,在什么情况下分别使用他们?举例说明。

答:如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读

38

到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

63、启动一个线程是用run()还是start()?

答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。

64、当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?

答:不能,一个对象的一个synchronized方法只能由一个线程访问。

65、请说出你所知道的线程同步的方法。

答:wait():使一个线程处于等待状态,并且释放所持有的对象的lock。

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。

notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

66、多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?

答:多线程有两种实现方法,分别是继承Thread类与实现Runnable接口

同步的实现方面有两种,分别是synchronized,wait与notify

67、线程的基本概念、线程的基本状态以及状态之间的关系

答:线程指在程序执行过程中,能够执行程序代码的一个执行单位,每个程序至少都有一个线程,也就是程序本身。

Java中的线程有四种状态分别是:运行、就绪、挂起、结束

68、简述synchronized和的异同 ?

39

答:主要相同点:Lock能完成synchronized所实现的所有功能

主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。

6 hashcode方法

有许多人学了很长时间的Java,但一直不明白hashCode方法的作用,我来解释一下吧。

首先,想要明白hashCode的作用,你必须要先知道Java中的集合。

总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。你知道它们的区别吗?前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是 方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。

于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。

这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals

40

方法的次数就大大降低了,几乎只需要一两次。

所以,Java对于eqauls方法和hashCode方法是这样规定的:

1、如果两个对象相同,那么它们的hashCode值一定要相同;

2、如果两个对象的hashCode相同,它们并不一定相同

上面说的对象相同指的是用eqauls方法比较。

你当然可以不按要求去做了,但你会发现,相同的对象可以出现在Set集合中。同时,增加新元素的效率会大大下降。

List接口对Collection进行了简单的扩充,它的具体实现类常用的有

ArrayList和LinkedList。你可以将任何东西放到一个List容器中,并在需要时从中取出。ArrayList从其命名中可以看出它是一种类似数组的形式进行存储,因此它的随机访问速度极快,而LinkedList的内部实现是链表,它适合于在链表中间需要频繁进行插入和删除操作。在具体应用时可以根据需要自由选择。前面说的Iterator只能对容器进行向前遍历,而ListIterator则继承了Iterator的思想,并提供了对 List进行双向遍历的方法。

Set接口也是Collection的一种扩展,而与List不同的时,在Set中的对象元素不能重复,也就是说你不能把同样的东西两次放入同一个Set容器中。它的常用具体实现有HashSet和TreeSet类。HashSet能快速定位一个元素,但是你放到HashSet中的对象需要实现hashCode()方法,它使用了前面说过的哈希码的算法。而TreeSet则将放入其中的元素按序存放,这就要求你放入其中的对象是可排序的,这就用到了集合框架提供的另外两个实用类Comparable和Comparator。一个类是可排序的,它就应该实现Comparable接口。有时多个类具有相同的排序算法,那就不需要在每分别重复定义相同的排序算法,只要实现Comparator接口即可。集合框架中还有两个很实用的公用类:Collections和Arrays。Collections提供了对一个Collection容器进行诸如排序、复制、查找和填充等一些非常有用的方法,Arrays则是对一个数组进行类似的操作。

Map是一种把键对象和值对象进行关联的容器,而一个值对象又可以是一个Map,依次类推,这样就可形成一个多级映射。对于键对象来说,像Set一样,一个Map容器中的键对象不允许重复,这是为了保持查找结果的一致性;如果有两个键对象一样,那你想得到那个键对象所对应的值对象时就有问题了,可能你

41

得到的并不是你想的那个值对象,结果会造成混乱,所以键的唯一性很重要,也是符合集合的性质的。当然在使用过程中,某个键所对应的值对象可能会发生变化,这时会按照最后一次修改的值对象与键对应。对于值对象则没有唯一性的要求。你可以将任意多个键都映射到一个值对象上,这不会发生任何问题(不过对你的使用却可能会造成不便,你不知道你得到的到底是那一个键所对应的值对象)。Map有两种比较常用的实现:HashMap和TreeMap。HashMap也用到了哈希码的算法,以便快速查找一个键,TreeMap则是对键按序存放,因此它便有一些扩展的方法,比如firstKey(),lastKey()等,你还可以从TreeMap中指定一个范围以取得其子Map。键和值的关联很简单,用pub(Object key,Object value)方法即可将一个键与一个值对象相关联。用get(Object key)可得到与此key对象所对应的值对象。

7 JAVA的11中设计模式

一:设计模式是最重要的课程之一,堪称软件界的九阳真经,设计模式是一大套被反复使用,多数人知晓的,经过分类编目的,代码总结,使用设计模式是为了可重用代码.让代码更容易被他人理解,保证代码可靠性。

二:学习设计模式最常见的理由是因为我们可以借其:

1. 复用解决方案----避免重蹈前人的覆辙,从学习他人的经验中获益,用不着为那些总是会重复出现的问题再次设计解决方案.

2. 确定通用术语-----设计模式在项目的分析和设计阶段提供了共同的基准点.

三:设计模式中一般都遵循这们的原则:

1. 按接口编程.

2. 尽量使用组合代替继承.

3. 找出变化并封装。

7.1 工厂模式

工厂模式按照《Java与模式》中的提法分为三类: 1. 简单工厂模式(Simple

Factory) 2. 工厂方法模式(Factory Method) 3. 抽象工厂模式(Abstract Factory) 这

42

三种模式从上到下逐步抽象,并且更具一般性。所以我建议在这种情况下使用简单工厂模式与工厂方法模式相结合的方式来减少工厂类:即对于产品树上类似的种类(一般是树的叶子中互为兄弟的)使用简单工厂模式来实现。来看看抽象工厂模式的各个角色(和工厂方法的如出一辙): 抽象工厂角色:这是工厂方法模式的核心,它与应用程序无关。

工厂模式有三个参与者,抽象产品(Product)、工厂(Creator)和具体产品(ConcreteProduct)。客户只会看到工厂和抽象产品。

public interface Product{

public String getName();

}

public class ConcreteProduct implements Product{

public String getName(){

return "产品1";

}

}

public class Creator{

public static Product create1(){

return new ConcreteProduct();

}

}

工厂模式的作用在于将创建具体产品的方法由工厂类控制,客户只需要知道产品的抽象类型

7.2 单例模式

定义:一个类在java虚拟机中只能创建一个对象。

43

单例模式的构建有两种方式:

a:懒汉式:指全局的单例实例在第一次被使用时创建。

b:饿汉式:指全局的单例实例在类加载的时候创建。

单例模式必须要满足以下四个条件:

1. 单例类必须要有一个私有的构造器.

2. 单例类的实例必须为全局的,且用private static修饰.

3. 必须提供一个对外开放的创建对象的方法。

4. 对放的方法必须是用公共,静态且同步的方法.public synchronized static xxx();

用到的地方:当一个类的实例,有且只能创建一个时用到。

7.3 门面模式

定义:定义一个高层接口,把所有子类的交互,通过这个接口来实现,这个接口集成了所有子系统的类。

<书面说法:为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用>

解决问题:子接口繁多,调用复杂,内部交互地方比较多。

7.4 策略模式

定义:定义一系列的算法,将它们封装起来,使得它们可以替换,使用策略模式使得算法可

44

独立于使用它的客户而变化。

解决问题:某个具体的解决方法有很多种可选实现。

适用情形:

a:如果在一个系统里面有许多类,它们之间的区别在于它们的行为,那么使用策略模式可以动态的让一个对象在许多行为中选择一个种行为。

b:一个系统需要动态地在几种算法中选择一种,那么这些算法可以包装到一个个具体算法类里面,而这些算法都是一个抽象算法类的子类,换言之,这些具本算法类均有统一的接口,由于多态性原则,客户端可以选择使用任何一个具体类算法,并只持有一个数据类型是抽象算法类的对象.

7.5 模板模式