Thread and Runnable

java面试----Thread and Runnable


该博客下所有样例代码会在github逐步更新,请耐心等待,您可以收藏本文章以获得更新通知。

众所周知,java中创建线程的方式有两种,分别是实现 ThreadRunnable接口。

1. 实例:

实现Thread :

public class Main {
 public static void main(String[] args) {
        demo1 demo = new demo1();
        demo.start();
        //另一种写法
        // demo1 demo = new demo1();
        // Thread thread1 = new Thread(demo);
        // thread1.start();
    }
}
class demo1 extends Thread{
    @Override
    public void run(){
        System.out.println("run");
    }
}

实现 Runnable

public class Main {
 public static void main(String[] args) {
        demo2 demo = new demo2();
        Thread thread = new Thread(demo);
        thread.start();
    }
}
class demo2 implements Runnable{
    @Override
    public void run(){
        System.out.println("run");
    }
}

这两种方法的区别在于,java中限制了单继承,所以继承Thread时就会不是很方便,继承Runnable较为灵活。

我们来看看下面的代码块:

public class Main {
    public static void main(String[] args) {
        demo2 demo = new demo2();
        Thread thread1 = new Thread(demo);
        Thread thread2 = new Thread(demo);
        Thread thread3 = new Thread(demo);
        Thread thread4 = new Thread(demo);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
class demo2 implements Runnable{
    int i=0;
    @Override
    public void run(){
        System.out.println(i);
        i++;
    }
}

其运行结果为:
在这里插入图片描述
另一个实现Thread的代码块

public class Main {
    public static void main(String[] args) {
        demo1 demo1 = new demo1();
        demo1 demo2 = new demo1();
        demo1 demo3 = new demo1();
        demo1 demo4 = new demo1();

        demo1.start();
        demo2.start();
        demo3.start();
        demo4.start();
    }
}
class demo1 extends Thread{
    int i=0;
    @Override
    public void run(){
        System.out.println(i);
        i++;
    }
}

运行结果是:
在这里插入图片描述
我们很容易发现,实现Thread接口的类,每创建一个对象都拥有单独的数据,对象与对象之间数据不共享
而实现ruannable的对象,只要是对同一个类创建的对象之间均数据共享

以上说法错误!!!!

网上大多数博客都是这么说的,然而事实并不是这样,注意看上面的代码,是建立了一个类的不同的对象,请问:不同对象之间如何数据共享?再看上面实现Runnable接口的代码,均是对一个对象建立了不同的线程,所以数据才共享!一定注意,通过上面的方式是无法实现数据共享的,因为是不同的对象所建立的不同的线程!

下面的方式正好是对一个实例对象做的建立线程操作,才是数据共享
如果用Thread用一以下方法也可以共享数据

public class Main {
    public static void main(String[] args) {
        demo1 demo = new demo1();
        Thread thread1 = new Thread(demo);
        Thread thread2 = new Thread(demo);
        Thread thread3 = new Thread(demo);
        Thread thread4 = new Thread(demo);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
class demo1 extends Thread{
    int i=0;
    @Override
    public void run(){
        System.out.println(i);
        i++;
    }
}

运行结果:
在这里插入图片描述 在这里插入图片描述
由于线程运行没有先后之分且同时访问了变量,所以会有多种输出情况,解决这一问题可以加锁来避免

class demo1 extends Thread{
    int i=0;
    @Override
    public synchronized void  run(){
        System.out.println(i);
        i++;
    }
}

输出结果总是:在这里插入图片描述
因为在多线程中,加上锁可以避免多个线程对同一资源进行访问。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

2.基本使用 and 生命周期

先来一张图。
在这里插入图片描述
当一个线程new出来时,并没有表示这个创建成功,执行start()方法后,线程的状态就由New转为了Runnable(可运行状态),注意此时并没有进入运行态,此时为就绪态(类比操作系统中的就绪、运行、阻塞三态中的就绪态),只有获得cpu的调度后才能进入运行态。

  • 当发生sleep()时,线程由Running态进入Blocked态:

    sleep():sleep是一个静态方法,需进行异常处理,其有两个重载方法,其中一个需要传入毫秒,另外一个既需要毫秒数,还需要纳秒数,该方法使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。

    例如有两个线程同时执行(没有synchronized 一个线程优先级为MAX_PRIORITY,另一个MIN_PRIORITY,如果没有sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了

  • 当发生join()时,线程也由Running态进入Blocked态:

    join():join()方法使调用该方法的线程在此之前执行完毕,也就是等待该方法的线程执行完毕后再往下继续执行,需要异常处理。

    join方法会使当前线程永远的等待下去,直到期间被另外的线程中断,或者join的线程执行结束,也可以使用另外两个重载方法,指定等待毫秒数,在指定的时间到达之后,当前线程也回退出阻塞。

  • yield():该方法使正在cpu运行的线程暂停执行使之返回就绪态并使之与其他线程再次公平抢占cpu。

  • Blocked状态:也称阻塞态,当线程通过sleep()join()进入阻塞态,当结束sleep()、中断join()或结束I/O时线程退出阻塞态进入就绪态(Runnable)。同样,当线程等待synchronize关键字锁定的资源时,同样进入Blocked态,当线程获得资源时进入就绪态。

  • wait()、notify/notifyAll():均为Object的本地final方法,一般在synchronized 同步代码块里使用。

    • wait():在同步代码块里使用,即 获得锁后,使用 wait() 方法进入阻塞态并释放锁。
    • notify/notifyAll():同样,在同步代码块里使用,用于唤醒被 wait() 方法暂停的线程,且当使用了notify/notifyAll() 后,并没有释放锁,而是要等同步代码块执行完毕后或者在同步代码块后的wait() 后释放锁,而被唤醒的线程需要重新竞争锁,并使得锁的线程进入就绪态等待cpu调度
  • 当线程执行完毕后或者异常退出时,该线程生命周期结束。

  • 守护线程:有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置 为守护线程,如果JVM发现只有守护线程存在时,将结束进程 ,如下:

 public static void main(String[] args) {
        demo1 demo1 = new demo1();
        demo1.setDaemon(true);
        demo1.start();
    }

3. 源码分析

我们先来看 Runnable 的源码:

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

是的,我们惊奇的发现,Runnable只有一个抽象方法,那为什么继承Runnable接口就可以创建多线程了呢?那就先来看看我们是怎么使用Runnable的

public class Main {
 public static void main(String[] args) {
        demo2 demo = new demo2();
        Thread thread = new Thread(demo);
        thread.start();
    }
}
class demo2 implements Runnable{
    @Override
    public void run(){
        System.out.println("run");
    }
}

是的,我们先实例化了一个继承runnable的类的对象,然后再通过 Thread thread = new Thread(demo);的方式创建一个thread对象,再通过thread.start()的方式启动线程;

在源码里我们可以发现,有这么一条

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

很明显,Thread类的构造函数允许Runnable的参,即通过传入Runnable对象是可以构建Thread实例的,
所以,无论是继承Thread类或者是实现Runnable接口的方法,起作用的永远都是Thread类,引入Runnable接口的目的是:java中有单继承的限制,若继承了Thread后是不能继承其他类的,所以这就造成了很大的麻烦,引入Runnable,就可以实现多个接口,是为了避免不能多继承造成的麻烦。

那当我们new了一个Thread出来,具体经过了什么过程创建出了一个线程呢?

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            if (security != null) {
                g = security.getThreadGroup();
            }
           
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
 		 /* 检查是否允许当前线程修改线程组的参数 */
        g.checkAccess();

        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        // 继承父线程是否为守护线程
        this.daemon = parent.isDaemon();
        // 继承父线程的优先级
	    this.priority = parent.getPriority();
	    // 获取类加载器
	    if (security == null || isCCLOverridden(parent.getClass()))
	         this.contextClassLoader = parent.getContextClassLoader();
	    else
	         this.contextClassLoader = parent.contextClassLoader;
	    // 获取继承的访问控制上下文
	     this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext();
	     // 获取执行方法
	     this.target = target;
	     // 设置优先级
	     setPriority(priority);
	     // 继承父类的本地变量
	     if (parent.inheritableThreadLocals != null)
	         this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
	      //设置栈的深度
	    this.stackSize = stackSize;
	        //设置线程id
	    tid = nextThreadID();
    }

    

由于Thread的构造函数里都调用了这个init()方法,所以我们直接忽略构造方法直接来看init().

  • 首先做了一个未命名的异常处理,然后再把当前对象的name赋上值。

  • 然后实例了一个Thread对象.我们来看看currentThread()是什么:

    public static native Thread currentThread();
    

    这是一个static 且是native的方法,native方法即非java语言实现的方法,通过调用Thread parent = currentThread();返回当前执行此方法的线程的信息。

  • SecurityManager security = System.getSecurityManager();安全检查,此处不做拓展

  • 如果构造线程对象时未传入ThreadGroup,Thread会默认获取父线程的TreadGroup作为该线程的ThreadGroup,此时子线程将和父线程在同一个ThreadGroup中。

  • 剩下的看上面的注释

这就完成线程的初始化,当我们执行了start()后,即:

/** 
		* 线程启动方法
		* 此方法的调用会引起当前线程的执行;JVM会调用此线程的run()方法,
		* 并不会被主要方法线程or由虚拟机创建的系统组线程所调用
		* 任何向此方法添加的新功能方法在未来都会被添加到虚拟机中
		* 结果就是两个线程可以并发执行:当前线程(从调用的start方法返回)和另一个线程(它在执行run方法). 
		* 一个线程可以被调用多次.
		* 尤其注意:一个线程执行完成后可能并不会再被重新执行.
		*/
	    public synchronized void start() {
	        //0状态值代表了NEW的状态.
	        if (threadStatus != 0)
	            // 不是新创建的状态抛出异常
	            throw new IllegalThreadStateException();
	
	        // 向线程组里添加此线程,注意:线程创建的时候只是将线程组中的容器容量增加1
	        group.add(this);
	
	        boolean started = false;
	        try {
	            // 使线程进入可运行状态(runnable)
	            start0();
	            started = true;
	        } finally {
	            try {
	                if (!started) {
	                    // 启动失败,再线程组中删除改线程
	                    group.threadStartFailed(this);
	                }
	            } catch (Throwable ignore) {
	
	            }
	        }
	    }
	    
		/* 本地方法,使线程进入可运行状态(就绪)*/
	    private native void start0();
/*线程获得CPU执行权限后执行的方法,实际上是调用的Runnable中的run()*/
	    @Override
	    public void run() {
	        if (target != null) {
	            target.run();
	        }
	    }
	    /*这是在run方法执行结束后,此方法由系统调用,用于在一个线程退出前做一些扫尾工作.*/
	    private void exit() {
	        if (group != null) {
	            // 加锁执行,删除该线程
	            group.threadTerminated(this);
	            group = null;
	        }
	         // GC 回收,全部置为null
	        /* Aggressively null out all reference fields: see bug 4006245 */
	        target = null;
	        /* Speed the release of some of these resources */
	        threadLocals = null;
	        inheritableThreadLocals = null;
	        inheritedAccessControlContext = null;
	        blocker = null;
	        uncaughtExceptionHandler = null;
	    }

实际上我们创建的线程均是本地方法创建的,再由用户和jvm进行管理,当线程执行完后,全部置为null以供垃圾回收。


未完待续

问题:

  1. 为什么继承Thread 不可共享资源而Runnable可以? (疑问太多,各种资料各种说法,待查询权威资料后更新)

  2. synchronize在jvm中是怎么使用的(或synchronize的原理)?

  3. 守护进程原理

  4. 守护进程生命周期

  5. start()run()的区别

    start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,相当于调用了一个普通的类的方法,并没有新的线程启动,start()方法才会启动新线程。

  6. 如何在两个线程间共享数据?

  7. 线程在jvm中的模型是什么?

  8. stop() 和 interrupt() 方法的主要区别是什么呢?

  9. seelp,wait,join,yield之间的区别是什么?

  10. 线程执行过程中哪一部分可能会抛异常?

  11. 为什么继承Runnable就可以创建多线程了?

  12. new thread 后的过程是什么?

参考资料

ruannable、Thread
ReentrantLock
join
生命周期
多线程总结
多线程基础
blocked
wait、notify/notifyAll
currentThread()
Thread源码解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值