所谓的"线程",可以理解成轻量级"进程",也是一种实现并发编程的方式
如果把一个进程,想象成是一个工厂,线程就是工厂中的若干个流水线
可以实现并发编程
线程比进程更轻量
实际进行并发编程的时候,多线程方式要比多进程方式更常见,也效率更高
联系:
同一个进程的多个线程之间共享的资源主要是两方面:
1.内存资源(两个不同进程之间,内存不能共享)
2.打开的文件
也有一些不是共享的资源:
1.上下文 / 状态 / 记账信息 / 优先级(每个进程要独立的参与 CPU 的调度)
2.内存中有一块特殊的区域:栈 (每个线程要独立一份)
区别:
本质上和管理进程一样,先用PCB描述,再使用双向链表来组织
内核只认 PCB
即:一个线程和一个 PCB 对应,而一个进程可能和多个 PCB 对应
上述逻辑是在 Linux 中的实现方式
吃鸡例子: 滑稽吃100只鸡
思考:线程数量是越多越好吗?
不是,因为线程的调度是有开销的,随着线程数量的增多,线程调度的开销也就越大
线程数量太多,非但不会提高效率,反而会降低效率
那么:一个进程中,最多能有多少个线程呢?
假设这个主机有 8核 CPU (两种极端情况:)
若任务纯是 CPU 密集型的,此时线程的数目大概就是 8 个左右
若任务纯是 IO 密集型的,理论上,有多少个线程多可以
现实中的情况是要介于两者之间,实践中一般需要通过 测试 的方式来找到合适的线程数,让这个程序效率够高,同时系统的压力也不会过大
Java 中如何使用多线程?
标准库中提供了一个类:Thread 类
public class ThreadDemo1 {
static class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello World, 我是一个线程");
}
}
public static void main(String[] args) {
// 创建线程需要使用 Thread 类,来创建一个 Thread 的实例
// 另一方面还需要给这个线程指定 要执行哪些指令/代码
// 指定指令的方式有很多种,此处先用一种简单的,直接继承Thread类,
// 重写 Thread 类中的 run 方法
// 当 Thread 对象被创建出来的时候,内核中并没有随之产生一个线程(PCB)
Thread t = new MyThread();
// 执行这个 start 方法,才是真的创建出一个线程
// 此时内核中才随之出现了一个 PCB,这个 PCB 就会对应让 CPU 来执行该线程的代码(上面的run方法中的逻辑)
t.start();
}
}
输出结果:
为了进一步观察当前确实是俩线程,可以借助第三方工具
JDK 中内置了一个 jconsole
但此时并不能看到线程信息,因为当前进程已经结束了
必须要想办法让进程不要那么快结束,才能看到线程信息
直接安排一个死循环~
public class ThreadDemo1 {
static class MyThread extends Thread{
@Override
public void run() {
// super.run();
System.out.println("Hello World, 我是一个线程");
while (true){
//死循环
}
}
}
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while (true){
// 死循环
}
}
}
输出结果:
此时运行程序,然后双击 jconsole.exe:
多线程并发执行 和 单线程 的对比:
例: 针对一个整数进行大量的循环相加:
private static long count = 100_0000_0000L;
public static void main(String[] args) {
serial(); //串行
// concurrency(); // 并发
}
private static void serial() {
// 获取当前时间戳
long begin = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
int b = 0;
for (long i = 0; i < count; i++) {
b++;
}
long end = System.currentTimeMillis();
System.out.println("time: " + (end - begin) + "ms");
}
输出结果:
即:串行针对两个整数累加100亿次,大概消耗5s
System.currentTimeMillis( ) — 获取到毫秒级的时间戳
时间戳:
以 1970 年 1月1日 0时0分0秒为基准时刻,计算当前时刻和基准时刻之间的秒数 / 毫秒数 / 微秒数 之差
private static long count = 100_0000_0000L;
public static void main(String[] args) {
// serial(); //串行
concurrency(); // 并发
}
private static void concurrency() {
long begin = System.currentTimeMillis();
// 匿名内部类
Thread t1 = new Thread(){
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
int b = 0;
for (long i = 0; i < count; i++) {
b++;
}
}
};
// 启动线程
t1.start();
t2.start();
try {
// 线程等待,让main 线程等待 t1和t2 执行结束,然后再继续往下执行
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// t1,t2,和 main 线程之间都是并发执行的
// 调用了 t1.start 和 t2.start之后,两个新线程正在忙着进行计算
// 此时 main线程仍然会继续执行,下面的 end 也就会被随之计算了
// 正确做法: 应该是 t1 t2计算完毕后,再来计算 end 的时间戳
long end = System.currentTimeMillis();
System.out.println("time: " + (end - begin) + "ms");
}
输出结果:
两个线程并发执行的时候,时间大概是 2.7s 左右,时间缩短了很多~
1.通过显示继承一个 Thread 类的方式来实现
2.通过匿名内部类的方式继承 Thread 类
3.显式创建一个类,实现 Runnable 接口;然后把 Runnable实例 关联到一个 Thread 实例上
4.通过匿名内部类的方式,实现Runnable
5.使用 lambda 表达式,来指定线程执行的内容
代码示例:
public class ThreadDemo3 {
// Runnable 本质上就是描述了 一段要执行的任务代码是啥
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我是一个新线程~");
}
}
public static void main(String[] args) {
// 1.显式继承 Thread
// 2.通过匿名内部类的方式,继承Thread来创建线程
// Thread t = new Thread(){
// @Override
// public void run() {
//
// }
// };
// t.start();
// 3.显式创建一个类,实现 Runnable 接口
// 然后把 Runnable实例 关联到一个 Thread 实例上
// Thread t = new Thread(new MyRunnable());
// t.start();
// 4.通过匿名内部类的方式,实现Runnable
// Runnable runnable = new Runnable() {
// @Override
// public void run() {
// System.out.println("我是一个新线程~~");
// }
// };
// Thread t2 = new Thread(runnable);
// t2.start();
// 5.使用 lambda 表达式,来指定线程执行的内容
Thread t = new Thread(()->{
System.out.println("我是一个新线程~~~");
});
t.start();
}
}
无论是哪种方式,没有本质上的区别 (站在操作系统的角度),核心都是依靠Thread类,只不过指定线程执行的任务的方式有所差异
细节上有点差别(站在代码耦合性角度):
通过 Runnable / lambda 的方式来创建线程 和 继承 Thread 类相比,代码耦合性要更小一些,在写 Runnable / lambda 的时候 run 中没有涉及到任何 Thread 相关的内容,这就意味着,很容易把这个逻辑从多线程中剥离出来,去搭配其他的并发编程的方式来执行,当然也可以很容易的改成不并发的方式执行
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_47988201/article/details/121046692
内容来源于网络,如有侵权,请联系作者删除!