Java线程

与你相遇

我一直记得第一次出现多线程导致的问题的是2017那年,我还是学生仔,那时候我帮一位老师完成一个学校的系统,那时候的我还是一个刚写Java1年多常常编写宏展开代码的孩子,其中一个功能是毕业学生去抢实习指导老师名额的,我那时候设计全局变量的数据结构还是一个

HashMap:
// Tid 是教师id, sids 是学生id集合 , count是总数
HashMap: [
    "Tid" : {
        "sids" : [sidA, sidB, ...],
        "count" : 0    
    },
    ...
]

哈哈哈,厚礼蟹的,现在哪个时期的我都会在注释上编写,傻子一个,学过程序的都不会用HashMap来承接这个结构,因为是线程不安全的啊。肯定是有人想问,你这个数据为什么不存放在DB上,而是要搞个全局变量,这是因为那时候考虑同时操作的人多了,频繁的修改数据库不好,于是我想到了redis作为中间缓存,等到积累到一定量才一次性更新到DB上,我最初的想法是这个,所以我还多设计了一个全局变量在前面去承上启下Redis的内容,所以问题就出现在这,上线测试(没什么人测试)的时候就出问题了,发现操作的数量和数据明显不对,所以老师急忙的找到我这个坑货问,经过我仔细思考(猛的查百度),终于发现了是多线程这个东西弄的!!!(怎么解决我忘记了,好像是改成ConcurrentHashMap分段锁hashmap)。

这次出错可是在同级同学面前,虽然不是公开测试,这个项目是老师私下组织的所以也是动用课余时间搞的。所以线程这个东西给我伤害还是挺厉害的,其他人基本上是查询操作,只有我这个涉及了线程的影响而不自知,当时肯定是慌得不行,拼命百度啊,往事不堪回首,哈哈哈。于是我之后几乎都会留个心眼,在涉及线程的操作,我格外小心,因为并发编程真不是一两句话解决的,也可能很多人在工作中也未能更多的涉及,或者是涉及了并不深。哪怕是涉及不少也编写过不好,一不小心也会落入多线程这个恶魔的魔爪中。

当然并不是老师上课的不讲过线程,但其实程序有时候真不是记住就行了,确实要实战,要总结实战才会真正的理解,而且那时候老师讲的知识也是非常简单,跟着敲敲代码,能运行就行了,但其中深奥的秘密就算和我们这群菜鸡讲,也是听不懂,而且这个时代,不是真的每个学生是真的喜欢计算机编程,也没有对于编程存在敬畏之心,完全是个挣钱工具罢了。所以在这上面犯错是很正常的,这也是我第一次在线程上犯错,甚至如今也不敢说真的理解了线程这个佬。

我也相信我现在也未能讲清楚线程的深奥,所以这个文章我可能是会持续更新修改,我只能记录每次自己对于线程的理解,我爱多线程带来的性能提升,但也恨控制不当带来的麻烦,并发编程这个也是十分考究编程佬技术的东西。

线程进行时

我们首先简单回顾一下Java中创建线程的方法吧~

继承Thread (宇宙大爆炸的奇点)

继承(extends)Thread类且重写run()方法,实例出来的对象,调用start();方法即可,这个是最原始也是最基础的创建方法。

/**
 * 通过继承 Thread 类,并重写run方法。
 * 只要实例化 继承的子类,调用start();方法即可。调用run方法是无效的,因为run()只是类里面普通的方法,并不发生其他任务,
 * 但start()会让线程变成就绪状态,等待vm的调度,这个调度又是由cup决定,等待cup执行指令,这个时候会去调用普通市民run(),完成方法,
 * 因此线程虽然是分开执行run的,但理论上cup执行还是单线程的,只是cup极快的速度处理指令,使得线程切换执行像是多线程同时执行的错觉。
 */
static class ThreadA extends Thread {

