Java多线程详解(底层原理 + 小demo + 线程池)与node单线程例子做比较

提示: 要对多线程和单线程有一定了解,还要知道基本的什么是线程什么是进程,二者间的关系,才能继续往下看哦

这篇文章起源于这段时间的勤思考,我一开始做的是Java,后来转做了段时间PHP,又做了段时间node,相比起Java,最大的不同点应该就是单线程与多线程的区别了;可是node中又有很多的异步IO,asycn和await等异步同步回调方法,心中就起疑惑了,于是带着疑问查阅了很多资料,由于网上很多都是复制粘贴的文章,真正自己总结的实在太少,打破砂锅问到底的文章也很少,大部分就来来去去告诉你Thread,Runable,run(),start()那些如何使用Java 的多线程而已,或者简单潦草一句“因为Java语言底层的支持,所以是多线程”…
!我真是佛了,在搜集大量资料后这里自己做一个总结,希望帮助到跟我一样有十万个为什么的同学们解惑:

问题一:node既然是有异步,那就是有子线程,那为何还能说是单线程串行执行呢??

其实node是可以多条线程的,当然也可以开多进程去实现多核优势,而且还有线程池去管理子线程,最简单的例子就是异步回调了,既然都异步了,那就是开启子线程去跑,不影响唯一一条主线程的前提下进行操作,在子线程完成了再通知回主线程,异步IO机制;但是单线程的前提就是不能阻塞主线程,可以让主线程不停的忙(边等烧开水边等煮饭边砍柴,水烧开了通知主线程去关火),不能让主线程去做计算量大的,执行时间长的工作(不能让主线程自己去炒菜,否则烧水和煮饭它就管不了了,分身乏术)。而且node的异步机制就是事件驱动的,简单滴说,就是水不烧开它不去管,饭没煮好鸣笛它也不去管,这里的煮饭烧水就是它在维护看护的一个事件队列,它一直在等待下一个事件的到来,再去做相对应的操作。
在实际开发中,就发生过并非请求量较高的时候并且还不能用异步的情况下,堆积了大量等待执行的请求,请求没有执行也没有修改等待状态,由于有定时器轮询,很多末尾的请求又重新被拿出来放到了请求队列中;还有一种就是加了await的同步方法,主线程眼巴巴等着它返回,另外调用的第三方又着急催,主线程没办法只能继续await,结果就请求超时了,所以能异步处理的事件尽量异步处理,不等待返回值的地方还是让它慢慢跑,解放主线程去执行其他的任务。

问题二:Java为什么就那么特殊支持多线程呢??

