线程 & 多线程
Posted 一朵花花
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程 & 多线程相关的知识,希望对你有一定的参考价值。
线程
线程概念
所谓的"线程",可以理解成轻量级"进程",也是一种实现并发编程的方式
如果把一个进程,想象成是一个工厂,线程就是工厂中的若干个流水线
为啥要有线程?
可以实现并发编程
- 单核 CPU 的发展遇到了瓶颈,要想提高算力,就需要多核 CPU,而并发编程能更充分利用多核 CPU 资源
- 有些任务场景需要 “等待 IO”,为了让等待 IO 的时间能够去做一些其他的工作,也需要用到并发编程
- 线程之间共享的资源多,完成一些操作时更方便
线程比进程更轻量
- 创建一个线程,比创建一个进程成本低
销毁一个线程,比销毁一个进程成本也低
(成本低的原因:新创建一个线程,不需要给这个线程分配很多的资源
如果新建一个进程,就需要给这个进程分配较多的资源) - 创建线程比创建进程更快
销毁线程比销毁进程更快
调度线程比调度进程更快
实际进行并发编程的时候,多线程方式要比多进程方式更常见,也效率更高
线程和进程的联系和区别
联系:
- 线程其实是包含在进程中的 (每个进程至少有一个线程存在,即主线程)
- 一个进程中可能会有多个线程
- 每个线程都有一段自己要执行的逻辑 / 指令
(每个线程都是一个独立的"执行流") - 同一个进程中的很多线程之间,共享了一些资源
同一个进程的多个线程之间共享的资源主要是两方面:
1.内存资源(两个不同进程之间,内存不能共享)
2.打开的文件
也有一些不是共享的资源:
1.上下文 / 状态 / 记账信息 / 优先级(每个进程要独立的参与 CPU 的调度)
2.内存中有一块特殊的区域:栈 (每个线程要独立一份)
区别:
-
进程是操作系统分配资源的最小单位
线程是造作系统进行调度和执行的最小单位 (所谓的操作系统进行进程调度,本质上就是操作系统针对这个进程的若干个线程进行调度) -
当创建一个进程的时候,就会自动随之创建一个线程(主线程)
一个进程被创建出来的同时,至少会随之创建一个线程 (主线程) -
进程和进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间
线程管理
本质上和管理进程一样,先用PCB描述,再使用双向链表来组织
内核只认 PCB
即:一个线程和一个 PCB 对应,而一个进程可能和多个 PCB 对应
上述逻辑是在 Linux 中的实现方式
多线程
吃鸡例子: 滑稽吃100只鸡
思考:线程数量是越多越好吗?
不是,因为线程的调度是有开销的,随着线程数量的增多,线程调度的开销也就越大
线程数量太多,非但不会提高效率,反而会降低效率
那么:一个进程中,最多能有多少个线程呢?
- 和 CPU 个数有关
- 和线程执行的任务的类型也相关
a) CPU 密集型:程序一直在执行计算任务
b) IO 密集型:程序没咋进行计算,主要是进行输入输出操作
假设这个主机有 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 相关的内容,这就意味着,很容易把这个逻辑从多线程中剥离出来,去搭配其他的并发编程的方式来执行,当然也可以很容易的改成不并发的方式执行
以上是关于线程 & 多线程的主要内容,如果未能解决你的问题,请参考以下文章