第8章 线程与对象串行化 Flashcards

1
Q

线程的概念

A

1、什么是线程
在多任务操作系统中,通过运行多个进程来并发地执行多个任务。而在操作系统中引入线程的概念后,由于每个线程都是一个能独立执行自身指令的不同控制流,因此一个包含多线程的进程也能够实现多项任务的并发执行。例如,一个进程中可以包含三个线程,一个线程运行GUI,第二个线程执行I/O操作,第三个线程招待执行后台计算工作。线程在80年代末才被真正引入,在提高系统效率等方面有显著作用,线程在高性能的操作系统中得到了广泛的使用,Windows、Solaris等都是支持线程的操作系统。在单处理机的计算机上,两个线程实际上并不能并发执行,但系统可以在一个极短的时间内完成的,这样给人的印象是并发执行。
线程是由表示程序运行状态的寄存器,线程不包含进程地址空间中的代码和数据,线程是计算过程在某一时刻的状态。系统在产生一个线程或在各个线程之间切换时,负担要比进程小得多,线程称为轻型进程(lightweight process)。线程是一个用户级的褓,线程结构驻留在用户空间中,能够被普通的用户级方法直接访问。
Java语言的一个重要的特性是在语言级上支持多线程的程序设计。线程可以定义为一个程序中的单个执行流。多线程是指一个程序中包含多个招待流,多线程是实现并发的一种有效手段。
多线程程序设计的含义是可以将程序任务分成几个并行的子任务。特别是在网络编程中,有很多功能是可以并发执行。HotJava浏览器就是一个多线程应用的实例。
2、Java中的线程模型
一个执行流是由CPU运行程序的代码、操纵程序的数据所形成的。线程被认为是以CPU为主体的行为。在Java中线程的模型就是一个CPU、程序代码和数据的封装体。
Java中的线程模型包含3部分:
(1)一个虚拟的CPU。
(2)该CPU执行的代码。
  (3)代码所操作的数据。
线程模型在Java中是由java.lang.Thread类进行定义和描述的。程序中的线程都是Thtead的实例。用户可以通过创建Thtead的实例或定义、创建Thread子类的实例建立和控制自己的线程。

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

线程的创建

A

线程体要通过一个对象传递给Thread类的构造方法。Java中的线程体是由线程类的run()方法定义,在该方法中定义线程的具体行为。线程开始执行时,也是从它的run()方法开始执行。
提供线程体的特定对象是在创建线程时指定的而创建线程对象是通过调用Thread类的构造方法实现的。
Thread类在Java API的java.lang包中定义,Thread类的构造方法的一般结构可以表示如下:
public Thread(ThreadGroup group,Runnable target,String name);
其中参数的含义是:
group—指明该线程所属的线程组。
target—提供线程体的对象,线程启动时,该对象的run()方法将被调用。
name—线程名称。Java中的每个线程都有自己的名称,如果name为null,则Java自动给线程赋予惟一的名称。
上述方法的每个参数都可以为null。不同的参数取null值,就成为Tread类的各种构造方法:
public Thread();
public Thread(Runnable target);
public Thread(ThreadGroup group,Runnbale target);
public Thread(String name);
public Thread(ThreadGroup group,String name);
public Thread(Runnable target,String name);
public Thread(ThreadGroup group,Runnable target,String name);
任何实现Runnable接口的对象都可以作为Thread类构造方法中target参数,而Thread类本身也实现了Runnable接口,因此可以有两种方式提供run()方法的实现:实现Runnable接口的继承Thread类。
(1)通过实现Runnable接口创建线程
在java.lang中Runnable接口的定义为:
public interface Runnable{
void run();
}
当实现Runnable接口的类的对象用来创建线程以后,该线程的启动将使得对象的run()方法被调用。通过这种方式创建线程的过程是:Runnable的一个实例作为参数传递给Thread类的一个构造方法,该实例对象提供线程体run()。
因此,一个线程是Thread类的一个实例。线程人一个传递给线程的Runnable实例的run()方法开始执行。线程所操作的数据是来自于该Runnable类的实例。另外,新建的线程不会自动运行,必须调用线程的strart()方法。
(2)通过继承Thread类创建线程
Thread类本身实现了Runnable接口,所以在java.lang的Thread类的定义中可以发现run()方法。因此,可以通过继承Thread类,并重写其中的run()方法定义线程体,然后创建该子类的对象创建线程。
(3)两种方法的比较
线程创建的两种方法各有自己的特点:
①采用继承Thread类方法的优点,这种方法中程序代码简单,并可以直接调用线程的方法。
②实现Runnable接口的优势,符合面向对象设计的思想。

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

