[深入了解线程对象与线程,线程与运行环境]
在基础篇中的第一节,我就强调过,要了解多线程编程,首要的两个概念就是线程对象和线程。现在我们来深入理解线程对象,线程,运行环境之间的关系,弄清Runnable与Thread的作用。
在JAVA平台中,序列化机制是一个非常重要的机制,如果不能理解并熟练应用序列化机制,你就不能称得上一个java程序员。
在JAVA平台中,为什么有些对象中可序列化的,而有些对象就不能序列化?
能序列化的对象,简单说是一种可以复制(意味着可以按一定机制进行重构它)的对象,这种对象说到底就是内存中一些数据的组合。只要按一定位置和顺序组合就能完整反映这个对象。
而有些对象,是和当前环境相关的,它反映了当前运行的环境和时序,所以不能被序列,否则在另外的环境和时序中就无法“还原”。
比如,一个Socket对象:
Socket sc = new Socket("111.111.111.111",80);
这个sc对象表示当前正在运行这段代码的主机和IP为"111.111.111.111"的80端口之间建立的一个物理连结,如果它被序列化, 那么在另一个时刻在另一个主机上它如何能被还原?Socket连结一旦断开,就已经不存在,它不可能在另一个时间被另一个主机所重现。重现的已经不是原来 那个sc对象了。
线程对象也是这种不可序列化对象,当我们new Thread时,已经初始化了当前这个线程对象所在有主机的运行环境相关的信息,线程调度机 制,安全机制等只特定于当前运行环境的信息,假如它被序列化,在另一个环境中运行的时候原来初始化的运行环境的信息就不可能在新的环境中运行。而假如要重 新初始化,那它已经不是原来那个线程对象了。
正如Socket封装了两个主机之间的连结,但它们并不是已经连结关传送数据了。要想传送数据,你还要getInputStream和getOutputStream,并read和write,两台主机之间才开始真正的“数据连结”。
一个Thread对象并建立后,只是有了可以"运行"的令牌,仅仅只是一个"线程对象"。只有当它调用start()后,当前环境才会分配给它一个运行的 "空间",让这段代码开始运行。这个运行的"空间",才叫真正的"线程"。也就是说,真正的线程是指当前正在执行的那一个"事件"。是那个线程对象所在的 运行环境。
明白了上面的概念,我们再来看看JAVA中为什么要有Runnable对象和Thread对象。
一、从设计技巧上说,JAVA中为了实现回调,无法调用方法指针,那么利用接口来约束实现者强制提供匹配的方法,并将实现该接口的类的实例作为参数来提供给调用者,这是JAVA平台实现回调的重要手段。
二、但是从实际的操作来看,对于算法和数据,是不依赖于任何环境的。所以把想要实现的操作中的算法和数据封装到一个run方法中(由于算法本身是数据的一 个部分,所以我把它们合并称为数据),可以将离数据和环境的逻辑分离开来。使程序员只关心如何实现我想做的操作,而不要关心它所在的环境。当真正的需要运 行的时候再将这段"操作"传给一个具体当前环境的Thread对象。
三、这是最最重要的原因:实现数据共享
因为一个线程对象不对多次运行。所以把数据放在Thread对象中,不会被多个线程同时访问。简单说:
class T extends Thread{
Object x;
public void run(){//......;}
}
T t = new T();
当T的实例t运行后,t所包含的数据x只能被一个t.start();对象共享,除非声明成 static Object x;
一个t的实例数据只能被一个线程访问。意思是"一个数据实例对应一个线程"。
而假如我们从外部传入数据,比如
class T extends Thread{
private Object x;
public T(Object x){
this.x = x;
}
public void run(){//......;}
}
这样我们就可以先生成一个x对象传给多个Thread对象,多个线程共同操作一个数据。也就是"一个数据实例对应多个线程"。
现在我们把数据更好地组织一下,把要操作的数据Object x和要进行的操作一个封装到Runnable的run()方法中,把Runnable实例从外部传给多个Thread对象。这样,我们就有了:
[一个对象的多个线程]
这是以后我们要介绍的线程池的重要概念。