    public static void main(String[] args) {
        ThreadA a = new ThreadA();
        a.start();
        ThreadA b = new ThreadA();
        b.start();
    }

    @Override
    public void run() {
        System.out.println("我是线程" + Thread.currentThread().getName());
    }
}

实现Runnable (管理遗产的律师)

我们知道Java类本身是单继承的(接口是多继承), 那么一个Java类一旦遇到继承了其他类但又想使用线程的情况时,继承Thread对象的方法就显得无可奈何了,我该如何解决这个问题呢?Java提供了Runnable接口解决,使用方法也很简单:

  1. 实现Runnable的run()方法。
  2. 对象作为参数丢给Thread对象创建。
  3. Thread.start()告知启动即可。
/**
 * 通过实现 Runnable接口,并实现run方法。
 * 实例化接口,并将对象作为传参在Thread实例化中,再调用start()方法来执行线程。
 * <p/>
 * 衍生知识:
 * 因为Java8 引入了Lambda 表达式对于匿名方法的实现,因此我们可以不需要通过创建实现类来实现Runnable接口的方法,
 * 直接通过Lambda表达式完成run()的编写即可。
 */
static class ThreadB implements Runnable {

    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadB());
        threadA.start();
        Thread threadB = new Thread(new ThreadB());
        threadB.start();
        // 匿名内部方法也可以实现,实现和Runnable的原理是一样的,都是编写一个Runnable接口去完成线程创建
        Thread thread = new Thread(() ->
                System.out.println("我是线程" + Thread.currentThread().getName())
        );
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("我是线程" + Thread.currentThread().getName());
    }
}

使用起来很简单吧,但我同时也很好奇:

  1. Runnable 接口为何能够实现多线程?
  2. 为何可以直接继承Thread编写run方法,还需要实现Runnable接口来实行run方法在传入到Thread对象?而且不使用Lambda表达式对于匿名方法的实行,感觉也没多么便利。

我简单的追踪下源码:

在这里插入图片描述
在这里插入图片描述
我们观察到Thread本身就是实现Runnable的接口,而未重写的run()方法是调用target.run()的,因此简单的推理实现了Runnable的实现类(实现了具体的run()方法),填写完run()方法之后通过new Thread(实现类),将实现类作为变量赋值到target变量来调用。

所以说通过Runnable继承的方法调用线程的时候,本质上还是使用继承Thread对象的方式,只不过是使用Runnable接口封装了run方法。

之所以要这么设计,就是因为Java类单继承的特性。这个跟优先级有关,如果一个类需要继承更高级的抽象类或者父类,肯定远比继承Thread去实现线程级别高。所以很多场景并不适用直接继承Thread方法去完成线程执行。至于为何多线程的优先级为何低,因为严格来说任何一个程序完全可以只靠单线程去运行,而多线程是作为性能提升的选择。

对此我也想说,网上有人会对比两种方式的好坏,其实哪有什么好坏之分,Runnable这个佬还不是一样要依附在Thread对象上调用线程?没有好坏,只有适不适用你当前的场景,如果你的类完成不需要继承其他类,你大方使用继承Thread并重写run方法和多个一个类实现Runnable接口是一个样。

Callable-FutureTask(知道回消息的人)

虽然Runnable解决了Java单继承的缺陷,而且使用也并不复杂,这自然是我们推荐的使用线程的方案,但这个方案好像并不算完美,人类总是如此贪婪的。我们发现Thread的run()方法是void的,因为主线程创建出新线程并不会阻塞着等待线程去执行run方法,因此你要求在主线程等待结果返回似乎是不可能的。这也是为何并发编程能够提升响应从而提升性能的根本原因。
但贪婪的人类总会有要求你填写的任务需要得到方法的计算结果的,简单来说,就是既要多线程的处理速度又要得到多线程处理的结果在这次请求中返回。因此为了满足任务需要得到计算结果,Java提供了Callable-FutureTask的方案。

Callable接口是Runnable的补充。但需要配合FutureTask完成。