线程的调度与线程控制

A

1、线程优先级与线程调度策略
Java中的线程是有优先级的。Thread类有3个有关线程优先级的静态常量:MIN_PRIORITY,MAX_PRIORITY,NORM_PRIOTY。其中MIN_PRIORITY代表最小优先级,通常为1;MAN_PRIORITY代表最高优先级,通常为10;NORM_PRIORITY代表普通优先级,默认值为5。线程的优先级是MIN_PRIORITY与MAX_PRIORITY之间的一个值,并且数值越大优先级越高。
新建线程将继承创建它的父线程。一般情况下,主线程具有普通优先级。这两个方法的定义为:
public final int getPriority();
public final void setPriority(int newPriority);
目前计算机多数是单个CPU,所以一个时刻只能运行一个线程。在单个CPU上以某种顺序运行多个线程,称为线程的调度。Java的线程调度策略是一种基于优先级的抢先式调度。其含义是:Java基于线程优先级选择高优先级的线程进行运行。
2、线程的基本控制
Thread类提供了如下基本线程控制方法:
(1)sleep() (使比其低的优先级线程运行)
该方法使一个线程暂停运行一段固定的时间。在休眠时间内,线程将不运行。由于线程的调度是按照线程的优先级的高低顺序进行的。当高优先级的线程不结束时,低优先级的线程将没有机获得CPU。有时高优先级的线程需要与低优先级的线程进行同步或需要完成一些费时的操作,则高优先级线程将让出CPU,使伟先级低的线程有机会运行。高优先级线程可以在它的run()方法中调用sleep()方法来使自己退出CPU,休眠一段时间,休眠时间的长短由sleep()方法的参数决定。sleep()方法的格式是:
static void sleep(int millsecond),休眠时间以毫秒为单位。
static void sleep(int millsecond,int nanosecond),休眠时间是指定的毫秒数与纳秒数之和。
(2)yiele() (只让给同优先运行)
调用该方法后,可以使具有与当前线程相同优先级的线程有运行的机会。如果有其他的线程与当前线程具有相同优先级并且是可运行的,该方法将把调用yield()方法的线程放入可运行线程池,并允许其他线程运行。如果没有同等优先级的线程是可运行状态,yield()方法将什么也不做,即该线程将继续运行。
(3)join()
t.join()方法使当前的线程等待,直到t结束为止,线程恢复到可运行状态。有3种调用格式:
①join()。
②join(long millis)。
③join(long millis,int nanos)。
(4)interrupt()(5)currentThread()
(6)isAlive()
(7)stop()
(8)suspend()与resume()

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

线程同步

