目录
认识线程(Thread)
相关概念
线程的概念
一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间执行多份代码。
线程被称为“轻量级进程”。一个进程可以包含一个或多个线程(且不能没有),同一个进程里的多个线程之间共用进程的同一份资源(这里的资源包括内存、文件描述符表等)。
操作系统在进行实际的调度时,是以线程为单位进行的,如果每个进程有多个线程,每个线程都是独立在CPU上调度的。线程是操作系统调度执行的基本单位。
为什么要有线程
第一“并发编程”称为“刚需”
进入了CPU的多核心时代,CPU再继续做小就变得更加困难,要想进一步的提高程序执行速度,就需要充分利用CPU的多核资源。
有些任务场景需要“等待IO”,为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。
第二虽然多进程也可以 实现并发编程,但是线程比进程更轻量。
进程太“重”,创建、开发、销毁进程的开销都是比较大的,说进程重,主要是重在了“资源分配/回收”上。 所以引入了线程,解决并发编程的前提下让创建、销毁、调度的速度更快。
第三线程虽然比进程轻量,但是人们还是不满足,于是又有了”线程池“(ThreadPool)和”协程“(Coroutine)
暂不记录,后续博客中会有相关内容梳理。
※进程与线程的区别
(1)进程是包含线程的,每个进程至少有一个线程的存在,即主线程。
(2)进程和进程之间不共享内存空间,同一个进程的线程之间共享了进程的同一份资源(主要指的是内存和文件描述符表)。
(3)进程是系统分配资源的最小单位,线程是系统调度的最小单位。
一个线程也是通过一个PCB来描述的,一个进程可能对应一个PCB,也可能对应多个。在前一篇博客中所介绍的PCB里的状态,上下文、优先级、记账信息都是每个线程有自己的,但是在同一个进程里的PCB之间,pid是一样的,内存指针和文件描述符表也是相同的。
Java线程和操作系统线程的关系
线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户提供了一些API供用户使用。Java标准库中的Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装。
创建线程
创建线程的五种方式
(1)继承Thread类,重写run()
(2)实现Runnable接口
(3)使用匿名内部类,继承Thread
(4)使用匿名内部类,实现Runnable
(5)使用lambda表达式
法一:继承Thread类,重写run()
代码展示:
class N_Thread extends Thread{
@Override
public void run() {
System.out.println("hello");
}
}
public class ThreadD1 {
public static void main(String[] args) {
Thread thread =new N_Thread();
thread.start();
}
}
运行结果:
我们在创建的N_ Thread线程上和主线程上同时进行输出,来体会一下同时运行的两个线程,为了更直观的感受,会在两个循环里加入sleep()函数,让结果呈现的速度变慢。
代码展示:
class N_Thread extends Thread{
@Override
public void run() {
while(true){
System.out.println("helloNThread");
try {
Thread.sleep(1000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadD1 {
public static void main(String[] args) {
Thread thread =new N_Thread();
thread.start();
while(true){
System.out.println("主线程");
try {
Thread.sleep(1000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
运行结果:
法一中需要注意的一些问题:
执行的顺序?
抢占式执行,虽然是存在优先级的,但是在应用程序层面上无法进行修改,内核本身并非是随机的,但是因为干预的因素太多,无法感知细节,所以就只能认为是随机的。
start() 与run()
start: 是真正的独立创建了一个线程(从系统这里创建的),线程是独立的执行流。
run:只是描述了线程要做的工作,如果直接在主函数中调用run方法,此时是没创建新的线程,全是主线程自己做。
系统查看进程
通过jdk中自带的工具jconsole可以查看当前java中的所有的进程
重载与重写
重载:Overload.同一作用域,多个方法,名字相同,参数的个数或类型不同,是同一个类里或者是子类和父类之间。
重写:Override 父亲有个方法子类也搞了一个名字相同,参数也完全相同,子类的方法就会被动态绑定的机制,会被调用。
new N_Thread
对象操作不能创建线程(说的线程指的是系统内核中的PCB)
调用start才是创建PCB,才有货真价实的线程。
调用的start会生成一个新线程,新线程去执行run。
法二:实现Runnable接口(解耦合)
Runnable的作用,描述一个”要执行的任务“,run方法就是任务的执行细节。
代码展示:
class MyRunnable2 implements Runnable{
@Override
public void run() {
System.out.println("hello2");
}
}
public class ThreadD2 {
public static void main(String[] args) {
Runnable runnable = new MyRunnable2();
Thread t = new Thread(runnable);
t.start();
}
}
运行结果:
法二中需要注意的问题:
抽象类和普通类的区别:
抽象类不能实例化,不能直接new,必须搞个子类来继承抽象类。抽象类里还有抽象方法,抽象方法没有方法体,需要让子类重写。
法三:使用匿名内部类创建Thread的子类
代码展示:
public class ThreadD3 {
public static void main(String[] args) {
Thread t =new Thread(){
@Override
public void run() {
System.out.println("使用匿名内部类创建Thread的子类");
}
};
t.start();
}
}
运行结果:
法三中需要注意的问题:
(1) 创建了一个Thread类的子类,子类没有名字所以叫做”匿名“;
(2)创建了子类的实例,并且让t引用指向该实例。
法四:使用匿名内部类实现Runnable
代码展示:
public class ThreadD4 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名内部类实现Runnable");
}
});
thread.start();
}
}
输出结果:
本质上和方法2实现Runnable接口相同,只不过把实现Runnable任务交给匿名内部类语法,此处是创建了一个类,实现Runnable,同时创建了类的实例,并且传给了Thread的构造方法。
法五:使用Lambda表达式(简单+推荐)
lambda:匿名函数,用一次即可销毁。
java里面函数没法脱离类存在,java为了和其他语言对齐,弄了个函数式接口,通过这个接口来实现lambda。
代码实现:
public class ThreadD5 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("使用Lambda表达式创建线程");
}
);
t.start();
}
}
输出结果: