Runnable和Thread源码分析

Runnable和Thread源码分析

问题的引入
Runnable是一个接口,它只有一个run()抽象方法,run()抽象方法需要被实现后才能使用。Thread类继承了Runnable接口,并且Thread中实现了run()方法。最后,通过Thread类的start()方法就可以启动线程了。这是我们平时在应用中使用线程的常用方法。如果多加思考你会发现以下几个问题:
1、为什么调用start()方法就可以启动线程,而不是调用run()。start()方法和决定线程运行内容的run()方法又有什么联系呢?
2、在Runnable层面实现run()方法和Thread层面实现run()方法的区别。
3、怎么用Runnable实现run方法,并在Thread中执行Runnable实例化后重写的run方法。
不着急下结论,我们通过分析源码来理清两者的关系。
Runnable源码
打开如下Runnable的源码,我们可以发现,它只含有一个抽象的run()方法。因为它是接口,所以需要被继承后重写run()方法才能使用。我们可以简单思考一下,如果需要启动一个新的线程,这意味着需要分配给它CPU资源来执行这个线程,而CPU是不归JVM(Java虚拟机)直接管辖的,自然需要通过JVM通过外部的接口来实现和操作系统的对话,调整CPU资源的分配,所以,线程的创建一定是一个native方法(实现Java调用底层的C、C++代码)。接口中的run()方法中的内容,仅仅代表线程运行的内容。一个结论出来了,Thread类中必然有native方法,Runnable无法脱离Thread类来新建线程。如果我们在new一个线程对象后直接调用run方法,也只是让当前线程去执行run()方法中的语句罢了,并没有实现多线程。

public interface Runnable {
    public abstract void run();
}

Thread源码
接下来我们看看Thread类的源码。
从下面这句我们可以看出Thread继承了Runnbale接口。

public class Thread implements Runnable {

下面是Thread类的run()方法。里面的target对象是什么?为什么target能调用run()方法?这个run()方法是谁的run()方法?

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

为了解决问题,我们把关注点放到Thread类的这几行代码上。在Thread类的一个构造方法中Runnable接口的实例化对象target被传入其中,通过几个方法的调用,并通过this.target = target;语句把Runnable的实例化对象保存在了Thread类中,成为了Thread类的一个成员变量,那么它调用的run()方法自然是在Runnable实例化中重写的run()方法。

    private Runnable target;
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);        
    }    
   private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        init(g, target, name, stackSize, null);
    }
   private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {
          ....................  //省略
         this.target = target;
         ......................//省略
    }

这里大家可能会有一点疑问,Runnable是一个接口,接口为什么会有对象?
Runnable入口参数的两种实现形式
1、定义一个类继承Runnable接口,并重写run()方法,创建这个类的实例化对象,在new一个Thread类对象的时候作为入口参数传入构造方法。根据里氏代换原则,任何基类(父类,在这里接口也适用)可以出现的地方,子类也可以出现(此处可以理解为向上转型),这个类的对象可以传入Thread类的构造方法。实现代码如下:

class MyThread implements Runnable{
	public void run() {	    
		while(ticket>0)
		{
			System.out.println("");
		}
	}
}

2、通过匿名内部类,直接new一个Runnable然后紧接着在后面重写run方法,把它作为参数传入构造方法,因为没有对象名,叫做匿名内部类,它可以实例化接口和抽象类。类似如下实现:

          Thread th1 = new Thread(new Runnable()
         	{
  		         public void run()
  		         {
  		        	 System.out.println("");
  		         }
  			});

start()方法
最后再看看线程创建的核心,start()方法。这里我们不关注其他的变量和内容,只关注线程的实现。我们发现在这个方法中调用了start0()方法。

    public synchronized void start() {
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

start0()方法就是我们一直在寻找的native方法,它没有Java的方法体,它关系到下层C、C++语言的调用,执行这个方法就会为线程申请物理资源并启动线程,线程运行后,便会执行run()方法。

    private native void start0();
    /**
     * 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)
     */

思路整理
关键:run()方法存在于Thread类和Runnable接口中。
1、如果我们选择创建一个类继承Thread,然后重写run()方法,原先的run()方法被子类重写的run()方法覆盖,原先run()方法中的 target.run();不可能被调用,线程对象调用start()方法后运行的是重写后的run()方法中的内容。
2、如果我们实例化Runnable对象,重写它的run()方法(注意哦!这里的run()方法不是Thread类中的run()方法),然后
在创建Thread对象的时候把它传入,接下来用Thread对象调用start()方法执行的就是target.run(),也就是Runnable实例化后重写的run()方法的方法体了。
3、如果在传入Runnable实例化对象后又重写了Thread类的run()方法,最终调用的是Thread类中重写的run()方法。

Runnable和Thread在应用上的区别

一个简单的买票程序
一、写一个类继承Thread,重写run()方法,new若干线程执行买票程序。
1、代码实现

public class Test {	
	    public static void main(String[] args)
	    {
	    	MyThread mt1 = new MyThread();
	    	MyThread mt2 = new MyThread();
	    	MyThread mt3 = new MyThread();
	    	mt1.start();
	    	mt2.start();
	    	mt3.start();
	    }
}
class MyThread extends Thread{
    private int ticket=10;
	public void run() {	    
		while(ticket>0)
		{
			System.out.println("卖出了ticket:"+ticket--);
		}
	}
}

2、运行结果
在这里插入图片描述
3、结果分析:每一张票都被买了3次,因为在每次new一个对象后,都会在java的堆中开辟一个新的空间来存储对象的信息,这就包括成员变量信息,在这里就是ticket变量,new了3个对象,就有3个地方存储了ticket值,彼此独立,因此每个线程执行着彼此独立的买票流程。
4、解决方法:可以ticket变量加上static修饰,使得它变为被所有对象共享的静态变量,可以解决这个问题。当然上述方法并不能体现我们这次讨论的实质,根据结果分析我们可以知道在Thread重写run()方法这种方式下多线程不能共享资源,它们彼此独立。另外,使用Runnable的run()方法实现也可以解决这个问题,下面来看第二种方法。
二、实现Runnable接口,作为参数传入Thread类。
1、代码实现

public class Test {	
	    public static void main(String[] args)
	    {
	    	MyThread mt = new MyThread();
	    	Thread th1 = new Thread(mt);
	    	Thread th2 = new Thread(mt);
	    	Thread th3 = new Thread(mt);
	    	th1.start();
	    	th2.start();
	    	th3.start();
	    }
}
class MyThread implements Runnable{
    private int ticket=10;
	public void run() {	    
		while(ticket>0)
		{
			System.out.println("卖出了ticket:"+ticket--);
		}
	}	
}

2、运行结果
在这里插入图片描述
3、结果分析:实现了10张票就卖出10次,因为通过前面的源码分析我们可以知道,每个线程都持有着Runnable实例化对象的引用,调用的都是这个对象的run()方法,因此是对同一个ticket变量进行操作。至于图中出现的顺序问题,是因为多线程操作的时候,修改变量的速度远大于打印出这个变量的速度,在改变了ticket值之后还没来得及打印出来,下一个线程就继续对变量进行操作了。
总结:在多线程的应用中我们更加倾向于使用Runnable来实现run()方法,因为这可以使得多线程在执行相同的代码的同时操作相同成员变量(多线程共享资源)。因此,多线程的最佳打开方式是:写一个类继承Runnable接口,重写run()方法,再以Thread为媒介启动线程。

猜你喜欢

转载自blog.csdn.net/mayifan_blog/article/details/85794988