A
1、多线程并发操作中的问题
在Java中引入线程的目的是为了支持多线程程序设计。在多线程的程序中,当多个线程并发执行时,虽然各个线程中语句的执行顺序是确定的,但线程的相对执行顺序是不确定的。有些情况下,这种因多线程并发执行而引起的执行顺序的不确定性是无害的,不影响程序运行的结果。但在有些情况下如多线程对共享数据操作时,这种线程运行顺序的不确定性将会产生执行结果的不确定性,使共享数据的一致性被破坏,因此在某些应用程序中必须对线程的并发操作进行控制。
2、对象的加锁及其操作
Java中对共享数据操作的并发控制是采用传统的封锁技术。
(1)对象加锁及其操作
一个程序中单独的、并发的线程对同一个对象进行访问的代码段,称为临界区(Critical Sections)。在Java语言中,临界区可以是一个语句块或是一个方法,并且用“synchronized”关键字标识。Java平台将每个由synchronized(Object)语句指定的对象设置一个锁,称为对象锁(monitor)。Java中的对象锁是一种独占的排他锁(exclusive locks)。其含义是,当一个线程获得了对象的锁后,便拥有该对象的操作权,其他任何线程不能对该对象进行任何操作。
(2)对象加锁的注意事项
对于对象加锁的使用有如下几点说明:
①返还对象的锁。对象的锁在如下几种情况下由持有线程返还:
●当synchronized()语句块执行完后。
●当在synchronized()语句块中出现例外(cxception)。
●当持有锁的线程调用该对象的wait()方法。
②共享数据的所有访问都必须作为临界区,使用synchized进行加锁控制。
③用synchronized保护的共享数据必须是私有的。④Java中对象加锁具有可重入性。
3、死锁的防治
如果程序中多个线程互相等待对方的持有的锁,而在得到对方锁之前都不会释放自己的锁,这就造成了都想得到资源而又都得不到,线程不能继续运行,这就是死锁。
Java完全由程序进行控制,防止死锁的了生。
4、线程间的交互wait()和notify()
有时,某个线程进入synchronized块后,共享数据的状态并不一定满足该线程的需要,它要等待其他线程将共享数据改变为它需要的状态后才能继续执行,但由于此时它占有了该对旬的锁,其他线程无法对共享数据进行操作,Java引入wait()和notify(),这两个方法是java.lang.object类的方法,是实现线程通信的两个方法。
如果线程调用了某个对象X的wait()方法X.wait(),则该线程将放入X的wait pool,并且该线程将释放X的锁;当线程调用X的notify()方法X.notify()时,则对象的wait pool中的一个线程将移入lock pool,在lock pool中线程将等待X的锁,一暗无天日获得便可运行。NotifyAll()把对象wait pool中的所有线程都移入lock pool,加此用wait()和notify()可以实现线程的同步:当某线程需要在synchronized块中等待共享数据状态改变时调用wait()方法,这样该线程等待并暂时释放共享数据对象的锁,其他线程可以获得该对象的锁,并进入synchronized块共享数据进行操作。当其操作完后,只要调用notify()方法就可以通知正在等待的线程重新占有锁,并运行。系统中使用某类资源的线程一般称为消费者,产生或释放同类资源的线程称为生产者。
5、不建议使用的一些方法
在线程的同步过程中,如下方法是不建议使用的:
(1)stop()
stop()强行终止线程的运行,容易造成数据的不一致。
(2)suspend()和resume()
这两种方法使得一个进程可以直接控制另外一个进程的执行,容易造成死锁。
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

线程状态与生命周期

A

线程创建后,就开始了它的生命周期。在不同的生命阶段线程有不同的状态。对线程调用各种控制方法,就使线程从一种状转换为另一种状态。线程的生命周期主要分为如下几个状态:
(1)新建状态(new)
(2)可运行状态(Runable)
(3)运行状态(Running)

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

线程相关的其他类与方法

A

