线程知识串讲

线程应该都不陌生,他的用处及优点也无需累述。一边走路一边听音乐属于多线程,一个餐厅同时接待多个客户也属于多线程。我们常用的Tomcat也是利用多线程来处理并发请求,一个用户请求可以看作是独立的一个线程(当然Tomcat会有线程池机制)。

本文侧重于:如何理解与线程有关的各个概念及对象,思考他们适用于哪些场景。整理时不是很严谨,如有错误,望指正。

1、Thread

首先从最简单的Thread对象开始。Thread就代表一个线程,用简单的几行代码就可以启动他:

public class ThreadDemo {

    static class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("我是子线程:" + Thread.currentThread().getName());
            //dosomething
        }
    }

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

实例化一个线程类(MyThread),然后调用start()方法即可。线程里要做的事情都放到run()方法里面去。这样main方法所在的主线程和MyThread子线程就可以同时执行,各干各的。
除了以上的写法其实还有更简单的写法:

public class ThreadDemo {
    public static void main(String[] args){
        Thread thread = new Thread(()->{
            System.out.print("我是子线程:" + Thread.currentThread().getName());
            //dosomething
        });
        thread.start();
        //dosomething
    }
}

恭喜,你已经会使用线程了,并且他很简单。别小看他,日常编程中,遇到耗时的任务(且后续操作不依赖此任务运行结果)时,这种方式足以应付。

举例一个典型场景,用户提交一个表单,后台完成相应的业务逻辑之后还要推送通知其他用户。推送是调用第三方服务,还会涉及到网络请求,消耗的时间比较长。这种情况下,推送就可以放到子线程去做,主线程直接完成响应。

但也别高兴太早,事情到这里并没有结束,有兴趣的朋友可以考虑下:以上两种开启子线程的方法又何区别?

2、Runable

上面的第二种方式,在实例化线程的时候调用了Thread众多构造方法中的一个,见下图Thread类源码:
在这里插入图片描述
构造函数的参数是Runable类型,这是要介绍的第二个和线程有关的对象。可以将它理解为“一个可执行的任务”。他也仅仅只能是个任务,因为在Runable接口里只定义了run方法,并且没有返回值。也就是说,把他放到线程里执行,你不能干涉他,并且也拿不到执行结果。(这和Callable有点区别,后续会讲)

回到刚才的问题,他们有什么区别:
第一种:继承Thread,并重写Thread的run方法,启动过程为:thread.start()–>中间过程–>thread.run()
第二种:实例化Thread,传递一个Runable任务,启动过程为:thread.start()–>中间过程–>thread.run()–>runable.run()。
注意两处标红的thread.run(),此run非彼run。第一处run方法已经被我们重写了,是真正的业务逻辑,而第二处是Thread类里面的默认逻辑,他会调用runable.run()方法,业务逻辑都在runable.run()里面。
这样看来仅仅是JDK的内部逻辑处理有点差异,对我们来说没有任何影响,喜欢哪种方式就用哪种。
如果感觉讲的不够详细,这里有更详细的:https://www.linuxidc.com/Linux/2016-03/128997.htm

值得注意的是以上两种方式,主线程和子线程没有必然的联系,也就是上面说的‘各干各的’。一种可能的情况是主线程已运行结束,但子线程还在运行中,这种情况下,子线程会继续运行,不会随着主线程的结束而结束。

