关于Java多线程两种实现方式的一点想法

在看某本书的时候里面讲到继承Thread类和实现Runnable接口的区别。有这么一条是说“继承Thread类不能资源共享,而实现Runnable接口可以。”于是产生了以下问题。

书上的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
*实现Runnable接口可以资源共享
*/
public class ShareResourceTest2 implements Runnable{
private int ticket =20;
public void run(){
for(int i=0;i<100;i++){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖票:ticke="+ticket--);
}
}
}
public static void main(String[] args) {
ShareResourceTest2 srt1 = new ShareResourceTest2();
Thread t1 = new Thread(srt1);
Thread t2 = new Thread(srt1);
Thread t3 = new Thread(srt1);
t1.start();
t2.start();
t3.start();

}
}

//部分输出结果 资源共享
//Thread-0卖票:ticke=20
//Thread-2卖票:ticke=19
//Thread-1卖票:ticke=18
//Thread-2卖票:ticke=16
//Thread-0卖票:ticke=17
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 继承Thread类不能实现资源共享
*/
public class ShareResourceTest1 extends Thread{
private int ticket =20;
public void run(){
for(int i=0;i<100;i++){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖票:ticke="+ticket--);
}
}
}
public static void main(String[] args) {
ShareResourceTest1 srt1 = new ShareResourceTest1();
ShareResourceTest1 srt1 = new ShareResourceTest1();
ShareResourceTest1 srt1 = new ShareResourceTest1();
srt1.start();
srt2.start();
srt3.start();
}
}
//部分输出结果 资源没有共享
//Thread-0卖票:ticke=20
//Thread-0卖票:ticke=19
//Thread-0卖票:ticke=18
//Thread-1卖票:ticke=20
//Thread-0卖票:ticke=17
//Thread-1卖票:ticke=19
//...
///Thread-2卖票:ticke=20
//Thread-1卖票:ticke=1
//Thread-2卖票:ticke=19
//Thread-2卖票:ticke=18

我一看不对啊,两个main方法都不一样。第一个是创建了一个Runnable然后再用这个Runnable去创建三个新的Thread。第二个直接创建了三个Thread。我陷入了沉思- -,我觉得第二个程序不能资源共享可能是这个main方法的原因,而不是因为继承了Thread类。于是我写了以下代码作为测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
*继承Thread类,但可以资源共享。
*/
public class ShareResourceTest1 extends Thread{
private int ticket =20;
public void run(){
for(int i=0;i<100;i++){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖票:ticke="+ticket--);
}
}
}
public static void main(String[] args) {
ShareResourceTest1 srt1 = new ShareResourceTest1();
Thread t1 = new Thread(srt1);
Thread t2 = new Thread(srt1);
Thread t3 = new Thread(srt1);
t1.start();
t2.start();
t3.start();

}
}
//部分输出结果 资源共享
//Thread-2卖票:ticke=20
//Thread-1卖票:ticke=19
//Thread-1卖票:ticke=18
//Thread-3卖票:ticke=17
//Thread-2卖票:ticke=16

我用这个继承了Thread的类创建了一个对象,然后用它再去创建三个线程,实现了资源共享。证明我的想法没有错,不能资源共享并不是因为它是继承了Thread类。而是因为创建这三个线程的方式。那到底是为什么会造成这种现象呢。
两种方式的区别也只有:

  • 一个是用Thread的无参构造方法构造了三个Thread。
  • 一个是用Thread的有参构造方法构造了三个Thread。

于是按ctrl+左键去看看Thread类,发现了这些东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public
class Thread implements Runnable {
//...
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
/* What will be run. */
private Runnable target;
//...
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
}

于是找到了造成以上三个程序结果的原因:

  • 注释中的What will be run 是一个 名为target的实现了Runnable接口的对象 。用有参(有target)构造函数构造一个Thread,该线程的run()方法即执行target的run()方法。如果用有参构造函数创建三个线程,并且传入的是同一个实现了Runnable接口的对象,那么这三个线程中的target指向同一个对象。所以这三个线程的对同一个对象进行操作,从而实现资源共享的效果。
  • 第二个程序用无参构造函数创建线程,则这个target为空,执行Thread本身的run()方法,而这个Thread继承了一个Thread,并重写过run()方法。执行的就是重写过的run()方法。我们创建三个这样的对象,是分别执行每个对象中的run()方法。所以并不能资源共享。
  • 而第三个程序虽然使用继承的方法重写了Thread的run()方法,但是并没有将这个Thread直接用new实例化来使用。而是用它作为一个Runnable target,重新创建了三个target相同的Thread对象。相当于用继承Thread重写run()方法的方式,来间接地实现Runnable接口= =,然后再创建线程。

为了再次验证这个想法,我们去找Thread的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
*找到了一大堆构造函数,只贴这两个了- -
*/
public
class Thread implements Runnable {
//...
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
//...
}

发现构造方法其实在用尽可能多的信息去填充init()这个方法的参数,如果没有就填null。
(顺便发现线程名称是“Thread-”再加一个数字,这个数字是threadInitNumber++。刚好解释了为什么我们的第三个程序的线程名是从1开始而不是从0开始。因为0被我们创建的第一个Thread占用了)
那我们看看init方法的实现

1
2
3
4
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}

emmm,它用同样的方式调用了一个参数更全的init方法。于是我们找到了Thread线程构造方法的核心部分。发现构造函数的target正是赋值给了run()方法执行的target。证明了之前的想法。
所以说资源共享的本质是,多个线程调用同一个对象的run()方法。那么我们的问题已经有了答案。
但来都来了,顺便瞧一下构造一个Thread最多都需要哪些参数吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//...
//当然要包含这句
this.target = target;
//...
}
参数 含义
ThreadGroup g 设置这个线程的线程组
Runnable target 这个线程要执行操作的对象,run()所在的对象。
String name 设置线程名
long stackSize 设定线程栈的大小,就是这个线程在JVM里栈的大小。
AccessControlContext acc 权限控制上下文,不是很懂–Java安全模型
boolean inheritThreadLocals 可继承的ThreadLocal

又看了看Thread的start方法,发现Thread的start方法调用了一个名为start0的本地方法。
而该Thread的run()法执行target的run()方法。所以要执行这个线程.run()也就是执行了target.run()。不过并没有新建真正的线程,只是执行了这个方法而已。