1、支持线程的类
在Java中,有如下支持线程的类:
(1)java.lang.Thread
在Java中,线程对象是由java.lang包中的Thread类导出的。Thread类定义并实现了Java中的线程。可以通过派生Thread类的子类定义用户自己的线程,也可以使用Runnable接口。
(2)java.lang.Runnable
Java中定义了Runnable接口,目的是使任何类都可以为线程提供线程体,即run()方法。
(3)java.lang.Object
Object是Java中的根类,它定义了线程同步与交互的方法:wait()、notify()以及notifyAll()。
(4)java.lang.ThreadGroup
Java应用程序中,所有的线程都属于一个线程组,线程组中的线程一般是相关的。java.lang包中的ThreadGroup类实现了线程组,并提供了对线程组或组中的每一个线程进行操作的方法。
(5)java.lang.ThreadDeath
一般用于杀死线程。
2、线程组
Java中每个线程都属于某个线程组。线程组使一组线程可以作为一个对象进行统一处理或维护。一个线程只能在创建时设置其所属的线程组,在线程创建后就不允许将线程从一个线程组移到另一个线程组。
线程组是由java.lang包中的ThreadGroup类实现的。在创建线程时可以显式地指定线程组,此时需要从如下不为3种线程构造方法中选择一种:
①public Thread(ThreadGroup group,Runnable target)。
②public Thread(ThreadGroup group,String name)。
③Public Thread(ThreadGroup group,Runnable target,String name)。
若在线程创建时并没有显式指定线程组,则新创建的线程自动属于父线程所在的线程组。在Java应用程序启动时,Java运行系统为该应用程序创建了一个称为main的线程组。如果以后创建的线程没有指定线程组,则这些线程都将属于main线程组。
程序中可以利用ThreadGroup类显式创建线程组,并将新创建的线程放入该线程组。
通过Thread类的getThreadGroup方法,可以获得线程所属的线程组。
ThreadGroup类对Java应用程序中的线程组进行管理。一个线程组可以包含任意数目的线程。一个线程组内不仅可以包含线程,还可以包含其他线程组。在Java应用程序中,最顶层线程组是main。在main中可以创建线程或线程组,并且可以在main的线程组中进一步创建线程组。因此Java应用程序中,形成了以main为根的线程与线程的树形结构。
Thtead类的其他方法
(1)setName()方法
public final void setName(String name),把线程的名字改为name。
(2)getNameUI方法
public final String getName(),返回当前线程的线程组中活动线程的个数。
(3)activeCount()方法
public static int activeCount(),返回当前线程的线程组中活动线程的个数。(4)getThreadGroup()方法
public final ThreadGroup getThreadGroup(),返回当前线程所属的线程组名。已经终止的线程返回null。
(5)setDaemon()方法
public final void setDaemon(boolean on),设置当前线程为Daemon线程,该方法必须在线程启动前调用。Daemon有时可称为服务线程,通常以比较低的优先级运行,它为同一个应用程序中的其他线程提供服务。
(6)isDaemon()方法
public final boolean isDaemon(),测试线程是否为Daemon线程,若是返回true;若不是返回false。
(7)toString()方法
public String toString(),返回线程的字符串信息,包括线程的名字、优先级和线程组。
(8)enumerate()方法
public static int enumerate(Thread[ ]tarray),把当前线程的线程组中的活动线程拷贝到tarray线程数组中,包括它们的子线程。
(9)ckeckAccess()方法
public final void checkAccess(),确定当前线程是否允许修改线程。用线程作参数时,如果该方法有安全管理则引起SecurityException异常。

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

对象的串行化

A

