举例说明如何通过这两种方式创建线程
继承Thread类创建线程类
public class FirstThread extends Thread {
int i = 0; //不同线程对象不能共享该成员变量
/**
* 重写run方法,该方法为线程的执行体
*/
@Override
public void run() {
for (; i < 10; i++) {
System.out.println(getName() + ": i = " + i);
}
}
}
public class TestFirstThread {
public static void main(String[] args) {
new FirstThread().start();
new FirstThread().start();
}
}
在main方法中创建了两次该线程类,并调用start方法开启线程,然后执行线程中的run方法。
注意:使用继承Thread类创建线程类时,不同线程对象不能共享该线程类的成员变量
上述代码输出结果
Thread-0: i = 0
Thread-1: i = 0
Thread-1: i = 1
Thread-0: i = 1
Thread-1: i = 2
Thread-1: i = 3
Thread-1: i = 4
Thread-1: i = 5
Thread-1: i = 6
Thread-1: i = 7
Thread-1: i = 8
Thread-1: i = 9
Thread-0: i = 2
Thread-0: i = 3
Thread-0: i = 4
Thread-0: i = 5
Thread-0: i = 6
Thread-0: i = 7
Thread-0: i = 8
Thread-0: i = 9
实现Runnable接口创建线程
public class SecondThread implements Runnable {
int i = 0;
@Override
public void run() {
for (; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": i = " + i);
}
}
}
public class TestSecondThread {
public static void main(String[] args) {
SecondThread st = new SecondThread(); //作为target传入Thread的构造方法
new Thread(st).start();
new Thread(st).start();
}
}
运行结果
Thread-0: i = 0
Thread-0: i = 1
Thread-1: i = 0
Thread-0: i = 2
Thread-0: i = 4
Thread-0: i = 5
Thread-0: i = 6
Thread-1: i = 3
Thread-1: i = 8
Thread-1: i = 9
Thread-0: i = 7
从运行结果可以发现用这种方法创建的多个线程可以共享线程类(严格来说不是线程类,只是和线程类一样实现了Runnable接口,说它是线程类的target更合适)的成员变量。下面会分析为什么会这样。
从源码角度分析以上两种方法
其实查看源码可以发现,以上这两种方法虽然实现不同,但内在的原理是一样的。
继承Thread
class Thread implements Runnable {
//........
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
可以看到Thread实现了Runnable接口并重写了run方法。FirstThread方法继承了Thread,其实也就是实现了Runnable接口,然后重写了run方法。
第一种方法开启线程是这样的:new FirstThread().start(); 它执行了Thread的无参构造方法
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
我们重点看第二个参数Runnable target,这里的target为null,也就是说第一种方式实现不需要target,但第二种方式会不一样。
实现Runnable
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到Runnable接口只有一个方法,而SecondThread实现了该接口并重写了这个方法。所以实际上SecondThread也是一个Runnable,也就是说SecondThread可以作为Thread类的一个target。
第二种方法开启线程是这样的:new Thread(st).start(); 它执行了Thread的含有一个Runnable参数的构造方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
这里的第二个参数不在是null,而是传来的参数target,然后在init方法执行过后,SecondThread对象被赋给了Thread的成员变量
private Runnable target;
所以现在虽然Thread的run方法没有被重写,但它实际上是调用了target的run方法,也就是SecondThread中重写的run方法。不妨再看一次Thread重写的run方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
所以说,这两种方法的实质都是重写了Thread中的run方法,只不过是直接重写该方法和调用target所重写的该方法的区别。
两种方式所带来的不同
分析过源码后,现在就更加清楚为什么不同的实现,会产生成员变量是否可以共享的问题。
- 首先是第一种方式,这里new了两个Thread的子类,也就是说有两个对象,等于实现了两个接口对象,那么接口中的run方法的变量自然不能共享了。
new FirstThread().start();
new FirstThread().start();
- 然后是第二种,这里虽然也是new了两个Thread,但是只有一个接口对象,就是st,那么接口中的run方法的变量自然就可以共享了(因为就只有这一个接口对象,也就是说只有这个变量,当然只能共享了)。
SecondThread st = new SecondThread(); //作为target传入Thread的构造方法
new Thread(st).start();
new Thread(st).start();
其实弄懂了这些后,我们也可以用第二种方式实现第一种的效果,只要多创建一个SecondThread(Runnable)对象就行了。
SecondThread st = new SecondThread();
SecondThread st1 = new SecondThread();
new Thread(st).start();
new Thread(st1).start();
改为上述代码之后,再运行就会和第一种方式的效果一样。原因很简单,这里多了一个SecondThread对象,两者的变量当然是不能共享的,因为根本就不在同一块存储空间。