创建多线程的两种方式(从使用到源码详解,内含高频面试题)

1、多线程的创建方式

官网说法:实现多线程一共有两种方法

  1. 实现Runnable接口
  2. 继承Thread类
/**
 * 用 Runnable方式创建线程
 */
public class RunnableStyle implements Runnable{
    public static void main (String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }

    @Override
    public void run () {
        System.out.println("用 Runnable方式创建线程");
    }
}

/**
 * 用 Thread方式实现线程
 */
public class ThreadStyle extends Thread{
    @Override
    public void run () {
        System.out.println("用 Thread方式实现线程");
    }

    public static void main (String[] args) {
        new ThreadStyle().start();
    }
}

注意:方法1更好

  1. 从代码架构的角度考虑,具体执行的任务(run方法的内容),它应该和线程创建以及运行的机制(即Thread类)是解耦的。两者不可混为一谈。所以从解耦的角度方法1更好
  2. 从资源节约的角度,使用方法2,每次我们想要新建一个任务,只能去新建一个独立的线程(这样损耗比较大,需要创建和执行以及销毁)。如果使用方法1,我们就可以利用后面的线程池等工具,大大减少创建线程和销毁线程所带来的损耗
  3. 使用方法2,一个类只能继承一个类,大大限制了可扩展性

两种方法的本质区别:

方法1:最终调用target.run()

方法2:run()整个都被重写

1.1 源码解读

重要字段

public class Thread implements Runnable {
    
    private Runnable target;//这个很重要
}

方法1

public class RunnableStyle implements Runnable{
    public static void main (String[] args) {
        Thread thread = new Thread(new RunnableStyle());//1
        thread.start();//2
    }

    @Override
    public void run () {
        System.out.println("用 Runnable方式创建线程");
    }
}

1Thread thread = new Thread(new RunnableStyle());

public Thread(Runnable target) {
    this(null, target, "Thread-" + nextThreadNum(), 0);
}public Thread(ThreadGroup group, Runnable target, String name,long stackSize) {
    this(group, target, name, stackSize, null, true);
}private Thread(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(
                SecurityConstants.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 (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

    this.stackSize = stackSize;


    this.tid = nextThreadID();
}

看到了没,我们创建的 Runnable 对象(new RunnableStyle())最后被赋予Thread类中的target字段。

2thread.start();

/**
* 使此线程开始执行;Java 虚拟机调用 run 此线程的方法。
* 这里具体为什么,我也不知道,带后面分解
*/
public synchronized void start() {

    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    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 */
        }
    }
}

看,自动调用run方法

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

所以我们调用run方法,本质上是调用我们创建的RunnaleStyle对象的run方法

方法2

public class ThreadStyle extends Thread{
    @Override
    public void run () {
        System.out.println("用 Thread方式实现线程");
    }

    public static void main (String[] args) {
        new ThreadStyle()//1
            .start();//2
    }
}

1new ThreadStyle()

public Thread() {
    this(null, null, "Thread-" + nextThreadNum(), 0);
}public Thread(ThreadGroup group, Runnable target, String name,long stackSize) {
    this(group, target, name, stackSize, null, true);
}private Thread(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(
                SecurityConstants.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 (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

    this.stackSize = stackSize;

    this.tid = nextThreadID();
}

2start();

本质上和前面是一样的,调用start方法后自动调用Thread类的run方法。不过,此时的run方法已经被重写为

@Override
public void run () {
    System.out.println("用 Thread方式实现线程");
}

1.2 同时使用两种方法

/**
 * 同时使用Runnable 和Thread两种方式实现线程
 */
public class BothRunnableThread {
    public static void main (String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run () {
                System.out.println("使用Runnable 方式实现线程");
            }
        }){
            @Override
            public void run () {
                System.out.println("使用Thread 方式实现线程");
            }
        }.start();
    }
}

输出:

使用Thread 方式实现线程

原因:

从面向对象的思想去考虑,使用匿名内部类重写run方法。此时,run方法已经被重写过了,所以不会执行target.run()

1.3 创建线程的方式(面试题)

