我记得最开始接触多进程, 多线程这一块的时候我不是怎么理解, 为什么要有多线程啊? 多线程到底是个什么鬼啊? 我一个程序好好的就可以运行为什么要用到多线程啊? 反正我是十分费解, 即使过了很长时间我还是不是很懂, 听别人说过也自己试过, 但总是没有理解透彻;
时间过了很久感觉现在对多线程有了一点新的理解, 我们还是从最基本的开始, 顺便看看从 jvm 的角度看看多线程在 jvm 中是怎么分配内存的, 顺便和前面的几篇内容串一下;
1. 现实中的多线程
举个例子: 假如你一个人在家, 你现在听首歌 5 分钟, 烧开水需要 10 分钟, 玩一局游戏要 20 分钟, 现在问题来了, 你完成这三件事总共需要多少分钟?
假如是小学生肯定会回答 5+10+20=35 分钟啊, 但我们比小学生牛一点, 稍微思考一下就知道是 20 分钟, 因为三件事可以同时做嘛, 玩游戏的同时可以听歌, 顺便烧开水, 一把游戏打完, 歌听完了, 水也烧开然后可以去泡茶了, 舒服!
我们用一个比较简陋的图看看这两种方式(这里先不考虑并发与并行的区别, 方便理解)
可以粗略的看到如果是小学生的话, 要一件事一件事的做, 最后花的时间是三者时间总和; 而我们比较聪明, 由于三件事互不影响, 我们可以三件事同时开始做, 这样就大大减少了不必要的等待时间, 最终三者花费的时间差不多就是最长的那一个.
这里稍微提一下并发和并行的区别;
并发: 这个是在计算机单核 CPU 的前提之下, 我们要清楚一个 CPU 在某一时刻只能做一件事, 但是现在有三件事 (听歌, 烧开水, 玩游戏) 交给 CPU 做, CPU 是个好人, 任劳任怨, 一下子去听歌, 一下子去烧开水, 一下子玩游戏, 最终可以把三件事都给做完, 但是假如同时有几百件事交给 CPU 做呢? emmmm, 最后 CPU 就被累垮了, 住院去了, 于是我们计算机也卡死了; 举个最贴近我们的例子: 以前上学的时候作业太多, 很多时候都是很多科目的作业都没有做完, 那怎么办呢? 只有早上去早点去抄一下同学的, 但是各个课代表来收作业了, 于是只能这个科目作业抄一点马上又把另外一个科目作业抄一点, 玛德, 最后终于在规定时间都抄完了, 可是假如你有 100 个科目的作业没做完, 你会怎么办? 用命去抄也抄不完了, 于是你就累病了....
并行: 多核 CPU 的前提, 现在一个电脑都有多个 CPU, 那么 CPU 同时就可以做多件事, 即使事情再多, 多个 CPU 进行切换最终花费的时间确实大大减少; 还是说说上面抄作业的例子, 假如你现在有 10 门科目的作业没做完, 就靠你一个头脑一只手肯定来不及啊! 于是这个时候你唤醒了前世的记忆, 原来你是哪吒转世, 特么的居然可以变成是三头六臂, 这得可以同时抄多少份作业啊!!! 一下子作业就做完了, 舒服! 但是这个时候作业科目太多的话你即使有三头六臂也不够用啊, 而且相互之间的协调也就变成一个很重要的问题.
并发和并行就是这个意思, 我们现在只关注并发, 看看在单核 CPU 的计算机中一个程序是怎么运行的?
2. 进程和线程
想想什么叫做进程呢? 我的理解就是程序进入了内存就是进程, 比如我们电脑桌面双击 QQ, 优酷, java 虚拟机等, 操作系统就会把这些软件的内容加载到内存中去运行去了, 然后就是运行某编程语言写的代码, 转化为机器码调用操作系统的接口, 然后操作系统的内核会那些硬件驱动程序发出一些指令, 然后我们的电脑屏幕就出现变化了... 我们简单画一画图, 我们主要看 JVM
我们再进入 JVM 中看看, 其中线程 1,2,3 就是我们在 java 代码中要去实现的;
进程: 我们百度一下进程的定义, 最重要的一点就是进程是操作系统资源分配的基本单位, 因为每启动一个程序, 一个进程就创建了, 在操作系统堆内存空间上就开辟了一块空间, 也就是分配了资源.
线程: 现在再来看线程, 百度一下线程定义, 其实就是说: 进程就是一个程序, 这个程序之中可能会同时执行多个任务的代码, 每一个任务就是一个线程, 而且每一个线程都会在 JVM 中有自己独立的 java 栈, java 堆, pc 寄存器等内存空间, 而且 CPU 只能切换线程, 即使是不同程序的线程也可以相互切换.
这里就要说明一下, 想比进程和线程, 创建一个进程是要在操作系统内存中去开辟空间, 会涉及到对操作系统一些函数的调用, 而创建一个线程 (比如在 JVM 中) 只需要在 jvm 中个部分开辟空间, 相比较之下, 肯定是创建线程所耗费的操作系统资源比较少, 但是也不可能无限制的创建很多线程, 不然 jvm 也会出问题!
我随便查了一下, 一般的 web 服务器线程数最大不能超过 CPU 核数 * 50, 如: 8 核 < 300,16 核 < 800, 根据实际情况还可以适当调一下.
记得有句话叫做多个线程之间会竞争 CPU 资源这句话当初我可是很久都没有理解, 这竞争 CPU 资源到底什么鬼? CPU 的资源到底是什么啊? emmmm..
记得以前家里比较穷, 没有像现在一样手机电脑这么多, 家里只有一个电视! 但是有的时候家里人每个人喜欢看的节目都不一样, 于是不可避免的相互之间就为了争这个遥控器而发生冲突, 哈哈哈! 这个时候遥控器就相当于 CPU, 我们每个人都相当于一个线程要完成自己的事情. 但是遥控器就一个, 就会相互抢遥控器, 有的时候我抢过来遥控器看火影忍者没到一分钟, 就被我姐抢去看美食节目, 没过一会儿遥控器就被我爸抢去看新闻去了.....
3.java 中的多线程用法
java 之中用多线程主要是 3 种方式: 类, 接口, 线程池, 接下来我们就随意看一下这三种方式
3.1. 类
这种方式主要是继承 Thread 类, 实现 run()方法, run()方法就是我们所需要做的任务的逻辑代码, 然后将这个类实例化调用 start()方法, 表示现在这个线程随时可以被 CPU 调用;
我还是以上面玩游戏, 烧开水和听歌为例, 随意写个小例子:
注意: 这里先不看 GC, 前台线程有四个线程, 我们创建的三个, 还有执行 main 方法的这个线程(这个也叫主线程), 我们只能保证主线程最优先运行, 至于这四个线程哪个先停止, 随机...
3.2. 接口
这种方式也差不多, 实现 Runnable 接口, 实现其中的 run()方法, 然后实例化这个对象并传入 Thread 类中, 再调用 start()方法;
3.3. 线程池
什么是线程池呢? 你看看我们上面写的创建线程的方法, 都是用的时候就去创建, 用完了就销毁, 下次又要用就又去创建, 这种做法很不好, 因为每次创建和销毁线程都是很消耗 jvm 内部资源的, 因为在 jvm 内部会进行申请空间, 分配空间和释放空间各种操作, 对 jvm 的性能会有一定的影响, 而且假如某个特殊的情况下每个线程只会运行很短的时间就会结束, 那么就会十分频繁的常见和销毁线程, 导致在 jvm 中频繁的申请和释放内存, 这极大的影响 jvm 的运行性能.
但是啊, 如果我们能在程序启动的时候, 就先创建一定数量的线程放在一个池子里, 我们要用的话就去拿, 用完了就再放到池子里, 这样就很好的避免了创建和销毁线程的过程, 这种方式比较友好; 其中这个存放线程的池子就叫做线程池, 接下来我们随意看看线程池的用法:
顺便一提, 利用线程池执行线程任务有两种方式, 一种是我们用的 pool.execute(xxxx), 另外一种是 pool.submit(xxxx), 用法和参数都一样, 只是用 submit()提交内部还是调用 execute(), 而且还可以获取线程执行后的返回值, 后面我们会分析到的;
线程池起到一个类似缓冲的作用, 它可以对池子中的线程数目进行控制, 想想, 假如我们程序直接创建线程那可能会由于创建线程太多导致 jvm 崩溃, 但是我们有一个确定容量的池子, 我们不用担心这个池子会炸了, 我们只需要从池子里拿就好了, 至于拿不拿得到的问题后面我们会好好分析的;
3.4. 看看 Callable 接口
这个接口干嘛的呢? 有了 Runnable 接口了, 还要这个接口干嘛?
不知道有没有注意到那个 Runnable 接口的 run()方法是没有返回值的, 也就是说我们只能把任务交给这个线程去做, 但是做了之后有没有成功, 线程是否异常我们都是不知道的, 于是才有了 Callable 接口, 这个接口就是对 Runnable 接口的一个补充, 这个接口的实现类中没有 run()方法, 却有一个 call()方法用于执行我们的任务逻辑, 而且还能有返回值, 并且能抛出异常等
顺便一提, 返回值已经被封装成一个 Future<T > 类型的了, 我们只需要从这个 Futrue 中取到返回值就可以进行后续操作了, 有兴趣的可以看看 Futrue 这个包装类中有哪些方法可以试试, 反正我暂时是没什么兴趣的....
4. 多线程下的 jvm 内存结构
初学者学多线程其实最迷糊的一点就是多线程的程序中, jvm 是什么样的啊? 还是向以前那样分吗? 到底多线程这个东西在 jvm 中是怎么样存在的呢? 下面我们就来简单看看;
我自己总结的一句话: 一个线程一个栈, 一个方法一个帧;
这句话的意思就是每创建一个线程就会创建一个栈, 每调用一个方法就会在栈中压入一个栈帧;
其实 java 栈是一个动态的东西, 不像我们前面看 jvm 内存结构就是一大块 java 栈, 里面可以有很多块, 一个线程一块, 总共合起来叫做 java 栈, 我继续来画一个丑陋的图看一看:
其实可以看到在我们 java 程序中用多线程的话, 那么每一个线程都会创建一个栈, 同时每个线程都有自己的 PC 计数器, 而且每个栈都是该线程私有的, 别的线程不能访问; 但是在 java 堆和方法区中的数据, 是所有线程共享的, 由于所有的线程都能够使用共享区的数据, 假设一个线程拿到堆中的一个 A 对象进行修改但是需要的时间比较长, 此时另一个线程也要拿到 A 对象进行判断然后做一些操作, 这个时候就会出问题, 因为前一个线程修改的数据还没有同步过来, 后面线程拿到的是旧数据, 这个问题就是多线程的同步问题, 后面我们慢慢分析;
5. 总结
其实初学者觉得多线程比较难, 主要是因为不理解多线程到底是什么? 我们可以把多线程代码用这种奇葩的形式看是不是明显多了, 其中主线程最开始执行并创建自己的栈和 PC 计数器, 一直到创建其他的三个线程并把分别调用 start()方法的时候, 这些线程会随机由 CPU 执行以及切换线程, 并且各个线程都会创建自己的栈和 PC 计数器; 而堆和方法区的数据是共享的, 这会导致出现线程同步问题;
注意: 千万不要觉得主线程比其他创建的线程要特殊, 除了我们程序是由主线程开始之外, 这些线程都是出于同一地位, 很有可能首先是主线程执行完毕, 然后再执行 1,2,3 这三个线程哦~~
来源: https://www.cnblogs.com/wyq1995/p/10742713.html