1、串行化概念和目的
将Java程序中的对象保存在外存中,称为对象永久化。Java中定义了两种类型的字节流ObjectInputStream和ObjectOutputStream支持对象的读和写,一般将这两种称为对象流。对象永久化的关键是将它的状态以一种串行格式表示出来,以便以后读该对象时能够把它重构出来。因此对Java对象的读、写的过程被称的对象串行化(Object serialization)。对象的串行化在下列情况下使用:
(1)Java远程方法调用RMI(Remote Method Invocation)
通过Socket进行对象间的通信。在这种远程对象的互操作中,有时需要传输对象。
(2)对象永久化
保存程序中的对象,以便在该程序的日后运行中使用。
对象串行化技术包括的内容有:如何使用ObjectInputStream类和ObjectOutputStream类实现对象的串行化;如何构造一个类使其对象可被串行化。
2、串行化对象的方法
(1)把对象写到对象输出流
把一个对象写到一个流中相对比较简单。具体是通过调用ObjectOutputStream类的writeObject()方法实现的。该方法的定义如下:
public final void writeObject(Object obj)throws IOException
ObjectOutputStream类是一种过滤流类,因此,对象流必须在其他流的基础上进行构造。
objectOutputStream类的writeObject()方法串行化指定的对象,并且遍历要串行化对象所引用的其他对象,通过递归方法把所有引用的对象在输出流中表示出来。在输出流中,要串行化的对象对某个对象的第一次引用会使该对象串行化到输出流中,并被赋予一个句柄,在对该对象的后续引用中将使用该句柄表示这个对象。这样,在对象串行化的过程中会完整保存对象之间的引用关系。
对象输出流类ObjectOutputStream实现了java.io.DataOutput接口。Dataoutput接口中除了基本write(byte[ ]b)等方法之外,还定义了许多写基本数据类型的方法。ObjectOutputStream类提供了如下写对象与基本数据类型的方法,如:
①writeObject(Object obj),写一个对象。
②write(byte[ ]buf),写一个字节数组。
③write(byte[ ]buf,int off,int len),写一个字节数组的一部分。
④write(int val),写一个字节。
⑤writeBoolean(Boolean val),写一个布尔值。
⑥writeByte(int val),写一个8位字节。
⑦writeChar(int val),写一个16位字节。
⑧writeChars(String str),写一个16位字符串。
⑨writeDouble(double val),写一个64位double型数值。
⑩writeFloat(flaot val),写一个32位float型数值。
writeInt(int val),写一个32位int型数值。
writeLong(long val),写一个64位long型数值。
writeShort(int val),写一个16位short型数值。
writeUTF(String str),采用UTF格式写一个字符串。
writeObject()方法在指定的对象不可串行化时,将抛出NotStrializableException类型的异常。任何一个对象只有它所对应的类实现了Serializable接口时,才是可串行化的。
(2)从对象输入流读取对象
当已经把对象或基本数据类型的数据写入一个对象流后,可以在以后的操作中把它们读进内并重构这些对象。从对象流中读取对象是使用ObjectInputStream类的readObject()方法。该方法的定义如下:
public final Object teadObject()
throws IOException,ClassNotFoundException
ObjectInputStream类的readObject()方法从串行化的对象流中恢复对象,并且通过递归的方法遍历该对象对其他对象的引用,最终恢复对象所有的引用关系。
对象输入流类ObjectInputStream实现了java.io.DataInput接口。DataOutput接口中除了基本read()等方法之外,不定期定义了许多读基本数据类型的方法。所以ObjectInputStream类提供了如下读对象与基本数据类型的方法。
①readObject(),读一个对象。
②read(),读当前对象。③read(byte[ ]buf,int off,int len),读一个字节数组的一部分。
④readBoolean(),读一个布尔值。
⑤readByte(),读一个8位字节。
⑥readChar(),读一个16位字节。
⑦readDouble(),读一个64位clouble型数值。
⑧readFloat(),读一个32位float型数值。
⑨readInt(),读一个32位int型数值。
⑩readLong(),读一个64位long型数值。
readShort(),读一个16位short型数值。
readUTF(),采用UTF格式读一个字符串。
3、构造可串行对象的类
一个类只有实现了Serializable接口,它的对象才是可串行化的。因此如果要串行化某些类的对象,这些类就必须实现Serializable接口。而实际上,Serializable是一个空接口,它的目的只是简单地标识一个类的对象可以被串行化。
Java中,Serializable接口的完整定义是:
package java.io;
public interface Serializable {
};
因此,在定义可串行化类时,只需要在类的定义中增加implements Serializable的子句。
在对象进行串行化时,只有对象的数据被保存,而该对象所属类的方法与构造方法不在串行化的流中。另外的实现Serializable类中,静态变量和使用transient关键字可以使某些数据不被串行化。对象数据包括这些数据项所引用的对象构成的体系称为对象图(Object Graph)。如果被保存对象的对象图中存在对不可串行化对象的引用,通过在引用该对象的成员变量前使用transient关键字,可以使被保存对象正常串行化。
可串行化类的数据的访问权限(public、protected、package或private )对于数据的串行化没有影响。数据是以字节的形式写流而字符串型数据将表示为UTF格式,即文件系统安全全局字符集转换格式。
对于对象的串行化处理,程序员可以不编写任何方法,使用Java提供的串行化默认机制。ObjectOutputStream类的defaultWriteObject()方法,把重构该对象所需的类的信息包括成员变量的信息以及需要序列化的数据自动写入到对象流中。而ObjectInputStream类的defaultReadObject()方法能够将对象流中的对象进行反串行化,即进行对象的重构。
4、定制串行化
对象串行化定制分为两个层次:一个层次是对可串行化类自己定义数据的输出进行定制,这可以称为一种部分定制串行化。另一个层次是对可串行化类所有的数据(包括自己定义的及其父类的数据)的输出都进行定制,这可认为是完全定制串行化。
(1)部分定制串行化
在串行化类中定义两个方法实现部分串行化的定制。这两个方法是:writeObject()和readObject()。writeObject()方法控制要保存的信息,一般是在流中增加其他附加的信息。readObject()方法既实现相应writeObject()方法所写入的信息,又可以用来在对象被恢复后进行对象数据的更新。writeObject()方法声明的格式必须按照下面的例子中的格式,并且如果要使用默认的串行化机制保存对象的非静态、没有transient关键字修饰的数据项,可以在该方法的第一处语句调用对象流的defaultWriteObject()方法,而随后可以出现对象序列化特殊处理的其他语句:
private void writeObject(ObjectOutputStream s)throws IOException {
s.defaultWriteObject();
//定制串行化的代码
}
readObject()方法必须把writeObject()方法写到对象流中的每个信息都以同样的顺序读出来。如果writeObject()方法中使用了默认的串行化机制,则在readObject()方法中也要首先调用defaultReadObject()方法。另外,readObject()方法可以对读出的对象执行计算或更新对象的状态等操作。
readObject()方法和writeobject()方法只能串行化直接的类。该类父类所需的串行化处理是由系统自动处理的。
(2)完全定制串行化
对于一个实现了Externalizable接口的类的对象,只有对象所属类的标识是自动保存到流中的。这个类不仅负责把实例对象的数据写到流中,而且还需要把其父类的数据也写到对象流中。Externalizable接口实现了Serializable接口,它的完整定义如下:
package java.io;
public interface Externalizable extends Serializable {
public void writeExternal(ObjectOutput out) throws IOException;
public void readExternal(ObjectInput in)throws IOException,java.lang.ClassNotFoundException;
}
实现完全定制串行化的类要遵守以下原则:
①必须实现java.io.Externalizable接口。
②必须实现writeExternal()方法以保存对象的数据或状态。并且该类必须负责把对象的各个超类的数据保存到流中。
③必须实现readExternal()方法,该方法从对象流中读取通过writeExternal()方法写入的对象的数据,同时不定期必须恢复父类中的数据。
④如果对象串行化中使用了外部定义的格式,则writeExternal()方法和readExternal()方法都必须完全依照该格式。
⑤必须定义一个具有public访问权限的不带参数的构造方法。
5、串行化中对敏感信息的保护
如果程序员编写的类涉及保密或比较敏感的数据,则在进行该类对象的串行化时必须注意保护敏感信息。在从串行化的流中恢复对象时,对象的私有数据也被恢复了。因此,对象串行化时必须采取保护手段,并且不能完全相信流中所包含的对象表示都是合法的。为了防止类遭遇上述安全问题,必须保证对象的敏感数据不从流中恢复,或者敏感数据在恢复后要由类进行某些验证。
目前已经不许多技术可以用来保护类中的敏感信息。最简单的方法是把一个类中包含敏感数据的成员变量(或称为域)定义为pviate transient。transient和static类型的数据项是不能进行串行化和反串行化的,这样可以使这些敏感数据不能写到流中,也不会通过串行化机制进行恢复。而且这种情况下,类的私有数据的读、写只能通过类提供的方法,不能由类外的其他方法如writeExternal()方法和readExternal()方法进行,因此保护了类中带有transient标记的变量。
特别敏感或重要的类不应该允许串行化,所以这样的类能实现Externalizable接口和Serializable接口。如果某些类非常需要对象的串行化,则串行化的过程要有特殊控制并且对所恢复的对象要具有验证机制。这些类应该实现writeObject()和readObject()方法来保存和恢复相应的数据。如果发现某些操作非法,可以抛出NotSerializlbleException异常以阻止进一步的访问。
6、串行化的注意事项
进行对象的串行化操作时,要注意下面两点:
(1)transient关键字的使用
(2)串行化对象存储或传输中的安全

How well did you know this?
1
Not at all
2
3
4
5
Perfectly