线程的诞生
了解进程存在的意义
实现了并发编程的效果(并发编程:有可能是并发执行,也有可能是并行执行)
并发编程的目的:充分利用上多核CPU资源,提升运行效率
了解进程创建和销毁的过程带来的问题
进程是如何创建的:
1、创建PCB
2、给进程分配资源(内存/文件)赋值到PCB中
3、将PCB插入到链表中。
进程是如何销毁的:
1、把PCB从链表从删除;
2、把PCB中持有的资源释放
3、销毁PCB;
虽然多进程已经实现了并发编程,但是存在重要的问题:
如果频繁的创建/销毁进程,这个操作就比较低效。
为什么这么说?
分配资源这个事情,对操作系统来说是一个比较大的事情,非常消耗时间
因为上述原因聪明的程序员发明了“线程”;
认识线程
线程是包含在进程中的。
一个进程默认会有一个线程(主线程),当然也可以有多个线程;
每个线程都是一个“执行流”可以单独在CPU上进行调度;
同一个进程中的这些线程,共用同一份系统资源(内存 + 文件)。
Tips:一个进程至少有一个线程,也可以有多个;
线程 : 可以理解为"轻量级进程“
优势:1、创建线程的开销比进程小
2、销毁线程的开销比销毁进程小
进程和线程的关系
前面有说到过,操作系统内核是通过PCB来描述进程的,更准正确的说法是,一组PCB来描述一个进程,每个PCB对应一个线程。
这一组PCB上的内存指针,和文件描述符表其实是同一份东西,而状态,上下文,优先级,记账信息,则是每个PCB(每个线程)自己有一份
进程是资源分配的基本单位
线程是调度执行的基本单位
使用多线程的目的
1、能够充分利用上多核CPU,能够提高效率
2、只是创建第一个线程的时候,需要申请资源,后续再创建新的线程,都是共用同一份资源(节省了资源申请的开销)销毁线程的时候,也只是销毁到最后一个的时候,才真正释放资 源,前面的线程销毁,都不必真释放资源
面试题:线程和进程之间的区别(谈到操作系统,线程进程的概念和区别必考)
1、进程包含线程
2、线程比进程更加轻量,创建更快,销毁也更快
3、同一个进程下的多个线程之间共用同一份资源(内存 + 文件),进程和进程之间则是独立的内存/文件资源
4、进程是系统分配资源的基本单位;线程是调度执行的基本单位
多线程存在的问题
1、线程数目不是越多越好
例如:临近寒假结束开学之际,小明的寒假作业有7本且一字未动,如果小明自己一个人写7本,是不是需要的时间很久?此时如果有3个人一起帮忙写,那是不是效率一下子就提高了,如果此时增加到7人,每人一本正正好好,那如果此时人有14个人,每个人都很热情想帮小明,这个人写一下那个人想抢过来帮一下忙写一下,此时这样反复的争抢也没有意义,根本没有办法好好写作业。
对应到线程是不是也是这样?线程数目也不是越多越好,CPU核心数是有限的,当线程数目达到一定程度的时候,CPU核心数就已经吃满了!此时继续增加也无法在提高效率,反而会因为线程田铎,线程调度开销太大影响了效率。
2、线程之间会互相影响
7个人7本作业,一人拿一本时候是不是特别合适?如果有两个人同时想拿走同一本他们比较熟悉的作业,那就回抢起来。
1、对比到线程也是一样的,如果两个线程修改同一个变量,也容易产生“线程不安全问题”。
如果7本作业,其他6个人6本都写的好好的,突然有一个人偷懒不想写,那检查作业时候是不是就会因为这一本而被判定没有好好完成作业。
2、对应到线程,如果某个线程运行过程中出现异常,并且异常没有处理好,整个进程都会随之崩溃!整个时候后续其他线程自然难以运行。
创建线程的方式
1、继承Thread类,重写run
解释代码:
Thread类:标准库中提供了一个Thread类,使用的时候 就可以继承整个类 (Thread类相当于Java对操作系统中的线程进行封装)
run方法:重写父类里面的run方法,里面的逻辑就是整个线程要执行的工作~创建子类并且重写run方法,相当于“安排任务“
在main方法中(主线程)创建实例 (
MyThread thread = new MyThread();
)并不会在系统中真正创建一个线程!
调用start()方法的时候,才会真正创建出一个新的线程~
新的线程就会执行run里面的逻辑,直到run里的代码执行完
注意:在这个代码中只有一个进程;main方法相当于这个进程的(默认)主线程,相当于线程的入口方法,我们调用的Mythread的线程相当于第二个线程,run就是这个新线程的入口方法,主线程执行完进程并不会销毁,而是等待所有进程执行完才销毁。
2、实现Runnable接口,重写run
解释代码:
在MyRunnable类里面实现Runnable接口,重写run方法,run方法里面就是这个线程要执行的任务
main方法里面的代码如图所示,解释下面这段代码:
优势1:这样可以解耦合把线程要干的活和线程本身分开了,使用Runnable来专门表示“线程要完成的工作”
把任务提取出来的目的就是为了解耦合,第一种创建线程的写法就把线程要完成的工作和线程本身耦合在一起了。
耦合高是不好的,例如:
未来要对这个代码进行调整,不再使用多线程了,用其他方式,代码改动就比较大,而Runnable这种写法就只需要把Runnable传给新方式的实体即可。
优势2:如果想搞多个线程,都干一样的活,这个时候也适合使用Runnable的。
3、使用匿名内部类,实现创建Thread子类的方式
Thread t = new Thread(){
@Override
public void run() {
System.out.println("线程完成执行的任务");
}
};
以上代码相当于创建了一个匿名的Thread的子类,同时实例化出一个对象。
4、匿名内部类创建实现 Runnable 子类对象
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("该线程执行的任务");
}
});
new Runnable(){}匿名内部类的实例,作为构造方法的参数
5、lambda 表达式创建 Runnable 子类对象
这里的lambda实际上就是第4中方式的简化版。
关于lambda表达式不过多赘述。
面试题:Java中有哪些方式能创建线程?
除了以上五种还有其他的方式后面的文章详细解答。
验证多线程的优势
前面一直说多线程能够充分利用多核CPU,提高程序的效率,利用代码直观的展现出来看看。
使用单线程和多线程的方式分别计算从0到100_0000_0000需要多长时间
1、串行执行:
// 串行执行任务
public static void serial() {
// 记录 ms 级别的时间戳.
long beg = System.currentTimeMillis();
long a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
long end = System.currentTimeMillis();
System.out.println("执行的时间间隔: " + (end - beg) + " ms");
}
2、并发执行
// 并发执行任务
public static void concurrency() throws InterruptedException {
long beg = System.currentTimeMillis();
Thread t1 = new Thread(()->{
long a = 0;
for(int i = 0; i < COUNT; i++) {
a++;
}
});
Thread t2 = new Thread(()->{
long a = 0;
for(int i = 0; i < COUNT; i++) {
a++;
}
});
t1.start();
t2.start();
//等待t1,t2执行完以后再结算结束时间
t1.join();
t2.join();
long end = System.currentTimeMillis();
System.out.println("执行的时间间隔: " + (end - beg) + " ms");
}
为什么要加上 t1.start(); t2.start();两个代码?
原因就是因为main线程,t1,t2线程三个线程是分别独立执行。
当main执行完t1,t2之后,仍然会继续往后走!如果t1,t2还没执行完,就计算结束时间,这是不合理的,所以要让main线程等差t1和t2执行完了才能够停止计时。
join:阻塞等待,在main中调用t1.join效果就是让main线程阻塞一直到t1线程执行完run方法,main才继续执行。
t1和t2是并发执行,而不是先执行完t1才执行t2
串行和并行执行的时间分别为:
结论:
使用两个线程,最终消耗的时间,不一定是一个线程消耗时间的50%;
一个线程串行执行:610ms左右;
两个线程并发执行:369ms左右;
确实两个线程并发执行快了很多,但是不是正好50%;
原因如下:
1、并发执行 = 微观上的并行 + 并发
其中并行执行确实会提高程序的执行速度,并发执行反而会因为一下子执行a线程,一下子执行b线程这样的来回调度,反而因为调度开销让时间增加
问题就在于一次程序运行有多少次是并行执行还有多少次是并发执行我们是不清楚的
2、包括创建线程的实例,也是有开销的.
串行执行,没有额外创建线程
并发执行,额外创建两个线程
总结:
如果计算量大,计算的久,创建线程的开销就更不明显 (忽略不计)
如果计算量小,计算的快,创建线程的开销影响更大,多线程的提升,就更不明显.
这个时候线程的调度/创建销毁都有影响