详细分析 Java 中实现多线程的方法有几种?(本质)

正确说法(本质)

实现多线程的官方正确方法: 2 种。

Oracle 官网的文档说明

https://docs.oracle.com/javase/8/docs/api/index.html

public class Threadextends Objectimplements RunnableA thread is a thread of execution in a program. The Java Virtual Machine allows an application to have multiple threads of execution running concurrently.Every thread has a priority. Threads with higher priority are executed in preference to threads with lower priority. Each thread may or may not also be marked as a daemon. When code running in some thread creates a new Thread object, the new thread has its priority initially set equal to the priority of the creating thread, and is a daemon thread if and only if the creating thread is a daemon.
When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:
The exit method of class Runtime has been called and the security manager has permitted the exit operation to take place.All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started. For example, a thread that computes primes larger than a stated value could be written as follows:
     class PrimeThread extends Thread {         long minPrime;         PrimeThread(long minPrime) {             this.minPrime = minPrime;         }
         public void run() {             // compute primes larger than minPrime              . . .         }     } The following code would then create a thread and start it running:
     PrimeThread p = new PrimeThread(143);     p.start(); The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started. The same example in this other style looks like the following:
     class PrimeRun implements Runnable {         long minPrime;         PrimeRun(long minPrime) {             this.minPrime = minPrime;         }
         public void run() {             // compute primes larger than minPrime              . . .         }     } The following code would then create a thread and start it running:
     PrimeRun p = new PrimeRun(143);     new Thread(p).start(); Every thread has a name for identification purposes. More than one thread may have the same name. If a name is not specified when a thread is created, a new name is generated for it.
Unless otherwise noted, passing a null argument to a constructor or method in this class will cause a NullPointerException to be thrown.

方法小结
方法一: 实现 Runnable 接口。
方法二: 继承 Thread 类。

代码实例:
实现Runnable接口

/**
 * <p>
 * 实现 Runnable 接口的方式创建线程
 * </p>
 *
 * @author 踏雪彡寻梅
 * @version 1.0
 * @date 2020/9/7 - 00:34
 * @since JDK1.8
 */
public class RunnableStyle implements Runnable {
    @Override
    public void run() {
        System.out.println("用 Runnable 方式实现线程~~~");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }
}

继承Thread类

/**
 * <p>
 * 继承 Thread 类的方式创建线程
 * </p>
 *
 * @author 踏雪彡寻梅
 * @version 1.0
 * @date 2020/9/7 - 00:37
 * @since JDK1.8
 */
public class ThreadStyle extends Thread {
    @Override
    public void run() {
        System.out.println("用 Thread 方式实现线程~~~");
    }

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

两种方式的对比

  • 方法一(实现 Runnable 接口)更好。

  • 方法二的缺点:

从代码的架构去考虑,具体执行的任务也就是 run 方法中的内容,它应该和线程的创建、运行的机制也就是 Thread 类是解耦的。所以不应该把他们混为一谈。从解耦的角度,方法一更好。

该方法每次如果想新建一个任务,只能去新建一个独立的线程,而新建一个独立的线程这样的损耗是比较大的,它需要去创建、然后执行,执行完了还要销毁;而如果使用 Runnable 接口的方式,我们就可以利用线程池之类的工具,利用这些工具就可以大大减小这些创建线程、销毁线程所带来的损耗。所以方法一相比于方法二的这一点,好在资源的节约上。

继承了 Thread 类之后,由于 Java 不支持多继承,那么这个类就无法继承其他的类了,这大大限制了可扩展性。

两种方式的本质区别

  • 方法一: 最终调用 target.run; ,通过以下两图可以知道使用这个方法时实际上是传递了一个 target 对象,执行了这个对象的 run 方法。

  • 方法二: run() 整个都被重写。一旦子类重写了父类的方法,原有方法会被覆盖被抛弃,即以下代码不会被这次调用所采纳。

综上,两种方法都是执行了 run 方法,只不过 run 方法的来源不同。

同时使用两种方法会怎样?

代码演示:

/**
* <p>
* 同时使用 Runnable 和 Thread 两种实现线程的方式
* </p>
*
* @author 踏雪彡寻梅
* @version 1.0
* @date 2020/9/7 - 22:38
* @since JDK1.8
*/
@SuppressWarnings("all")
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。传入了一个 Runnable 对象。
然后重写了 Thread 的 run 方法。最后启动线程。
因为重写了 Thread 的 run 方法,所以它父类的 run 方法就被覆盖掉了,所以即便传入了 Runnable 对象也不会执行它。

总结

从以上的分析中,准确的讲创建线程只有一种方式那就是构造 Thread 类,而实现线程的执行单元有两种方式(run 方法的两种不同实现情况)

方法一: 实现 Runnable 接口的 run 方法,并把 Runnable 实例传给 Thread 类,再让 Thread 类去执行这个 run 方法。

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

经典错误说法

1. 线程池创建线程也算是一种新建线程的方式

  1. 线程池创建线程代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* <p>
* 线程池创建线程的方法
* </p>
*
* @author 踏雪彡寻梅
* @version 1.0
* @date 2020/9/7 - 23:05
* @since JDK1.8
*/
public class ThreadPools {
   public static void main(String[] args) {
       ExecutorService executorService = Executors.newCachedThreadPool();
       for (int i = 0; i < 1000; i++) {
           // 添加任务
           executorService.submit(new Task() {});
       }
   }
}

class Task implements Runnable {
   @Override
   public void run() {
       try {
           Thread.sleep(500);
       } catch (Exception e) {
           e.printStackTrace();
       }
       System.out.println(Thread.currentThread().getName());
   }
}
  1. 线程池创建线程源码(DefaultThreadFactory 中) 

    3. 分析

通过线程池中的源码,可以知道线程池创建线程的方式本质上也是通过构造 Thread 的方式创建的。所以线程池创建线程的本质和上文中是一样的。所以不能简单的认为线程池也是一种新的创建线程的方式。

2. 通过 Callable 和 FutureTask 创建线程,也算是一种新建线程的方式

  1. 类图展示

 

2. 分析

从类图中可以知道这两个创建线程的本质也是和之前的一样的。

3. 无返回值是实现 Runnable 接口,有返回值是实现 Callable 接口,所以 Callable 是新的实现线程的方式

和第2个说法类似

4.定时器是新的实现线程的方式

  1. 定时器实现线程代码示例

import java.util.Timer;
import java.util.TimerTask;

/**
* <p>
* 定时器创建线程
* </p>
*
* @author 踏雪彡寻梅
* @version 1.0
* @date 2020/9/7 - 23:48
* @since JDK1.8
*/
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);
   }
}
  1. 分析

和前面几点一样,定时器创建线程的方法最终本质也离不开上文中的那两类方法。

5. 匿名内部类和 Lambda 表达式的方式创建线程是新的创建线程方式

  1. 实际上也和前面几点一样是一个表面现象,本质上还是那两种方法。

  2. 使用方式代码示例

/**
* <p>
* 匿名内部类创建线程
* </p>
*
* @author 踏雪彡寻梅
* @version 1.0
* @date 2020/9/7 - 23:54
* @since JDK1.8
*/
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();
   }
}
/**
* <p>
* Lambda 表达式创建线程
* </p>
*
* @author 踏雪彡寻梅
* @version 1.0
* @date 2020/9/7 - 23:58
* @since JDK1.8
*/
public class LambdaDemo {
   public static void main(String[] args) {
       new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
   }
}
  1. 运行结果

