线程(thread):
- 线程其实是包含在进程中的
- 一个进程中可能有多个线程
- 每个线程都有一段自己要执行的指令,每个线程都是一个独立的"执行流"
- 同一个进程中的很多线程之间,是共享了一些资源
如果把一个进程想象成是一个工厂,那么线程就是工厂中的若干个流水线,
所谓的线程可以理解成是一种轻量级的进程,也是一种实现并发编程的方式
- 创建一个线程比创建一个进程成本低
- 销毁一个线程比销毁一个进程成本也低
成本低的原因是,新创建一个线程,不需要给这个县城分配很多资源,如果新创建一个进程那么就需要分配较多的资源
实际进行并发编程的时候,多线程方式要比多进程方式更常见,也效率更高.
同一个进程的多个线程之间共享的资源主要是两方面
- 内存资源(但是两个不同进程之间的内存不能共享)
- 打开的文件
也有不共享的资源.
- 上下文/优先级/记账信息(每个县城要独立参与CPU的调度)
- 内存中有一块特殊的区域 栈 空间是每个线程都要独立一份
当创建一个进程的时候,就会自动随之创建一个线程(主线程)
一个进程被创建出来的同时,至少会随之创建一个线程
进程是操作系统分配资源的最小单位
线程是操作系统进行调度和执行的最小单位
所谓的操作系统进程调度,本质上就是操作系统针对这个进程的若干个线程进行调度
操作系统如何管理线程呢?
本质上和管理进程一样.
先描述:用PCB描述
再组织:用一个双向链表来组织
内核只认PCB,一个线程和一个PCB对应,一个进程可能和多个PCB对应
内核中把若干个从属于同一个进程的线程称为线程组
线程过多时,并发能力就达到上限了,继续增加线程不会增加效率,甚至反而降低效率(线程调度是由开销的)
一个进程中最多能搞多少个线程呢
- 和CPU个数相关
- 和线程执行的任务类型也相关
1.CPU密集型:程序一直在执行计算任务
2.IO密集型:程序没咋进行计算,主要是进行输入输出.
如果一个主机有8核CPU(两种极端情况)
如果是纯CPU计算,此时线程的数目大概是8个左右
如果任务是纯IO密集型,理论上搞多少线程都可以
现实中的情况是介于两者之间 一般通过测试的方式来找到合适的线程数
多线程程序,还是有一些问题的
涉及到线程不安全的问题
一个线程如果抛出异常,并且没有很好地处理这个异常,那么就会终止整个进程.
代码部分:
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("hello world, 我是一个线程");
while (true) {
}
}
}
public static void main(String[] args) {
// 创建线程需要使用 Thread 类, 来创建一个 Thread 的实例.
// 另一方面还需要给这个线程指定, 要执行哪些指令/代码.
// 指定指令的方式有很多种方式, 此处先用一种简单的, 直接继承 Thread 类,
// 重写 Thread 类中的 run 方法.
// [注意!] 当 Thread 对象被创建出来的时候, 内核中并没有随之产生一个线程(PCB).
Thread t = new MyThread();
// 执行这个 start 方法, 才是真的创建出了一个线程.
// 此时内核中才随之出现了一个 PCB, 这个 PCB 就会对应让 CPU 来执行该线程的代码. (上面的 run 方法中的逻辑)
t.start();
while (true) {
}
}
在上面的代码中,涉及到两个线程,MyThread创建出来的线程,main方法对应的线程
为了进一步的观察当前确实是两个线程,可以借助第三方的工具来查看进程中的线程情况
JDK中内置了jconsole这样的程序,必须要想办法让进程结束别那么快,才能看到线程
写一个简单的代码来演示一下:
多线程并发执行和单线程之间的对比,针对一个整数进行大量的循环相加.
时间戳:以1970年1月1日0时0分0秒为基准时刻,计算当前时刻和基准时刻的之间的秒数/毫秒数/微秒数之差
private static long count = 100_0000_0000L;
public static void main(String[] args) {
// serial();
concurrency();
}
//串行
private static void serial() {
long beg = 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 - beg) + " ms");
}
//多线程
private static void concurrency() {
long beg = System.currentTimeMillis();
Thread t1 = new Thread() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
}
};
//匿名内部类,创建了一个没有名字的类,只知道这个类继承自Thread ,{} 中是该类的具体代码,同时也会new出该类的一个实例
Thread t2 = new Thread() {
@Override
public void run() {
int b = 0;
for (long i = 0; i < count; i++) {
b++;
}
}
};
t1.start();
t2.start();
try {
// 线程等待. 让主线程等待 t1 和 t2 执行结束, 然后再继续往下执行.
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// t1 t2 和 main 线程之间都是并发执行的.
// 调用了 t1.start 和 t2.start 之后, 两个新线程正在紧锣密鼓的进行计算过程中,
// 此时主线程仍然会继续执行, 下面的 end 就随之被计算了.
// 正确的做法应该是要保证 t1 和 t2 都计算完毕, 再来计算这个 end 的时间戳.
long end = System.currentTimeMillis();
System.out.println("time: " + (end - beg) + " ms");
}
serial() 运行时间: 8075ms
concurrency () 运行时间: 3684ms
线程数量多了之后,效率是会提高,但是不一定会成倍提高.因为受到影响的因素可能很多
比如线程创建和销毁也是会消耗时间的,线程多了以后,线程的调度也会需要时间的.
单线程的代码编译器更好进行优化
创建线程的几种代码写法:
- 通过继承Thread类的方式来实现
- 通过匿名内部类的方式继承Thread类
- 显示创建一个类,实现Runnable接口,然后把这个Runnable的实例关联到Thread实例上
- 通过匿名内部类来实现Runnable接口
- 使用Lambda表达式
3
public class Test {
// Runnable本身就描述了一段要执行的代码
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我是个线程");
}
}
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
4
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("我是一个线程");
}
};
Thread t = new Thread(r);
t.start();
}
5
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("我是一个新线程");
});
t.start();
}