线程
线程是包含在进程里面的一个“执行流”,每一个线程之间按照顺序执行自己的代码,多个线程之间“同时”运行各自的代码。这样就能够把一个大的任务分成不同的小任务呢,交给不同的执行流分别执行。其中,一个线程运行后执行了另外的线程,那么这个线程一般被称为“主线程”。
进程和线程之间的区别
(1)进程是包含线程的,线程是在进程内部的,每一个进程至少有一个线程,即主线程;
(2)每一个进程有自己独立的虚拟地址空间,说明进程之间的资源是独立的(进程的独立性),也有自己的独立的文件描述符表;同一个进程的多个线程之间,共用一份虚拟地址空间和文件描述符表,线程之间的资源是共享的,不同进程里面的不同线程,没有共享的资源;
(3)进程是操作系统中资源分配的基本单位,线程是操作系统中调度的基本单位;
(4)多个进程同时执行的时候,如果一个进程挂了,一半不会影响其他的进程。同一个进程的多个线程之间,如果一个线程挂了,可能会直接把整个进程带走。
(5)线程是轻量级的进程。创建线程比创建进程更快,销毁线程比销毁进程更快,调度线程比调度进程更快。
关于线程池这里不做讨论。
为什么需要多线程编程
首先,单核CPU的发展遇到了瓶颈,要提高算力,就需要多核CPU,另外,有些任务场景需要“等待IO”。为了让等待IO做其他的工作,需要用到并发编程(多线程编程)。
并行: 同一时刻做很多操作。比如吃饭和看手机,吃饭的时候同时看手机就是并行。
并发:多个任务可以交叉重叠进行。比如吃饭和看手机,吃一会饭后停下来,看一会手机,看一会手机后吃一会饭,两者交替出现。
简单的线程程序
Java自带的线程
新建一个java文件。
这个代码是我们在刚开始学Java的时候写的代码。当我们运行起来这个程序的时候,操作系统会给这个程序生成一个进程,这个进程叫做java进程。在java进程里面,就会有一个线程调用main方法,而调用这个main方法的线程,在进程里面是属于自带的线程,一般也叫主线程。即使是最简单的程序,也涉及到线程。
虽然上述代码没有手动创建其他线程,但是java进程在运行的时候,内部会创建出很多线程,比如垃圾回收线程等。
手动创建一个简单线程
接下来,我们手动创建一个线程。在谈到多进程的时候,会提到“父进程”和“子进程”。但是在多线程中没有父子线程的说法,也就是说,线程之间是对等的。
Java中创建线程,需要用到一个关键类:Thread类。
一种比较简单的创建线程的方法是写一个子类继承Thread类,然后重写其中的run方法。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("thread");
}
}
public class demo1 {
public static void main(String[] args) {
Thread thread = new MyThread();//向上转型
thread.start();
System.out.println("main");
}
}
重写的run方法是在描述新创建好的线程要执行什么任务,但是还没有执行。光创建了这个类,还不算是创建了线程,还需要创建实例。向上转型实例化类后,start方法才是真正开始创建线程,在操作系统内核中,创建对应线程的PCB,让PCB加入到系统链表中,参与调度。
在代码中,虽然先启动的线程,后打印的main,但是代码的结果是先打印main,后打印thread。每一个线程是独立的执行流,main方法所在的线程是一个执行流,myThread实例所在的线程是一个执行流。这两个执行流各自执行各执的代码,是并发(是宏观上说法,应该是并发+并行)的执行的关系互相不干扰。这个时候,两个线程的先后顺序,取决于操作系统调度器的具体实现。大致上可以把这里的调度规则简单看成“随机调度”,但不是真正的随机,取决于实现。也就是说,执行的结果是随机的。
先看到main还是thread是不确定的。即使运行多次还是先main后thread,但还是说先main还是先thread是不确定的。这个过程和线程的创建和开销有一定的关系。main方法所在的main线程是自带的,在调用main方法的时候,就已经是创建好了,而thread手动创建的线程需要线程的创建和开销,运行多次我们看见的虽然是先main后thread,但是先main还是先thread仍然是不确定的。所以在编写多线程代码的时候,需要注意默认情况下多个线程之间的执行顺序是无序的。
既然是随机调度的,但是我们有手段来影响线程执行的先后顺序。调度器的自身行为是修改不了的,我们最多就是让某一个线程进行等待,等另一个线程执行结束后再执行这个线程。这里不详细说明。
process这一行,最后的数字表示进程的退出码。进程的退出码表示进程的运行结果,使用0表示正常结束;非0表示进程执行完毕;还有一种情况就是main线程还没有返回,进程就崩溃了,这个时候返回的是一个随机的值。对于线程来说,谁先执行是不知道的,谁先结束也是不知道的。
现在我不想让进程结束这个这么快,可以使用循环来查看 ,同时为了查看的更加清晰,可以使用sleep方法。
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class demo1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
while (true) {
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
看到的结果是看到main和thread交换打印。但是注意的是每一波打印几个是不确定的,先打印哪一个内容也是不确定的,也都是在调度器在控制。这里假设两次打印为一组,可以看到先打印main还是thread是不确定的。
可以使用jjdk的查看线程工具jconsole这样的工具就可以查看。
左下角的线程方块中的信息,有一个main线程来调用main方法的主线程。咱们手动创建的线程就是thread-0。除此之外,还有一些辅助的线程,有的负责垃圾回收,有的负责处理调试信息等。这里就不讨论。
Thread类中run方法和start方法的区别
代码中我们看到了run方法和start方法,如果我们只调用run方法,会出现什么?
使用start方法可以看到两个线程并发执行,两组打印交替出现。而使用run方法,可以看到只是在打印thread,没有打印main。前面我们重写run方法的时候,写了一个死循环,直接调用run方法,没有创建新的线程,只是在之前的线程中,执行了run里面的内容;使用start方法,是创建了新的线程,新的线程在里面会调用run方法,新的线程和旧的线程(main线程)之间是并发的执行过程。
创建线程的常见的几种写法
1、创建一个类继承Thread,重写run方法
我们已经写过了。这种写法把线程和任务内容绑在一起。
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class demo1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
while (true) {
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2、创建一个类,实现Runnable接口,重写run方法。
这种写法把线程和任务内容分开,降低耦合性。这里创建的Runnable,相当于定义了一个任务(代码要干什么),还需要Thread实例,把任务交给Thread,然后start方法来创建具体的线程。
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class demo2 {
public static void main(String[] args) {
//创建线程
//相当于定义了一个任务 -- 代码要干什么
//需要thread实例,把任务交给thread
//thread.start创建具体的线程
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3、继承Thread类,但是不显式继承,使用匿名内部类。
new Thread() 后面的 { 就是创建了一个匿名内部类,这个类是Thread的子类,同时前面的new关键字,创建了这个类的一个实例。整个操作就是继承、方法重写、实例化。
public class demo3 {
public static void main(String[] args) {
//匿名内部类写法
Thread thread = new Thread() {
//匿名内部类,完成继承,方法重写,实例化
//在start之前,线程只是准备好,没有真正创建出来
//start执行,才创建线程
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
}
}
再次强调,在start之前写的Thread只是java中对于线程的表示,并没有真正的创建线程。要跑起来,还需要main线程调用start方法,执行了start方法,才是真正的在操作系统中创建了线程。
4、使用匿名的Runnable类
public class demo4 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
}
}
5、使用lambda表达式定义任务
推荐使用这种写法。
public class demo5 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
使用多线程最主要的还是利用多核CPU,提升效率,对于CPU密集型场景和IO密集型场景,多线程编程就显着很重要了。