1.什么是线程
一个线程就是⼀个"执行流",每个线程之间都可以按照顺序执行自己的代码,多个线程之间"同时"执行着多份代码.
2.为什么引入线程(Thread)
当前的cpu都是多核心的cpu,需要通过一些特定的编程技巧(并发编程),把要完成的任务拆解成多个部分,并分别让他们在cpu不同的核心上运行。如果没有进程,就会让任务只在一个核心上运行,会让这个核心负担重,任务完成也不高效,且其他核心是闲着的,那么为什么不利用起来呢?
而"多进程"编程的模式,就可以起到"并发编程"的效果,因为进程可以被调度到不同的 cpu 上运行。
虽然多进程编程可以解决上述问题,但又带来了新的问题。
在服务器开发的圈子里,这种并发编程的需求场景是非常常见。作为一个服务器,要能够同时给多个客户端提供服务,如果同一时间来了很多客户端,但服务器却只能利用一个 cpu 核心工作,速度就会比较慢。
那么如何解决这个问题呢?
把每个客户端连上服务器,服务器都创建一个进程,给客户端提供服务这个客户端断开了,服务器再把进程给释放掉。但是,如果这个服务器频繁的有客户端来来去去,那么服务器就也需要频繁创建/销毁进程。单个一次创建/销毁进程,其实并不会给服务器造成什么太大的负担,就怕频繁的创建销毁进程,这时候服务器的响应速度就会变慢。
而引入线程最主要的目的就是为了解决上述"进程"太重量(创建/销毁 开销比较大)的问题。
线程也称为“轻量级进程”,与进程相比,线程的创建/销毁开销就比较小。
线程,可以理解成"进程的一部分”。一个进程中,可以包含一个线程或者多个线程,使用 PCB 这样的结构体来描述进程。
更详细的说法是 描述一个进程的PCB 是由 若干个描述线程的PCB 联合在一起的。
进程是系统资源分配的基本单位,线程是系统调度执行的基本单位
为什么说线程创建/销毁的开销比进程更小?
是因为创建进程可能要包含多个线程,而这个过程中,涉及到资源分配/资源释放。
但创建线程相当于资源已经有了,省去了资源分配/释放步骤了 。
就前文谈到 客户端—服务器 的问题,每个客户端连上来之后,服务器给他分配一个进程处理,但现在引入线程了,就可以每个客户端给他分配一个线程来处理,就能起到优化效果了。
省去资源分配/释放的步骤指的是什么?
同一个进程包含 N 个线程,这些线程之间是共用资源。只有你创建第一个线程的时候(也是创建进程的时候),才去进行资源申请操作后续再创建线程,后续都没有申请资源的过程。
3.理解多线程
这里举个例子来帮助理解,
比如要 饭店要人在包间消灭掉一百只烤鸡,如果一个人吃,很慢。
1).引入多进程的方案
再找一个人分担,两人同时各吃50只,但需要多开一个包厢,多给一张桌子,这花费也不低。
2).引入多线程的方案
让两个人在同一个包间吃,依然一人五十只鸡,但是不需要额外的空间和桌子,空间成本降低,但吃鸡效率并没有上来。
3).引入更多的线程
也就是多叫些人来一起吃鸡,那么此时吃鸡效率就能上来了,十个人吃一百只鸡就是原来一个人吃一百只效率的10倍。也就是随着线程引入的增多,每个线程要完成的任务量减少了,整体的速度就会更快。
4).能否无线引入线程
吃鸡的人越多,虽然效率越快,但是空间有限,桌子也只有那么大,超出了承载能力,就会出现问题。比如很多人无法凑到桌子边上,没法和正在桌子上的人并发的执行任务,并且外围的人往里挤,就可能把正在吃鸡的人给打断。
线程安全问题:
1.这个过程中,多个人之间是可能会产生冲突的。比如1号和2号同时看上了同一个大鸡腿,但两人互不相让,导致两个人就发生冲突,打起来了,也就是程序出现 bug,即出现了线程安全问题。
2.如果某个人, 想吃的总吃不到,不太开心,然后不想吃了,他直接掀桌!!! 这时候就使别的人也吃不成了。
如果一个线程抛出异常,并且没有很好的捕获处理好,就会使整个进程退出,其他线程也就没有了。
这也多线程编程的值得关注的难点,一个线程出问题,会影响到别的线程。
相比之下,进程和进程之间独立性更好。一个进程挂了,一般不会影响到其他进程。
4.多线程代码的常用写法
1.继承Thread,重写run
class MyThread extends Thread{
public void run(){
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
Thread t=new MyThread();
// 没有创建出新的线程, 就是在主线程中执行上述 run 中的循环打印.
t.start();//hello thread 和 hello main 并发执行
// t.run();//只有 hello thread
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
start 和 run这两个方法的区别是什么呢?
作用功能不同:
- run方法的作用是描述线程具体要执行的任务;
- start方法的作用是真正的去申请系统线程
运行结果不同:
- run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
- start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。
调用start方法: 调用run方法:
2.重写Runnable方法,重写run,把Runnable实例传入到Thread中
class MyRunnable implements Runnable{
public void run(){
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
Thread t=new Thread(new MyRunnable());
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
3.针对 1的变形.使用匿名内部类, 继承 Thread 并重写 run.
public class Demo3 {
public static void main(String[] args) {
Thread t=new Thread(){
public void run(){
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
4.实现 Runnable, 重写 run, 使用匿名内部类
public class Demo4 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
5.使用 lambda 表达式
public class Demo5 {
public static void main(String[] args) {
Thread t=new Thread(() -> {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
上述每种方法执行的结果都是大同小异,就是hello main和hello thread 并发执行,并且是抢占式执行,你也不知道hello thread 的下一句还是hello thread 或者是hello main。