答:从不同的角度看,有不同的答案

从官方文档的来看,总共有两种。分别是实现接口Runnable方式和继承类Thread方式

展开来说两个方法哪个好哪个不好。实现Runnable有三点优势是优于继承Thread类的

  1. 从代码架构的角度考虑,具体执行的任务(run方法的内容),它应该和线程创建以及运行的机制(即Thread类)是解耦的。两者不可混为一谈。所以从解耦的角度方法1更好
  2. 从资源节约的角度,使用方法2,每次我们想要新建一个任务,只能去新建一个独立的线程(这样损耗比较大,需要创建和执行以及销毁)。如果使用方法1,我们就可以利用后面的线程池等工具,大大减少创建线程和销毁线程所带来的损耗
  3. 使用方法2,一个类只能继承一个类,大大限制了可扩展性

刚才虽然说到了两种方法,但是看原理这两种方法本质都是一样。其实都是执行Thread类的run方法。只不过一种是执行Runnable接口的run方法,一种是重写Thread类的run方法

然后展开来说,除了上面说的两种方法,还有很多方法创建多线程。比如,线程池、定时器。但是他们在源码上面都和上面两个方法一样,万变不离其宗

总结,创建线程只有一种方式,那就是构造Thread类,而实现线程的执行单元有两种方式

方式一:实现Runnable接口的run方法,并把Runnable实现传给Thread类。然后,调用Thread类的run方法

方式二:重写Thread的run方法(继承Thread类)

1.4 创建线程池的错误说法

“线程池创建线程也是创建线程的方式”

public class ThreadPools {
    public static void main (String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.submit(new Task(){});
        }
    }
}
class Task implements Runnable{

    @Override
    public void run () {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

本质上和方法1类似,将我们创建的Runnable对象r传递给Thread类的target字段

public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
                          namePrefix + threadNumber.getAndIncrement(),
                          0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}

“通过Callable和FutureTask创建线程,也算是一种创建线程的方式”
在这里插入图片描述
在这里插入图片描述
所以本质是离不开Runnable接口的

“无返回值是实现runnable接口,有返回值是实现callable接口,所以callable是新的实现线程的方式”

“定时器”

public class DemoTimmerTask {
    public static void main (String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run () {
                System.out.println(Thread.currentThread().getName());
            }
        },1000,1000);
    }
}

“匿名内部类”

/**
 * 使用匿名内部类创建线程
 */
public class AnonymousInnerClassDemo {
    public static void main (String[] args) {
        new Thread(){
            @Override
            public void run () {
                System.out.println(Thread.currentThread().getName());
            }
        }.start();
        new Thread(new Runnable() {
            @Override
            public void run () {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
}

不要被表象所迷惑,创建线程的方式其实和之前的方式没有任何的区别

“Lambda表达式”

public class Lambda {
    public static void main (String[] args) {
        new Thread(
                () -> System.out.println(Thread.currentThread().getName())
        ).start();
    }
}

总结:

多线程的实现方式,在代码中写法千变万化,但其本质还是一样的

1.5 实现Runnable接口和继承Thread类哪种方式更好(面试题)

  1. 从代码架构角度,这里分为两件事情,第一件事情是我们具体的任务(即run方法的内容)。第二个事情和整个线程生命周期相关(比如说,创建线程,销毁线程),这个其实是由Thread类实现的。这两个事情他们的目的不一样,站在代码架构的角度应该去解耦。所以实现Runnable接口方式更好,因为它实现了解耦的目的。
  2. 新建线程的损耗,如果使用了继承Thread方法,通常我们想要新建一个任务,只能去new一个类。这样做的损耗比较大,我们需要去新建一个线程,执行完之后还要去销毁。如果使用实现Runnable接口的方式,我们可以反复地利用当前线程,比如说线程池就是这样做的,这样一来我们对于线程的损耗就少了
  3. Java不支持双继承。大大限制了扩展性
  4. 所以说实现Runnable接口比继承Thread类方式更好

https://coding.imooc.com/class/362.html课程笔记(原创)

  • 29
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值