这个问题我查了很多资料,大部分都只说了Java怎么实现多线程,却没说到底具体为什么支持。既然大家都说它是底层实现的支持,那Java底层就是JVM,那就是JVM支持,于是我换了个搜索问法“JVM多线程的实现”,有了。资料说:Java从遥远的1.2版本开始,JVM就是被设计成LWP轻量级进程来实现,(LWP详细介绍和与线程的异同:https://blog.csdn.net/youxia0075454/article/details/78834175);它再向下一层就是KLP,然后到Thread schedule,再到CPU;这样才能让内核的线程支持Java的线程调用,具体结构图如下:

最上面的p就是process进程,它下面分出多个线程其实就是JVM支持的LWP轻量级进程。

在这里插入图片描述
原文博客:https://blog.csdn.net/a407479/article/details/80670789。非常感谢这位大佬的分享,让我得到了自己要的总结。

问题三:多线程真的比单线程快吗?快多少?

不一定快的,在网上很多实验中,单线程串行执行在模拟十万次循环请求以下速度是比多线程要快的,因为省去了线程切换的开销(线程切换要从阻塞态到唤醒,再到获得CPU执行,还要自增计数)这样不断切换是很耗资源,加上还有为了线程安全加synchronized这种同步锁的话,更是慢。但是如果达到百万千万级别,多线程就比单线程快几倍了,具体的比较网上大把博客做了Test Demo的这里就不重复造轮子了。

下面就是基础多线程Demo环节,想必网上已经很多了,这里我就写得更加简单容易理解一点吧,除了extend Thread 外,还能实现Runnable和Callable实现,这里我只实现一种,其他网上一大把》》》

public class Main {

public static void main(String[] args) {
    MyThread myThread = new MyThread();
    new Thread(myThread,"A").start();
    new Thread(myThread,"B").start();
    new Thread(myThread,"C").start();
}

// 实现多线程方法之一:继承Thread类
public static class MyThread extends Thread {
    // 有十张电影票
    private int ticket = 50;

    @Override
    public void run(){
        while (true) {
            if(ticket<=0){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "拿到了一张票,号码为:" + ticket);
            ticket = ticket - 1;
        }
    }
}

}

看看输出结果:

在这里插入图片描述
AC抢到了同一张票,因为不是线程安全的,它们打起来了;现在加synchronized同步锁,就是让所有线程来到这里都乖乖在门口排队,强制它们遵守规则。一次只能进入一个,买够了就走,等待下一次买票。
在这里插入图片描述
仅仅改了这里。
在这里插入图片描述

可以看到它们一个个就很遵守规则了。不会打起来,但是synchronize锁的使用要谨慎,一定程度上非常影响性能,如果要用,可以细化锁的粒度,或者用lock()代替,这里我会在另外一篇文章说明,这里不做重点阐述。

关于线程的其他一些操作方法可以看菜鸟教程;例如isAlive判断是否存活,Thread.sleep()暂停,睡眠等待,setName设置线程名 ,getId获取线程ID等方法。

老生常谈:wait()和sleep()的区别;wait是挂起,放弃锁竞争,释放掉锁,直到待你notify唤醒它才重新参与竞争(注意notify()是随机唤醒的,也可以唤醒指定线程但是要自己实现,就是要么确保只有一个指定线程wait,要么就用线程name或者ID做判断,看看随机唤醒的是不是那个你要的,做个判断;notifyAll是唤起全部wait的线程)。sleep不会释放锁,睡眠后可以让出CPU资源。sleep要抛一个异常:

try{
Thread.sleep(200);
} catch (InterruptedException e){
e.printStackTrace();
}

举一个排队买票的例子:wait() 就是让你等等先别买票,可能有的打算(可能不需要买票也能进去玩,或者还要等一个人一起买),那你就要退出来,不能霸占着窗口不让别人买票。 sleep()是等待一会儿,他票还是要买,只是找不到钱包了,或者找不到手机支付了,所以可以让出CPU给其他线程跑,但是它不释放锁,还是霸占着买票窗口(因为它不是不买,而是暂时找不到钱包);那为什么要抛出异常,这样理解:有人一直找不到钱包,霸占窗口严重影响后面的人,保安就要把它拎出去了。

线程池

先看看它长什么个样子:

在这里插入图片描述
在这里插入图片描述
corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。
maximumPoolSize:最大线程池大小。
keepAliveTime:空余线程存活时间,指的是超过corePoolSize的空余线程达到多长时间才进行销毁。
unit:销毁时间单位。
workQueue:存储等待执行线程的工作队列。
threadFactory:创建线程的工厂,一般用默认即可。
handler:拒绝策略,当工作队列、线程池全已满时如何拒绝新任务,默认抛出异常。

线程池工作流程:

1:有新的任务进来的时候,首先会判断线程池中的线程数是否小于线程池的核心大小corePoolSize;如果是,证明还可以继续创建线程去跑新的任务。就创建新线程直接执行新任务。
2:当线程池线程已经大于corePoolSize时候,就把任务放到缓冲工作队列workQueue中等待执行。
3:判断workQueue工作队列是否已满,如果是,则判断当前线程数是否大于maximumPoolSize最大线程池大小。
4:如果当前线程数小于maximumPoolSize;则创建新线程处理任务。
5:如果线程数已经大于或者等于maximumPoolSize;则无法创建线程,由于队列又已满,只能执行拒绝策略reject方法。
经过上面一通折腾,我们知道了线程是什么,有什么优缺点和具体怎样工作,同时我们也知道了多线程这种玩法非常耗费资源,所以,如果每次使用都任性的创建销毁是非常浪费的;同时我们知道工作线程数与内核线程数的对应关系,如果随意开工作线程,也会超出系统的承受能力导致内存不足而整个服务器拖慢速度甚至崩溃。
所以,线程池的好处很明显就有:
1:能够控制,限制最大线程数。
2:提高线程的复用率,减少重复创建销毁的开销,节约系统资源。
3: 提供了工作线程队列,饱和策略,拒绝策略,能够更好的管理工作任务线程。

线程池那么厉害,是如何工作的呢?

首先上一个线程池的工作流程图吧。

在这里插入图片描述

知识点:向线程池提交Runnable任务时候,有多少种方法?有什么区别?

1: 上面说过的关于创建多线程中有一种方法就是implement Runnable 来实现;其余的操作一样是重写run();然后加一些自己的任务代码逻辑。

2:有submit 和 execute 两种方法。下面是两种方法的详细介绍:

pool.execute(new RunnableTest(“Task1”)); 这里用execute方法提交Runnable Task;没有返回值,没有返回值就是不能监控到任务完成情况。
Future future = pool.submit(new RunnableTest(“Task2”));这里用submit;submit有返回值;是一个Future类型的。可以用这个返回值来判断任务是否执行(如果future.get()==null 就是执行成功)。
/Future类源码******************/其中五个方法都怎么用可以看文档或者百度,很多资料的。

public interface Future {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}

好了,到这里总结的就差不多了,如果还要更深入一步的了解底层原理,其实看源码也不难理解,自己创建两个线程玩玩,创建两个executorPoolService,点开里面的源码看看,其实相对还是好理解的(spring那些才难看懂)。以上是我收集了很多资料看后做的的总结;如果有说错的地方欢迎指正出来,大家一起进步哈。谢谢大家的观看,有不明确的地方可以留言给我,留言必回!鞠躬!

发布了13 篇原创文章 · 获赞 34 · 访问量 3719

猜你喜欢

转载自blog.csdn.net/whiteBearClimb/article/details/103857731