总结 

多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗。

常见面试问题

有多少种实现线程的方法?思路有 5 点。

从不同的角度看,会有不同的答案。

典型答案是两种。这两种方式的对比。

从原理来看,两种本质都是一样的(都是实现 run 方法)。

具体展开说其他方式(代码的实现上的不同方式,原理还是基于那两个本质)。

将以上几点做结论。

实现 Runnable 接口和继承 Thread 类哪种方式更好?

从代码架构角度。(应该去解耦,两件事情:1.具体的任务即 run 方法中的内容;2.和线程生命周期相关的如创建线程、运行线程、销毁线程即 Thread 类去做的事情)

新建线程的损耗的角度。(继承 Thread 类,需要新建线程、执行完之后还要销毁,实现 Runnable 接口的方式可以反复的利用同一个线程,比如线程池就是这么做的,用于线程生命周期的损耗就减少了)

Java 不支持多继承的角度。(对于扩展性而言)


作者:踏雪彡寻梅
链接:https://www.xilikeli.cn/article/9
来源:https://www.xilikeli.cn
商业转载请联系作者获得授权,非商业转载请注明出处。

- END -

 

猜你喜欢:

字节码增强:原理与实战

Hive基础面试题总结

MapReduce和YARN基础面试题总结

HDFS基础面试题总结

数据中台从哪⾥来,要到哪⾥去?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值