那先看看Callable-FutureTask的简单使用:

/**
 * 通过实现 Callable的方法实现,与Runnable区别是允许返回值,会抛出异常处理。
 * 但Callable不能直接放入Thread中实例化,需要借助FutureTask || Future 的帮助完成运算返回结果。
 */
static class ThreadC implements Callable<String> {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadC threadC = new ThreadC();
        FutureTask<String> futureTask = new FutureTask<>(threadC);
        Thread thread = new Thread(futureTask);
        thread.start();
        String result = futureTask.get();
        System.out.println(result);
    }

    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName();
    }
}

流程相对还是比较简单的,

  1. 实现Callable接口来填写call()方法;
  2. 实例化实现类丢给FutureTask对象;
  3. 把FutureTask对象丢给Thread()构造器里;
  4. 最后调用经典方法Thread.start();
  5. 启用线程,不同的是可以通过FutureTask.get();方法来获取返回值。

从用法和特性上,我们就可以看出可以用于两个地方:

  1. 需要多个处理结果汇总的场景,通过多线程分别去处理子结果,然后在主线程结合成最终结果返回。
  2. 当前任务结果并非需要立马处理的,可以放给多线程去处理,然后主线程去执行下一个业务,这样能够不阻塞优先级更高的业务从而提升响应。

了解了怎么使用,好奇的我会问FutureTask这个是什么玩意?让我们稍微简单的追寻下源码:
在这里插入图片描述
在这里插入图片描述

怎么说?最终好像还是关联到Runnable接口上了?是意味FutureTask本质上也是Runnable的实现类吗?看到这是不是可以简单推理下:

FutureTask实际上和用户自定义的实现类一样,实现了Runnable的run方法了!

所以我们在FutureTask中检索一下run方法:
在这里插入图片描述
简单的阅读,V 泛型代表用户定义的返回类型,result定义就是返回值,并在填写的call()讲明如何计算出对应的result,最后不发生异常的情况,set(result)结果到对象中,因此需要掉用结果,我们调用的是FutureTask的get()方法。

也就是说Callable接口只不过是FutureTask的成员变量,用途是提供可以返回结果的call方法,而真正去执行线程的是定义好的FutureTask实现类。而FutureTask本质上就是Runnable的实现类。
所以所谓的能返回结果,并不是真正在线程得到结果,而是在执行Runnable的run方法里完成结果的保存,从而在主线程中能够获取到结果。

应该并不难理解吧?那我就再水水文章长度:FutureTask实际就是Runnable的实现类,填写了固定的run()方法调用,而具体的返回值是用户通过填写Callable接口call()方法定义如何计算返回值,然后保存到FutureTask的变量中,就可以在主线程通过FutureTask.get()方法得到结果;最后一步就是将FutureTask作为Runnable接口的实现类那样开启线程。花里胡哨的,最终还是回归到了Thread对象本身,无论是Runnable亦或者是Callable,都是在本身Thread的基础上开枝散叶。

线程池(工资有限的招聘)

多线程固然是能够提升性能的利器,但如同一个企业一样,钱并不是无限使用,cpu,运行内存,存储。。都是有限的,如果任由HR无限制的找人,不用一天就倒闭了。当然线程本身就是对象,而且并不小,如果无限制的创建使用,vm很快就会内存溢出从而程序无响应。

因此创建固定线程的数量线程池来处理任务,自然是难以躲避的场景。但也并非是一定,因为线程池本身就是会保存着几个活跃员工,意味着内存就会被固定占用,不管是否存在任务执行。因此线程池也是一个双刃剑,并非一定是最完美的方案。

线程池应该说是应该掌握的内容,本身为了提升系统的性能,如何使用线程成为了程序员必备的技能之一,那如何正确有效的使用线程就是考验技术的问题,因此使用线程池,需要大篇幅的了解,而我现在暂时不想写,只能说待续。。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值