目录
目录
当代CPU都是多核心的,普通的一段代码一般是在一个核心上完成的,无论如何优化这段代码仅仅也是利用到CPU的一个核心,就算这个核心跑满了,其他核心也只能干瞪眼看着。
并发编程:
通过写特殊的代码把多个核心都充分利用起来这样的编程就叫做”并发编程”。
多进程编程就是一种典型的并发编程。
多进程编程虽然能够解决问题,但是效率方面却不理想,多进程编程有个特点就是太“沉重”,
每次创建与销毁进程都有比较大的时间与空间的开销。在应用场景中开销十分明显。
最典型的列子就是服务器开发,针对每个客户端的请求,都要创建一个单独的进程,这个进程为客户端提供服务。
线程:
为了解决这个问题发明了“线程”。线程可以看作是轻量的进程,可以解决并发编程的的问题,开销也更加小。
多线程是经常会用到的东西,也是面试重点考察的问题。
进程在Linux系统中是通过PCB这样的结构体来描述,通过链表的形式组织的。线程在Linux系统中同样也是以PCB结构体描述的。
线程与进程的关系
进程可以近似看成一组PCB,而一个线程可以看作一个PCB
一个进程里面可以包含多个线程但是不能没有线程。因为一个进程创建后默认里面包含一个线程。
此时线程可以独立在CPU上进行调度执行。
进程可以看作为系统“资源分配”的最小基本单位
线程可以看作为系统”执行调度“的最小基本单位。
所以一个可执行的程序在双击运行后,操作系统就会创建进程进行各种资源的分配(内存,硬盘,网络带宽....)同时也会在这个进程中创建一个或者多个线程,这些线程会到CPU上执行。(一个进程的线程数一般不超过CPU核心数)。每个线程都有自己的状态,优先级,上下文,进账信息等每个线程都会到CPU上独立执行。
线程为什么会比进程更轻?
因为线程的创建省去了”分配资源“,线程的销毁省去了”释放资源“的过程。
引入多线程方案就可以大大提高效率
进程:这里有一百个螺丝需要去打造完成,此时只有一个工人。多线程就是多多聘请几位工人共同去完成这个100个螺丝制造。
但是如果工人的数目超过了超过最大用人标准就是发生工资发不下来的情况。线程的数目超过了CPU逻辑处理的数目就也就是会发生竞争CPU资源。容易导致线程发生崩溃,一个崩溃也会顺带其他的崩溃。因为线程共用一个内存资源。
start和run方法的区别
作用功能不同:1.run方法的作用具体是描述线程执行的任务
2.start方法的作用是在真正的去操作系统内核申请一个线程。
运行结果不同:1.run方法是一个类中的普通方法,主动调用和普通调用一样,
只会顺序自行一次。
2.start调用方法后。start方法内部会调用java系统本地方法(封装了对系统底层的调用)真正启动线程(于是程序中多出一个代码),并且执行run方法中的代码,run执行完后线程进入销毁阶段。
进程和线程的概念与区别
1.进程中至少包含一个线程,也可以有多个线程。
2.线程是操作系统进行调度执行的基本单位。
进程是操作系统进行资源分配的基本单位。
3.同一个进程的线程之间占用同一份系统资源(内存,硬盘,网络带宽)
尤其是对于”内存资源“,就是代码中修改的变量或者对象是同一份,容易造成重复修改。
4.线程是当下并发编程的主流方式,通过多线程就可以很好的利用CPU的多核心,但是并不是线程数目越多越好,当线程数目充分把CPU多个核心都利用起来时,这个时候再增加线程数反而可能会效率降低。(线程的调度也是有开销的)
5.多个线程之间,可能会相互影响。此时就容易发生线程安全问题,一个线程抛出异常就有可能会把其他程序一起带走,全部崩溃。
6.多个进程之间,一般不会相互影响。一个进程崩溃一般不会影响其他的进程正常进行。
线程创建的五种常用方式
1.继承Thread,重写run方法。通过Thread实列start启动线程。
Thread是标准库自带的类,负责完成多线程的相关开发。可以直接使用不需要导入任何包
本代码包含两个线程,一个是主线程mian,一个是普通线程t.start()
mian中的代码执行到t.start()时,创建出普通线程,两个线程并发执行。
main打印“hello main”,普通线程打印“hello Thread”;每个线程隔1秒执行一次
class MyThread extends Thread{ //主要目的不是继承而是为了重写run()
@Override
public void run() {//TODO:线程的入口,逻辑实现
while (true){
System.out.println("helllo Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread t = new MyThread();
//TODO:创建线程
t.start();
while (true){
System.out.println("hello main");
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.实现Runnable,重写run。通过Thread的实例,把Runnable的实例传入进去,再调用start。
Runnable用来描述要执行的任务是什么,通过Thread创建线程,通过Runnable来描述,不是通过Thread自己描述。
有人认为Runnable更有利于解耦合是代码更加集中。
Runnable只是描述任务的载体,并不是与线程概念强相关的,后续这个Runnable可以描述其他任务,比如线程池,虚拟线程等等。
//TODO:继承Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
//描述线程的逻辑
while(true){
System.out.println("hello Thread");
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
// e.printStackTrace();
throw new RuntimeException(e);
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
//将myRunnable整体放入
Thread t = new Thread(myRunnable);
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
3.本质与1一样,通过匿名内部类的方式来编写
继承Thread,重写run,但是通过匿名内部类(定义在其它类里面的并且没有类名)来实现
public class Demo3 {
public static void main(String[] args) {
//TODO:匿名内部类
Thread thread= new Thread(){
@Override
public void run() {
while (true){
System.out.println("hello Thread");
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
thread.start();
while (true){
System.out.println("hello main");
try{
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.本质与2一样,通过匿名内部类实现
创建Runnable的匿名内部类,重写run方法,把子类的实例赋值给Thread的对象的引用
public class Demo4 {
//Runnable的匿名内部类实现
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//把创建的子类赋值给t引用
Thread t = new Thread(runnable);
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
5.基于对lambda表达式来创建线程
lambda本质就是匿名内部类的更简化写法,很多时候匿名内部类目的不是写“类”,而是写类的方法。lambda就是为了写run的方法。
创建Thread的匿名内部类,调用start在内核创建线程。
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (true){
System.out.println("hello thread");
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
介绍Thread类与属性
Thread类时JVM用来管理线程用的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联。
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象并命名 |
Thread(ThreadGroup group,Runnable target) | 这个线程用于分组管理,分好的组即为线程组 |
给线程起名字还是非常方便的,虽然不能影响线程的执行效率,但是可以更加方便地调试代码。
线程创建的时候默认的名字是Thread-0,Thread-1.......
Thread(ThreadGroup group,Runnable target),ThreadGroup线程组,把多个线程放到线程组里方便设置统一属性以及管理。现在很少使用线程组,多用线程池。
如果把t.start()放在最后
ID,名称,状态和优先级都是JVM自动分配的不能手动设置。
isAlive()为true表示内核中存在此线程,为false则线程已经在内核中结束无了。
一般地,一个Thread对象对应一个线程(PCB),也可能Thread对象存在但是系统内部的线程还没有创建。代码中创建的Thread对象,生命周期和内核中实际存在的情况不一定是一样的。
1.调用start之前,内核中还没有创建线程为 false
2.线程的run执行完毕,内核的线程就完毕了,但是Thread对象依然存在。
getState(),java线程的状态不只有阻塞和就绪,还有很多种状态.......
getPriority()设置不同的优先级会影响系统的调度但只是概率上的,需要大量数据才观察到。
前台线程与后台线程
后台线程:在线程执行的过程中,不能阻止进程结束(虽然本身还有很多未执行,但是进程结束了也会被强制结束)
前台线程:如果某个线程在执行过程中能够阻止进程的结束,这个就是前台进程。
在与客人吃饭时,就算吃饱了也不能随意离开,就算是我没吃饱但是客人吃饱了要走,那我也要起身离开。(后台线程)
客人没有吃饱,就要等着客人吃饱,其他人都不能走,只有客人吃宝饱啦大家才能走。(前台线程)。
下面代码:t在没有被修改为后台线程之前是前台线程,程序会一直执行此线程代码直到结束,修改为后台线程后一下子就结束了。因为主线程没有什么逻辑要去执行,而t线程并不能影响主线程的结束,所以会一瞬间就结束。
区别:前台线程与后台线程除了影响进程的结束其他没有什么太大区别。线程创建出来默认就是前台线程。
经典面试题:start()与run()区别
run()描述了一个线程执行的任务和逻辑,也可以称为现成的入口。
start()调用系统相应的API在操作系统内核中去实现一个线程的创建(创建PCB,加入链表)
在系统中创建好线程之后,随着程序的运行再去执行run()。
start()的执行速度是非常快的(创建的线程更轻量),一旦start执行完毕系统内部就会有两个线程一起执行。
调用start,不一定非得是main方法,也可以是其他线程,也就是说线程里也可以创建别的线程。
一个对象Thread只能调用一次start,如果调用多次会报错。
线程的终止(结束)
B正在运行,A想让B结束,核心就是让B的run方法执行完毕,此时B就自然结束了。
而不是让B执行一半,直接强制把B强制结束.
举例一:
如果将isQuit改为main方法中的局部变量行不行?
图二代码编译错误,此时会发生变量捕获(是lambda表达式/匿名内部类的一个语法规则)
如图一:isQuit和lambda定义在一个作用域中,此时lambda内部是可以访问到lambda外部(和lambda同一个作用域的)中的变量的。写成成员变量,此时isQuit是父类的成员变量,内部类访问父类成员变量是天经地义的事情,和“变量捕获”就无关了。
图二会报错因为变量捕获有特殊要求,要求变量是final修饰或者是 事实变量(不变的量)
此时isQuit在下面被修改了。
举例二:
Interrupt会设置标志位,是线程的状态发生改变,会唤醒sleep
sleep被唤醒之后又会清空线程的状态/标志位。
为什么不直接t.isInterrupted(),因为这是lambda表达式,在编译器眼里t还没有被定义呢,捕获不到这个t变量,所以只能通过currentThread的方式得到t的线程引用。
图二抛出一个异常RuntimeException,由于判定isterrupted()和执行打印这两个操作的时间太快了,所以该线程的时间大部分停留在sleep上。
两个线程同时进行,在main中调用interrupt设置标志位,还会会把sleep刚刚沉睡的时间唤醒,抛出InterruptedException异常,由于catch中默认抛出会RuntimeException异常。
由于sleep会清空设置的interrupted标志位,所以while会继续循环。就像没有被设置的一样。
因此,想要结束循环就要在catch后面设置return/back
总结:1.A线程想要中断B线程,B线程想要无视就直接在catch中啥都不做。
2.B线程想要立即被结束就在catch中写return/back
3.B想要稍后结束就可以在catch中写点别的最后的程序,执行完成之后在进行return/back