各位读者好, 我是小陈, 这是我的个人主页
小陈还在持续努力学习编程, 努力通过博客输出所学知识
如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
希望我的专栏能够帮助到你:
JavaSE基础: 从数据类型 到 类和对象, 封装继承多态, 接口, 综合小练习图书管理系统等
Java数据结构: 顺序表, 链表, 二叉树, 堆, 哈希表等 (正在持续更新)
JavaEE初阶: 多线程, 网络编程, html, css, js, severlet, http协议, linux等(正在持续更新)
上篇分享了进程 相关的知识, 认识了什么是进程, 什么是并行, 什么是并发
但 Java 并不鼓励多进程编程, 但是十分鼓励多线程编程, 线程也是Java中很重要的知识点, 从本篇开始讲介绍多线程相关的基础内容, 内容较多, 分为若干篇持续分享
提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!
一个线程就是一个 “执行流”, 每个线程之间都可以按照顺序执行自己的代码, 多个线程之间 “同时” 执行着多份代码
上篇说到, 进程其实就是运行起来的程序 , 而所谓的 “执行流” 如何理解呢 ?
比如你打开QQ, 这是一个程序, 运行后就生成了一个进程, 你在QQ里发信息, 这是一个执行流, 生成了一个线程
你和好友打视频电话, 这也是一个执行流, 也就是一个线程
同理, 打语音电话, 逛QQ空间…都会生成一个线程
比如你打开微信, 生成一个进程, 在微信打视频电话, 会生成一个线程, 逛朋友圈会生成一个线程, 打视频电话会生成一个线程, 微信摇一摇也会生成一个线程…
如果把进程比作工厂(程序), 那么线程就是工厂里的一条流水线(执行流)
那不难理解什么是多线程了, 工厂里有多条流水线嘛
比如你在微信, 一边和女朋友打视频电话, 一边和备胎聊天(手动狗头保命) , 视频通话和发消息这两个线程同时运行, 就是多线程并发运行
上一篇介绍进程时提到, 并行和并发是由操作系统控制的, 程序员感知不到, 所以一般把并行和并发都 统称为并发
Java中并不鼓励多进程编程, 而十分鼓励多线程编程, 为啥?
多进程编程 和 多线程编程 都能满足并发需求的场景, 而线程比进程更轻量, 并且某些场景下, 多线程编程可以提高代码的整体执行效率
如何理解线程更轻量?
首先, 线程是被包含于进程中的
, 这不难理解, 毕竟线程是程序中的执行流
例如打微信电话和运行微信的关系, 打微信电话的前提一定是先运行微信
其次, 在上篇介绍进程时引出了一个重要概念: 进程是操作系统分配资源的基本单位
进程的创建, 销毁, 调度, 都是比较耗时的操作, 主要就体现在资源分配这种耗时操作上(因为系统需要通过遍历找到空闲资源, 每次申请都需要遍历)
于是, 操作系统就允许一个进程中可以同时存在一个或多个线程, 多个线程之间 相互独立, 并发运行
这就意味着, 多个线程共用
一份进程申请出来的系统资源(内存资源和硬盘资源)
, 创建进程时, 系统资源分配完, 进程中再创建线程时, 就使用已经申请好的资源就行了, 这样线程就省去了资源分配这种耗时操作
但是创建线程本身还是需要一些时间消耗的
综上所述, 线程比进程更轻量 , 所以 :
创建线程 比 创建进程更快.
销毁线程 比 销毁进程更快.
调度线程 比 调度进程更快.
进程和线程图示 :
所以现在应该明白:
上篇讲到了 进程的调度, 而进程中的线程往往是 “并发” 执行的, 其实 操作系统真正调度的是线程, 当进程中只有一个线程时, 调度这个线程时就是在调度(只包含一个线程的)进程
所以这就引出另一个重要概念: 线程是操作系统调度执行的基本单位
多个线程的执行可以 :
在多个 CPU 核心上同时运行(并行) 或者 在一个 CPU 核心上快速调度(并发)
线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池” (ThreadPool) 和 “协程” , 这个以后会发文介绍
1, 进程是包含线程的
2, 每个进程
都有自己的独立的
内存空间地址和文件描述符表, 而同一个进程中的线程(们)
会 共用
一份内存空间地址(内存资源) 和 文件描述符表(硬盘资源)
3, 进程是操作系统 分配资源的基本单位, 线程是操作系统 调度执行的基本单位
4, 多个进程之间互不影响, 具有独立性, 同一个进程中的线程(们)
虽然也独立运行,但一旦有某个线程崩了, 整个进程都会崩, 会影响进程中的其他线程
1, 问 : 计算机什么时候会创建一个进程或线程?
答 : 对敲代码写程序的程序员来说 : 取决于如何编写代码, 可以用代码创建进程或线程, 对使用软件的用户来说 : 打开(运行)程序就会创建一个进程, 使用程序中的功能就会创建一个线程
2, 问 : 进程不会公用系统资源, 但线程会共用系统资源, 那线程之间会不会因为错误的内存操作产生bug?
答 : 取决于程序员如何编写代码
3, 问 : 创建进程时申请到的资源有多少, 是程序员控制的吗?
答 : 有一部分
是程序员控制的, 比如创建多少变量, 申请多少内存空间, 打开多少文件…还有一部分是操作系统运行进程时的额外资源
4, 问 : 为什么一个线程崩了会影响进程中的其他线程?
答 : 由于线程之间是透明的, 共用一份资源, 如果一个线程
崩了, 那么这个线程极有可能(即将)破坏了这个进程
中的内存地址空间, 所以如果继续执行其他线程, 是由风险的, 说白了就是: 你好我好大家好, 我不好谁都别想好(一颗老鼠屎坏了一锅粥)
5, 问 : 一个进程中可以存在一个或多个线程, 那线程的个数有没有上限?
答 : 理论上, 只要资源无限, 线程就可以有无限个, 可惜系统资源是有限的, CPU核心数也是有限的, 如果线程个数太多, 反而不会体现出 “并发编程” 的好处, 甚至效率还会下降
6, 问 : 那如果进程 A 中的线程太多怎么办? 能不能再创建一个进程 B , 把进程 A 中的线程分给进程 B ?
答 : 进程 A 中线程太多导致运行效率下降, 只能说明系统资源不够了, 即便是再创建一个进程, 也是在同一个 CPU 上运行, 资源总是没有变化, 所以根本上解决问题的方法只有 : 再搞一个 CPU 这就涉及到分布式了, 我暂时还不太了解, 就不瞎BB了
在学习多线程之前, 我们写过的所有 Java 代码都是单线程的 , 当运行 idea 时, 就创建出了一个 idea 进程, 当运行 main 方法时, 就创建出了一个Java 子进程, 这个 Java 子进程里暂时只有一个 main 方法所在的线程, 称之为主线程
这个状态下, 我们的代码暂时是单线程执行的
如何编写多线程程序呢? 首先要学会如何创建其他的线程 : 使用 Thread 类
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装, Thread 类是 JVM 用来管理线程的一个类,每个线程都有一个唯一的 Thread 对象与之关联
下面介绍使用 Thread 类创建线程的两种方法
1, 首先自定义一个类 MyThread, 继承 Thread 类
2, 重写父类 Thread 类中的 run 方法, run 方法的方法体描述了线程执行什么操作
3, 实例化 MyThread
4, 调用 myThread 对象的 start 方法启动一个线程 , start负责启动一个线程
class MyThread extends Thread {@Override public void run() {System.out.println("一个新的thread线程");}
}public class Test1 {public static void main(String[] args) {// 向上转型Thread thread = new MyThread();// 启动线程thread.start();System.out.println("主线程");}
}
run 方法是一个特殊的 “入口方法”, 通过 start 方法启动 thread 线程后, 会自动调用 run 方法, 执行 run 方法里的代码, 注意, run 方法不是我们自己调用的, 是被自动调用的
调用 start 方法之后会创建出另一个 thread 线程
此时, java 子进程中就有了一个主线程, 和一个 thread 线程, 两个线程开始"并发"执行
也可以使用匿名内部类的方式 :
运行效果和上面一致, 只是写法不同
public class Test2 {public static void main(String[] args) {Thread thread = new Thread() {@Overridepublic void run() {System.out.println("一个新的thread线程");}};// 启动一个线程thread.start();System.out.println("主线程");}}
}
1, Runnable 接口 中仅提供了一个 run 方法, 我们定义一个 MyRunnable 类, 实现 Runnable 接口
2, 重写接口中的 run 方法, run 方法的方法体描述了线程执行什么操作
3, 实例化 MyRunnable
4, 把 myRunnable 对象作为参数传给 Thread 类 的构造方法
5, 用 thread 对象调用 start 方法, start负责启动一个线程
使用 Runnable 接口这种方式和继承 Thread类 这两种方式没有本质区别, 都是重写 run 方法, 自己描述线程执行哪些操作, 然后通过 start 启动线程
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("一个新的thread线程");}
}
public class Test3 {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread thread = new Thread(myRunnable);// 启动一个线程thread.start();System.out.println("主线程");}
}
执行效果和上述代码都一致
同样可以使用匿名内部类的方式:
public class Test4 {public static void main(String[] args) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("一个新的thread线程");}} );// 启动一个线程thread.start();System.out.println("主线程");}
}
执行效果和上述代码都一致
上面两种方式哪种更好呢?
Thread类 中还提供了很多方法可以被子类重写, 但我们创建线程, 需要重写的只有 run 方法, 所以当我们不需要重写其他方法时, 最好是实现 Runnable 接口来创建线程
同时, Runnable 接口是一个函数式接口, 这个接口中只提供了 run 方法, 所以实现 Runnable 接口来创建线程时, 更推荐的写法是 lambda 表达式
对 lambda表达式 不太熟悉的读者可以看看 介绍lambda表达式语法及使用方式 的这篇文章
使用 lambda表达式 的写法 :
public static void main(String[] args) {// lambda表达式Thread thread = new Thread( () -> {System.out.println("一个新的thread线程");});// 启动一个线程thread.start();System.out.println("主线程");}
由于主线程和 thread 线程中都只有一句打印操作, 所以我们不太能感受到多线程的效果, 接下来我们对上述代码做出如下修改 :
1, run 方法中循环打印 10 次, 每打印一次休眠1秒
2, main 方法中循环打印 10 次, 每打印一次休眠1秒
Thread 类中提供了一个 sleep 静态方法, 参数设置为1000, 表示休眠 1 秒再继续执行线程
可能会导致 InterruptedException 这个受查异常(编译时异常), 表示线程在休眠时提前中断(唤醒), 需要用 try-catch 捕获, 否则编译无法通过
sleep 方法会在下一篇文章 [介绍Thread类的常用方法] 中再做说明
public static void main(String[] args) {Thread thread = new Thread( () -> {for (int i = 0; i < 10; i++) {System.out.println("一个新的thread线程");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});// 启动 thread 线程thread.start();for (int i = 0; i < 10; i++) {System.out.println("主线程");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
可以看到, 程序会 “交替” 执行两条打印操作, 因为主线程和 thread 线程是并发执行的
并且没有固定顺序先打印谁, 再打印谁, 这也就说明了, 多线程在并发执行时, 操作系统对线程的调度是无序的, 随机的
尽管会有 “优先级” 这一说, 但只是理论上会优先调度, 并不是绝对
如果把上述代码中的 start 方法改成 run 方法, 我们程序员来调用 run 方法也是可以的, 但这样就不能创建出 thread 线程了, 这种状态下就是单线程了
在单线程这个状态下, 就会先执行完 run 方法里的循环打印, 才执行第二个循环打印
以上就是本篇的全部内容, 主要介绍了
1️⃣什么是线程: 是进程里的一个执行流
2️⃣进程和线程的区别
如何用代码创建线程
两个重要概念 :
1️⃣进程包含线程, 一个进程里有一个或多个线程
2️⃣线程是操作系统进行调度执行的基本单位
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~
上山总比下山辛苦
下篇文章见