可能有人会问,有没有可能主线程结束了,子线程也一起结束呢?答案是肯定的,利用Thread的setDaemon()方法,看代码:

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(()->{
            System.out.println("我是子线程:" + Thread.currentThread().getName());
            for(int i=0;i<3;i++){
               System.out.println("子线程输出:" + i);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.setDaemon(true);//将子线程设置为守护进程
        thread1.start();

        Thread.sleep(3000);
        System.out.println("主线程结束");

    }
}

运行结果:
在这里插入图片描述
第14行 thread1.setDaemon(true); 是将thread1设置为守护线程,这概念也可以简单了解下。与守护线程相对的是用户线程(也就是main方法所在的主线程)。守护线程不能脱离用户线程而独立存在,随用户线程结束而结束
可以看到子线程输出0,1后就没有然后了,如果将代码第14行注释掉输出结果为:
在这里插入图片描述

问题是解决了,但子线程是被中止掉的,而不是自然运行完。有没有办法等子线程运行完成,然后同主线程一起完成呢?用图片表达会形象点:
在这里插入图片描述
为了解决这个问题,我们引出FutureTask。

3、FutrueTask

FutrueTask是第三个要介绍的和线程有关的对象。这个英语不知该如何翻译比较好,暂且先把他理解成:在未来某个时间会完成的任务那么究竟会在什么时候会完成呢,我们不得而知,但我们可以通过FutureTask.get()方法来获知是否完成。如果已完成get方法会立即返回结果,如果还没完成则等待,直到他完成。当然,如果你等得不耐烦也可以把他取消掉。

真是个神器,有了它不用操心子线程的状态,可以更聚焦于业务,只需在需要结果的时候调用FutureTask.get()来获取就可以了。举个生活中的例子,约女友吃饭,看代码:

public class ThreadDemo3 {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        FutureTask<String> task = new FutureTask<>(() -> {
            System.out.println("女友:洗脸...");
            Thread.sleep(3000);
            System.out.println("女友:穿衣服...");
            Thread.sleep(3000);
            System.out.println("女友:照镜子...");
            Thread.sleep(3000);
            System.out.println("女友:化妆...");
            Thread.sleep(3000);
            System.out.println("女友:自拍...");
            Thread.sleep(3000);
            System.out.println("女友:出门...");
            Thread.sleep(3000);

            return "我到了";
        });
        Thread girlfriendThread = new Thread(task);

        System.out.println("我:通知女友...");
        girlfriendThread.start();

        System.out.println("我:身 手 钥 钱 出门");
        Thread.sleep(1000);

        System.out.println("我:等待女友");
        String girlfriendResponse =  task.get();
        if("我到了".equals(girlfriendResponse)){
            System.out.println("我:女友已到,点餐");
        }

    }
}

输出结果:

我:通知女友...
我:身 手 钥 钱 出门
女友:洗脸...
我:等待女友
女友:穿衣服...
女友:照镜子...
女友:化妆...
女友:自拍...
女友:出门...
我:女友已到,点餐

可以看到,在通知了女友后,我就可以做自己的准备了,等我到了约会地点后就可以通过task.get()来获取女友的结果,只要女友一到,我就开始点餐。整个过程女友线程自动运行,不用我操心。
这当中JDK帮我们做了什么呢?其实在调用FutureTask.get()时,JDK会判断FutureTask的状态是否已经完成,完成就直接返回,没有的话就一直等待(这过程中还一系列的判断),而FutureTask完成后会将执行结果Set进去,并更新状态。有兴趣的话可以看FutureTask的源码。总之,JDK已经帮我们做了很多事情。
除了上面说的优点外,FutureTask还有如下特点:

  • 有返回值
  • 可以取消任务
  • 可以抛异常

题外话,在代码第4行,传给FutureTask构造函数的是Callable类型。他和Runable很像,理解时只要记住Runable是没有返回值,而Callable是有返回值即可
题外外话,代码中之所以Callable能以Lambda形式传值是因为在Callable接口上有个@FunctionalInterface注解。

上面这个例子比较生活化,举个工作中比较常见的场景:发布一个活动,活动地址有经纬度,保存活动信息的时候,需要生成活动地址的地图位置图片,以便在活动列表展示。生成地图图片由后端请求高德服务,比较耗时,所以将其塞到FutureTask中。等主线程其他事情处理完毕,调用FutureTask.get()获取地图图片的url,然后和活动的其他信息一起保存到数据库中。

好了,到目前所讲的都还是比较简单的,主线程只开启了一个子线程,但他却已经能应付工作中的很多场景。相信还有很多朋友并不满足,那么,让我们先休息一下,回来之后继续讲解比较比较难的部分:多个子线程

后面涉及到线程有关的对象有:ExecutorService、ThreadLocal、volatile、synchronized、ReentrantLock、AtomicInteger、CountDownLatch等

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值