引言
进程模型的抽象使得我们不用考虑中断、定时器和上线文切换,还能实现程序并发执行的目的。但是不同的进程实际上并不能共享同一个地址空间,多线程可以解决这个问题。
线程的优点
首先,线程能解决引言中描述的问题,同一个进程下的每个线程,都共享同一个地址空间,它们能访问该进程中的共享变量。
其次,线程比进程更轻量级,所以它们能比进程更容易(即更快)创建,也更容易撤销。在许多系统中,创建一个线程较创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,具有这一特性是很有用的。
最后,在多CPU系统中,多线程能够实现真正的并行。
对于第一个关于共享地址空间、共享变量的描述,如果我们只是从文字描述上去理解,可能不会那么深刻,幸好我们可以用熟悉的java来举例。
Java中的共享变量
所谓共享同一个地址空间,能访问该进程中的共享变量,我们从java语言的角度来看,就是对类实例字段、类静态字段和构成数组对象的元素的访问,这些类型的数据是被多线程共享的,局部变量和方法参数是线程私有的,不被共享。我们可以看下面的例子:
public class ShareVariableTest {
private SimpleObject aSimpleObject ;
private static SimpleObject aStaticSimpleObject ;
public ShareVariableTest(SimpleObject simpleObject) {
this.aSimpleObject = simpleObject;
}
public static void main(String[] args) {
ShareVariableTest svt = new ShareVariableTest(new SimpleObject());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(svt.aSimpleObject);
System.out.println(ShareVariableTest.aStaticSimpleObject);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(svt.aSimpleObject);
System.out.println(ShareVariableTest.aStaticSimpleObject);
}
});
thread1.start();
thread2.start();
}
}
在上面的代码中,有实例字段变量aSimpleObject和静态字段变量aStaticSimpleObject,他们都能被多个线程访问,当然,就可能被多个线程修改。
对于构成数组对象的元素,我们可以参考下面的例子:
public class ShareVariableTest {
private static SimpleObject[] simpleObjects = new SimpleObject[10];
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
ShareVariableTest.simpleObjects[0] = new SimpleObject();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
ShareVariableTest.simpleObjects[1] = new SimpleObject();
}
});
thread1.start();
thread2.start();
}
}
simpleObjects作为ShareVariableTest的静态字段变量,是可以被多线程共享的,但是这个例子中它不是重点,simpleObjects指向的是堆上的数组,数组里面会有多个(10个)变量,每个变量指向堆上的一个SimpleObject对象,我们在thread1和thread2中修改的是这些变量的值,所以构成数组的元素在java中也是共享的。
还有一个更复杂的例子:
public class ShareVariableTest {
public static void main(String[] args) {
final SimpleObject[] simpleObjects = new SimpleObject[10];
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
simpleObjects[0] = new SimpleObject();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
simpleObjects[1] = new SimpleObject();
}
});
thread1.start();
thread2.start();
}
}
这个例子中,我们没有实例变量和静态变量,有的只是一个局部变量simpleObjects。
它是在栈上生成的,它指向的对象是一个在堆上的数组,数组里面会有很多的SimpleObject变量,每个变量分别指向堆上面的不同SimpleObject对象。
局部变量simpleObjects当然不是共享的,因为他在main线程的私有方法栈上,而它指向的堆上的数组对象里面的每个指向SimpleObject的变量是共享的,可以被多个线程访问,thread1和thread2的run方法中就对它们进行了修改。
当然,这个例子并不是很清晰,因为内部类的使用增加了我们理解这个例子的难度,匿名内部类访问的外部变量必须是final的,但是这只是对simpleObjects这个变量的限制,对于数组里面的每个SimpleObject变量,仍然是线程共享的。
明白了上面三个示例,线程共享同一地址空间,能够访问进程中的共享数据,这个描述在我们脑海中应该不再那么抽象和空洞了。
当然,共享变量也不只是java中多线程的概念,许多语言例如c都会有多线程之间的共享变量,这里举出java的例子,一方面是因为这个系列是java并发编程,类似共享变量的定义这些东西我们迟早会讲到,另一方面java的例子对于java开发者也更容易理解,如果是其他语言的例子,只会增加理解上的难度,最关键的一点,其他语言我也不是很熟。 。。
小结
线程不仅有更小的创建、销毁和切换开销,还能共享同一地址空间,方便对共享数据进行访问。但是共享数据会造成数据竞争,数据竞争的出现,就拉开了并发编程的序幕。这个是我们后续会慢慢讲述的内容。