美文网首页Java线程基础
并发面试题:java中有几种方法可以实现一个线程?

并发面试题:java中有几种方法可以实现一个线程?

作者: 叩丁狼教育 | 来源:发表于2019-02-15 14:44 被阅读146次

    本文作者:黄海燕,叩丁狼高级讲师。原创文章,转载请注明出处。

    1.创建并启动线程的6种方式:

    1)继承Thread类创建线程
    2)实现Runnable接口创建线程
    3)使用Callable和FutureTask创建线程
    4)使用线程池,例如用Executor框架
    5)Spring实现多线程(底层是线程池)
    6)定时器Timer (底层封装了一个TimerThread对象)

    1.1 继承Thread类创建线程

    1.1.1继承Thread类方式创建线程的实现步骤:

    步骤:
    1):定义一个类A继承于java.lang.Thread类.
    2):在A类中覆盖Thread类中的run方法.
    3):我们在run方法中编写需要执行的操作---->run方法里的,线程执行体.
    4):在main方法(线程)中,创建线程对象,并启动线程.
            创建线程类对象: A类 a = new A类();
            调用线程对象的start方法: a.start();//启动一个线程

    注意:千万不要调用run方法,如果调用run方法好比是对象调用方法,依然还是只有一个线程,并没有开启新的线程.

    1.1.2需求:使用两个线程实现边听歌边打游戏

    实现代码:

    //音乐线程
    public class MusicThread {
    public static void main(String[] args) {
    //创建游戏线程对象
    GameThread game = new GameThread();
    //启动游戏线程
    game.start();
    while(true){
    System.out.println(Thread.currentThread().getName()+"听音乐!");
    }
    }
    }
    //游戏线程
    class GameThread extends Thread{
    @Override
    public void run() {
    while (true) {
    System.out.println(Thread.currentThread().getName()+"打游戏!");
    }
    }
    }
    

    注意:有的小伙伴可能觉得音乐线程没有启动,在这里其实音乐线程已经启动起来了,而启动音乐线程的对象就是我们的JVM,此处main方法其实启动的时候会创建一个主线程去执行main方法,所以我在这里使用主线程作为了我的音乐线程.

    1.2 实现Runnable接口创建线程

    1.2.1实现Runnable接口方式创建线程的实现步骤:

    1):定义一个类A实现于java.lang.Runnable接口,注意A类不是线程类.
    2):在A类中覆盖Runnable接口中的run方法.
    3):我们在run方法中编写需要执行的操作---->run方法里的,线程执行体.
    4):在main方法(线程)中,创建线程对象,并启动线程.
            创建线程类对象: Thread t = new Thread(new A());
            调用线程对象的start方法: t.start();

    1.2.2需求:使用两个线程实现边听歌边打游戏

    实现代码:

    //音乐线程
    public class MusicThread {
    public static void main(String[] args) {
    //创建游戏线程对象
    Thread game = new Thread(new Game());
    //启动游戏线程
    game.start();
    while(true){
    System.out.println(Thread.currentThread().getName()+"听音乐!");
    }
    }
    }
    
    
    //游戏
    class Game implements Runnable{
    @Override
    public void run() {
    while (true) {
    System.out.println(Thread.currentThread().getName()+"打游戏!");
    }
    }
    }
    

    1.2.3 继承方式和实现方式的区别

    • 1)继承方式是一个类继承了Thread后成为线程类的子类,实现方式是一个类实现Runnable接口,但是这个类不是线程类,因为该类没有start等方法.
    • 2)启动的时候继承方式直接调用自己的start方法,实现方式是借助了Thread中的start方法启动的,自身没有start方法
    • 3)继承方式调用的run方法是通过方法覆盖,通过继承方式实现的,运行的时候先找子类,没有最后才运行父类的run方法.实现方式是执行Thread的run方法,而Thread中的run方法调用了实现类中的run方法,使用过组合关系的方法调用实现的.

    1.3实现 Callable 接口

    1.3.1使用Callable和FutureTask创建线程的实现步骤:

    • 1)定义一个Callable接口的实现类
    • 2)创建Callable实现类对象传递给FutureTask构造器
    • 3)将FutureTask对象传递给Thread构造器
    • 4)Thread对象调用start方法启动线程
    • 5)通过FutureTask对象的get方法获取线程运行的结果

    注意:
         Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

    使用场景:使用多线程计算结果并返回该结果。

    1.3.2需求:使用2个线程异步计算1-1000,000内之和

    实现代码:

    public class CallableDemo {
    public static void main(String[] args) throws Exception  {
    //1.创建并启动线程
    Callable<Integer> call1 = new CallableImpl(0, 50000);
    Callable<Integer> call2 = new CallableImpl(50001, 100000);
    
    
    FutureTask<Integer> f1 = new FutureTask<>(call1);
    FutureTask<Integer> f2 = new FutureTask<>(call2);
    
    
    new Thread(f1).start();
    new Thread(f2).start();
    //2.获取每一个线程的结果
    int ret1 = f1.get();
    int ret2 = f2.get();
    int ret= ret1+ret2;
    System.out.println(ret);
    }
    }
    class CallableImpl implements Callable<Integer>{
    
    
    private int min;
    private int max;
    
    
    public CallableImpl(int min, int max) {
    this.min = min;
    this.max = max;
    }
    
    
    @Override
    public Integer call() throws Exception {
    int sum = 0;
    for (int i = min; i <= max; i++) {
    sum+=i;
    }
    return sum;
    }
    }
    

    1.3.3Callable和Runnable的区别如下:

    1. Callable定义的方法是call,而Runnable定义的方法是run。

    2. Callable的call方法可以有返回值,而Runnable的run方法不能有返回值。

    3. Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。

    注意:

        FutureTask为Runnable的实现类
        FutureTask可以视为一个闭锁(门闩),因为只有当线程运行完才会出现结果。

    1.4使用线程池(Executor框架【后面详细讲解Executor框架】)

         线程池,顾名思义就是一个池子里面放了很多的线程,我们用就将线程从里面拿出来,使用完毕就放回去池子中。设计和数据库连接池相似,存在静态工厂方法用于创建各种线程池。

    操作步骤:

    1)使用Executors工具类中的静态工厂方法用于创建线程池
          newFixedThreadPool:创建可重用且固定线程数的线程池,
          newScheduledThreadPool:创建一个可延迟执行或定期执行的线程池
          newCachedThreadPool:创建可缓存的线程池
    2)使用execute方法启动线程
    3)使用shutdown方法等待提交的任务执行完成并后关闭线程。

    代码演示如下:

    public class Demo4 {
        public static void main(String[] args) {
            Executor executor = Executors.newFixedThreadPool(5);
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            ((ExecutorService) executor).shutdown();
        }
    }
    

    1.5 Spring实现多线程

        在Spring3之后,Spring引入了对多线程的支持,如果你使用的版本在3.1以前,应该还是需要通过传统的方式来实现多线程的。从Spring3同时也是新增了Java的配置方式,而且Java配置方式也逐渐成为主流的Spring的配置方式。

    代码演示如下:

    导入的包:

    <dependencies>
         <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter</artifactId>
              <version>2.1.0.RELEASE</version>
         </dependency>
    </dependencies>
    

    配置类:

    @Configuration
    @ComponentScan("cn.wolfcode")
    @EnableAsync //允许使用异步任务
    public class SpringConfig {}
    

    服务类:

    @Service
    public class SpringService {
        @Async // 这里进行标注为异步任务,在执行此方法的时候,会单独开启线程来执行
        public void dowork1() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
            }
        }
        @Async
        public void dowork2() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
            }
        }
    }
    
    
    

    测试类:

    public class SpringThreadDemo {
        public static void main(String[] args) {
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
            SpringService bean = context.getBean(SpringService.class);
            bean.dowork1();
            bean.dowork2();
        }
    }
    

    注意:此时会出现一个DEBUG信息

    image.png

    在这里DEBUG信息不是什么错误,不会影响代码的正常运行,其实可以不用管的,但是为什么出现这个问题呢?

        Spring的定时任务调度器会通过BeanFactory.getBean的方法来尝试获取一个注册过的TaskExecutor对象来做任务调度,获取不到TaskExecutor对象再尝试找ScheduledExecutorService 对象,都找不到就报DEBUG信息。报错之后就找自己本身默认的scheduler定时器对象,这个举动其实是做一个提醒作用,所以如果没有强迫症可以不用管它。

    1.5.1解决Spring使用多线程的报错信息

    强迫症患者想要解决怎么办,三种方式:

    1.在log4j文件中加入log4j.logger.org.springframework.scheduling = INFO(治标不治本)
    2.在本配置文件或者配置类中设置一个bean
    3.配置类实现AsyncConfigurer接口并覆盖其getAsyncExecutor方法

    1.6 定时器

    严格来说定时器(Timer)不是线程,他只是调度线程的一种工具,它里面封装了一个线程,所以我们可以使用定时器来使用线程。

    image.png image.png

    操作步骤:

    1)创建Timer 对象
    2)调用schedule方法
    3)传入TimerTask子类对象

    代码演示如下:

    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
            }
    
    
        }
    }, 100, 100);
    

    想获取更多技术干货,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html

    相关文章

      网友评论

        本文标题:并发面试题:java中有几种方法可以实现一个线程?

        本文链接:https://www.haomeiwen.com/subject/biopeqtx.html