线程--创建线程的几种方式及源码分析

开始整理下线程的知识,感觉这块一直是盲区,工作中这些东西一直没有实际使用过,感觉也只是停留在初步的认识。前段时间一个内推的面试被问到,感觉一脸懵逼。面试官说,我的回答都是百度的第一页,有时间往第二页看看。废话停止,进入正题。

一、创建线程的常用方式:继承Thread类,实现Runnable接口,通过Callable和Future创建线程;

1、继承Thread类创建线程类

(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。

package com.cqfczc.util;

public class FirstThreadTest extends Thread{
  int i = 0;
  //重写run方法,run方法的方法体就是现场执行体
  public void run(){
  for(;i<100;i++){
  System.out.println(Thread.currentThread().getName()+" "+i);
  }
}
public static void main(String[] args){
  for(int i = 0;i< 100;i++) {
    System.out.println(Thread.currentThread().getName()+" : "+i);
    if(i==20) {
      new FirstThreadTest().start();
      new FirstThreadTest().start();
    }
  }
}
}

思考:为什么要继承Thread,继承后为何会调用到我们写的run方法,执行顺序是否正常?
解答:1、因为Thread类是Java类库提供给我们的一个创建线程调用类,我们要创建一个线程只能继承它,因为Java的API提供的类库是遵循“禁止修改、开发扩展”的原则。我们不能直接修改Thread类中的方法,只能通过继承来扩展父类中的方法;2、当子类重写父类的方法后执行时是优先调用子类中的方法,若没有重写则调用父类的方法,这是Java的基本语法不用多说;3、打印结果可以说明执行顺序是凌乱无规则的,在上面的代码中,只能保证每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
java.lang.Thread类中有个run()方法如图所示:
 

如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作。图中的targer就是一个Runable实现类;由此我们分析可知,若不出创建Runnable对象,只能继承Thread类并且覆盖父类的run方法线程执行时才会有效果。

2、实现Runnable接口创建线程类

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。

public class RunnableThreadTest implements Runnable{
  private int i;
  public void run() {
    for(i = 0;i <100;i++) {
    System.out.println(Thread.currentThread().getName()+" "+i);
    }
  }
  public static void main(String[] args) {
    for(int i = 0;i < 100;i++) {
      System.out.println(Thread.currentThread().getName()+" "+i);
      if(i==20) {
        RunnableThreadTest rtt = new RunnableThreadTest();
        new Thread(rtt,"新线程1").start();
        new Thread(rtt,"新线程2").start();
      }
    }

  }
}

思考:为何实现Runnable即可创建线程,执行run方法体?

解答:我们直接看Thread类的源码即可明白,Thread这个类也是实现Runnable接口并且实现了run方法;

此处就不用做过多的解释了,看到上面的源码截图肯定都理解了。只是我们实现了Runnable接口后只用run方法,没有配合操作线程的方法,因此我们需要构造一个Thread对象,并将实现类放进去才能执行操作。到此处大家应该彻底明白为何我们实现了Runnable接口后,必须要把这个实现类再到Thread类中了。

三、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

public class CallableThreadTest implements Callable<Integer>{
  public static void main(String[] args) {
    CallableThreadTest ctt = new CallableThreadTest();
    FutureTask<Integer> ft = new FutureTask<>(ctt);
    for(int i = 0;i < 100;i++) {
      System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
      if(i==20) {
        new Thread(ft,"有返回值的线程").start();
      }
    }
    try {
      System.out.println("子线程的返回值:"+ft.get());
    } catch (InterruptedException e) {
      e.printStackTrace();
    } catch (ExecutionException e) {
      e.printStackTrace();
    }
  }
  @Override
  public Integer call() throws Exception {
    int i = 0;
    for(;i<100;i++) {
      System.out.println(Thread.currentThread().getName()+" "+i);
    }
    return i;
  }
}

分析Callable和FutureTask的源代码就可发现,第三种其实是前俩种的一个变换使用,机制是一样不做过多陈述,我截一些源代码图片一看就明白了。

下图是Callable的接口源码:

下图是FutureTask类的源码:

下图可以得到RunnableFuture接口的本质是继承了Runnable

Java基础知识扩展:

1、通过上面的图片我们也可以得到其他结论:接口是可以继承接口的,RunnableFuture就继承了,并且继承了多个接口;是不是此时有些疑惑,Java遵循的是单继承多实现,为啥这里有多继承了。

山人分析接口是常量值和方法定义的集合,是一种特殊的抽象类。java类是单继承,但接口可以多继承。为何不允许类多继承,假设A同时继承B和C,而B和C同时有一个D方法,A如何决定该继承那一个呢,但接口不存在这样的问题,接口全都是抽象方法继承谁都无所谓,所以接口可以继承多个接口。

2、如下图所示,该方法执行线程中的那块代码

山人分析:上图的代码结构为new Thread(){}.start(),利用Java基础知识可知,创建了一个Thread的子类,大括号中的run方法是对父类的重写,小括号中的代码是构造方法的传参。代码执行时应该优先调用子类重写父类的方法,因此线程会执行大括号中的方法体。

三种创建线程的方式对比下:

采用实现Runnable、Callable接口的方式创见多线程
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类;在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
个人感觉三种方式也就这点所谓的区别,实在找不出冠冕堂皇的区别了。

线程系列1--Java创建线程的几种方式及源码分析的更多相关文章

  1. -1-5 java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁 sleep()和wait()方法的区别 为什么wait(),notify(),notifyAll()等方法都定义在Object类中

     本文关键词: java 多线程 概念 进程 线程区别联系 java创建线程方式 线程组 线程池概念 线程安全 同步 同步代码块 Lock锁  sleep()和wait()方法的区别 为什么wait( ...

  2. Java创建Timestamp的几种方式

    1.java创建Timestamp的几种方式 Timestamp time1 = new Timestamp(System.currentTimeMillis()); Timestamp time2 ...

  3. Java 序列化和反序列化(三)Serializable 源码分析 - 2

    目录 Java 序列化和反序列化(三)Serializable 源码分析 - 2 1. ObjectStreamField 1.1 数据结构 1.2 构造函数 2. ObjectStreamClass ...

  4. Java 序列化和反序列化(二)Serializable 源码分析 - 1

    目录 Java 序列化和反序列化(二)Serializable 源码分析 - 1 1. Java 序列化接口 2. ObjectOutputStream 源码分析 2.1 ObjectOutputSt ...

  5. java创建线程的两种方式及源码解析

    创建线程的方式有很多种,下面我们就最基本的两种方式进行说明.主要先介绍使用方式,再从源码角度进行解析. 继承Thread类的方式 实现Runnable接口的方式 这两种方式是最基本的创建线程的方式,其 ...

  6. 【Java入门提高篇】Day21 Java容器类详解(四)ArrayList源码分析

    今天要介绍的是List接口中最常用的实现类——ArrayList,本篇的源码分析基于JDK8,如果有不一致的地方,可先切换到JDK8后再进行操作. 本篇的内容主要包括这几块: 1.源码结构介绍 2.源 ...

  7. 【Java】NIO中Selector的select方法源码分析

    该篇博客的有些内容和在之前介绍过了,在这里再次涉及到的就不详细说了,如果有不理解请看[Java]NIO中Channel的注册源码分析, [Java]NIO中Selector的创建源码分析 Select ...

  8. Java高并发之无锁与Atomic源码分析

    目录 CAS原理 AtomicInteger Unsafe AtomicReference AtomicStampedReference AtomicIntegerArray AtomicIntege ...

  9. ElasticStack系列之十六 & ElasticSearch5.x index/create 和 update 源码分析

    开篇 在ElasticSearch 系列十四中提到的问题即 ElasticStack系列之十四 & ElasticSearch5.x bulk update 中重复 id 性能骤降,继续这个问 ...

随机推荐

  1. Ruby 参考教程

    Ruby 参考教程 https://www.ruby-lang.org/zh_cn/documentation/ http://ruby-doc.org/docs/ https://ruby-chin ...

  2. python3爬虫图片验证码识别

    # 图片验证码识别 环境安装# sudo apt-get install -y tesseract-ocr libtesseract-dev libleptonica-dev# pip install ...

  3. sql的关键字

    整理一下sql的关键字,一直都在用,只是很少去整理,所以今天简单整理一下,主要是整理CRUD的一些关键字. 写在前面:sql 不区分大小写 select 简单查询语句 select columnNam ...

  4. 三主机配置 keepalived VIP高可用

    三台主机: 192.168.33.134 192.168.33.136 192.168.33.137   实验前: 关闭selinux 和iptables     1). 192.168.33.134 ...

  5. java技术面试之面试题大全

    转载自:http://blog.csdn.net/lijizhi19950123/article/details/77679489 Java 面试知识点总结 本篇文章会对面试中常遇到的Java技术点进 ...

  6. 解决IDEA报错Could not autowire. There is more than one bean of 'xxx' type

    更新项目之后IDEA突然出现了这样的报错信息.显示Could not autowire. There is more than one bean of 'xxx' type.这个错误的意思是xxx类型 ...

  7. Pythonic定义

    Pythonic定义 Python最常用的编码风格还是PEP8,详见:http://jython.cn/dev/peps/pep-0008/ Pythonic确实很难定义,先简单引用下<Pyth ...

  8. [微信小程序]聊天对话(文本,图片)的功能(完整代码附效果图)

    废话不多说, 先上图: <!--pages/index/to_news/to_news.wxml--> <view class='tab'> <view class='l ...

  9. 在Linux中,当需要从磁盘读取块时,进程状态会发生什么变化?被封锁了吗?如果是这样,如何选择另一个流程来执行?

    当某个进程需要从磁盘中获取数据时,它实际上会停止在CPU上运行以让其他进程运行,因为该操作可能需要很长时间才能完成-至少需要5ms的磁盘寻道时间,而5ms就是1000万从程序的角度来看,CPU周期是永 ...

  10. datePicker 及 timePicker Diolage弹出对话框式 比较好看的 监听事件

    DatePickerDialog 的监听 new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() { @Override ...