一、引用线程的原因
多任务操作系统,希望系统能同时运行多个任务。所以会涉及到进程,需要对进程进行管理、调度等。
而单任务操作系统,就完全不涉及到进程,也不需要管理、调度了。
而进程,就是解决并发编程的这样问题,事实上,进程也能解决大部分并发编程的问题(Java不提倡多进程编程)。但有些情况就很乏力了,如下:
网站 / Web开发,是一种服务器程序,我们知道,一个网站服务器在同一时刻,会受到很多请求,针对这些请求,会创建进程,一个请求创建一个进程,创建完一个进程,又要销毁这个进程,这意味着这个网站服务器要频繁的创建和释放资源,这个操作开销是比较大的,
原因:
我们知道,进程是资源(CPU,硬盘,内存,网络带宽)分配的基本单位,而一个进程,刚启动的时候,首当其冲的就是申请内存资源,因为进程需要把依赖的代码 / 数据,从磁盘加载到内存中。
而从系统分配一个内存,并非是件容易的事,一般来说,申请内存的时候需要指定一个大小,系统内部就要把各自大小的空闲内存,通过一定的数据结构,给组织起来,实际申请的时候,就需要去这样的空间中进行查找,找到一个大小合适的空闲内存,进行分配。
结论:进程在创建和销毁的时候,开销比较大,主要体现在申请和释放资源上。
这时,我们就引入线程,来解决开销比较大的问题。
二、线程的概念
线程也可以理解成“轻量级进程”,基于进程做的一些改进和调整,使其变得开销(资源的申请和释放)不那么大。
因为进程的独立性,一个进程在内存中申请一块资源时,那个块资源只能让那个进程使用,其他的进程不能使用。而一个线程在内存中申请了一块资源,其他不同的线程也可以使用这块资源,这样就避免了多次的资源申请和释放。PCB可以表示进程,也可以表示线程。
进程在内存中的使用范围,如图
PCB有个属性,是内存指针
多线程的PCB,也有内存指针,但可以指的是同一块内存空间,以及进程有的pid、状态、
上下文、优先级等,线程也有。
三、进程和线程的区别
1、进程包含线程,进程可以理解成多个线程的组合,这些线程称为线程组。
关系图如下:
2、进程扮演的角色是申请内存空间,而线程扮演的角色是调度数据 / 执行代码。
3、1个进程至少有1个线程,每个进程有自己的资源空间,而进程里的线程共用这块资源空间。
4、进程和进程之间不会相互影响,但是进程中的某个线程出问题了,可能会影响到这个进程中的其他线程,导致这个进程也出问题。
5、同一个进程中的线程之间,可能会相互干扰,引起线程安全问题。
6、线程不是越多越好,应该要合适,如果线程太多了,调度开销可能非常明显。
四、操作系统的内核
内核,是操作系统最核心的功能模块,操作系统 = 内核 + 配套的应用程序,操作系统大概可以分为两个模块,用户空间(用户态),内核空间(内核态),操作系统理解为银行,银行大概分为两个区域,如图:
办事窗口里面的区域是做一些比较重要的事,一般人都在大堂,只能在大堂里活动,办事窗口里面能做一些核心,重要的事,外面的人也不能闯进来,这时,我们可以理解为内核空间是办事窗口,用户空间是大堂,如图:
那如果用户空间里的程序,想要使用内核资源,需要针对系统内的软硬件资源进行操作,要怎么办呢,这时,Windows操作系统就站出来了,提供系统的api给这些程序使用,调用内核资源,进一步在内核中完成这样的操作。
为什么要划分出内核空间(内核态)和用户空间(用户态)呢?
我们想想,如果一个应用程序可以直接调用硬件资源,如果出现一个bug了呢?不直接把硬件给干烧了,所以,划分出这两模块,主要目的还是为稳定,系统会封装一些api,这些api都是安全的操作,供应用程序使用,就不至于对系统 / 硬件设备造成危害了。
五、多线程编程
1、线程创建的几种方式
(1)、继承Thread类,重写run
一个简单的线程创建,代码如下:
class MyThread extends Thread { @Override public void run() { //run方法是该线程的入口方法 System.out.println("Hello World"); } } public class SystemCode { public static void main(String[] args) { //2、根据刚才的类,创建一个实例 Thread t = new MyThread(); //3、调用Thread的start方法,才会真正调用系统的api,在系统内核中创建线程 t.start(); } }
执行结果:
(2)、实现Runneble接口,重写run
代码如下:
class MyThread implements Runnable { @Override public void run() { while (true) { System.out.println("Hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class ThreadDemo1 { public static void main(String[] args) { Thread t = new Thread(new MyThread()); t.start(); while (true) { System.out.println("Hello main"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
执行效果:
要怎么查看进程的情况呢?
1、使用jdk自带的jconsole.exe文件。
看到这里有这么多的线程,我们发现,其实一个java的进程,包含的线程也挺多的。
这里除了main线程和我们自己创建的线程,其余的线程,都是 jvm 自带的线程,用来完成垃圾回收(gc自动帮你释放内存),监控统计各种指标(如果我们的代码出问题了,这些指标就可以给我们提供一些参考和线索),把统计指标通过网络的方式,传输给其他程序。
堆栈跟踪信息:是很有意义的,描述了线程的调用栈,线程里当前执行道路哪个方法的第几行代码了,这个方法是怎么一层一层调用过去的。
和我们所写的代码行序是对应的
2、使用idea查看线程
debug调试我们的代码,然后打断点到这:
这有个下拉窗,我们就可以看到现有的线程了:
(3)继承Thread类,重写run,但使用匿名内部类
代码:
public class ThreadDemo2 { public static void main(String[] args) { Thread t = new Thread() { @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(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
执行效果和上面的一样
说明:内部类的最大用途,也就是匿名内部类,没有名字,意味着不能重复使用,用一次就不要了。new Thread()后面 { }里的意思是要定义一个类,这个类继承自Thread,此处的 { } 中可以定义子类的属性和方法,此处的 { } 最主要目的就是重写run方法。
这个代码还创造出了子类的示例
这里的 t 指向的实例,并非单纯的Thread,而是Thread的子类,因为我们不知道这个子类叫啥,因为是匿名的。
(4)、实现Runneble接口,重写run,使用内部类
代码:
public class ThreadDemo3 { 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(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
注意:匿名内部类是在()里面的。
(5)使用lambada(推荐)
代码:
public class ThreadDemo4 { 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); } } } }
注意:lambda表达式来代替功能接口,所以要代替的是实现接口的功能,重写的是run方法,所以要写在Thread()的括号里面。
六、star()和run()的区别
看到这,是不是觉得对这两个区别优点疑惑?但事实上,这两个概念是毫不相干的两个东西,创建出来的线程实例,只有调用 star,才会真正调用系统的api,在系统内核创建出线程;而 run 是线程执行的入口。这里,就有老铁疑惑了,我用 run 也可以调用这个重写的方法啊,和用 start 一样,哎,这就不对了,举个以下例子:
两个代码分别单独执行star 和 run
代码:
run:
class MyThread extends Thread { @Override public void run() { while (true) { System.out.println("这是我的线程,正在工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class ThreadDemo4 { public static void main(String[] args) { Thread t = new MyThread(); t.run(); //t.start(); } }
start:
class MyThread extends Thread { @Override public void run() { while (true) { System.out.println("这是我的线程,正在工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class ThreadDemo4 { public static void main(String[] args) { Thread t = new MyThread(); //t.run(); t.start(); } }
执行效果:
这两代码的执行效果是一样的,但我们现在改一下,如下:
run:
class MyThread extends Thread { @Override public void run() { while (true) { System.out.println("这是我的线程,正在工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class ThreadDemo4 { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.run(); //t.start(); while (true) { System.out.println("这是主线程,正在工作"); Thread.sleep(1000); } } }
start:
class MyThread extends Thread { @Override public void run() { while (true) { System.out.println("这是我的线程,正在工作"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class ThreadDemo4 { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); //t.run(); t.start(); while (true) { System.out.println("这是主线程,正在工作"); Thread.sleep(1000); } } }
分别执行效果:
run:
start:
可以看到不同了吧,这是因为star之后,这里有两个线程(t 线程和主线程),他们都是各自调度各自的线程,就有两个循环在执行了,但是如果使用 run 的话就只有一个线程了,只在一个循环里执行,所以之后在当前方法里面循环,不会执行下面的循环。