一、前言
二、多线程概述
在了解多线程之前,我们非常有必要先了解什么是线程;
而在了解线程之前,我们又必须先了解什么是进程,因为线程是依赖于进程而存在的。
(一)进程与多进程
因此,让我们先来看看什么是进程:
进程,其实就是一个正在运行的程序。
进程是系统进行资源分配的基本单位,每一个进程都有它自己的内存空间和系统资源。
单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情,也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。
那么,多进程有什么意义呢?
多进程其实就是CPU在做着程序间的高效切换让我们觉得是同时进行的,并且可以提高CPU的使用率。
(二)线程与多线程
什么是线程呢?
在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。
线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位(系统进行调度的基本单位)。
单线程:程序只有一条执行路径。
那么,对于什么是多线程,我们就能很容易的理解了:
多线程:程序有多条执行路径。
多线程有什么意义呢?
多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权,从而提高该应用程序的使用率。
(三)补充知识点
1、不能保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。
2、并行和并发
前者是逻辑上同时发生,指在某一个时间内同时运行多个程序;
后者是物理上同时发生,指在某一个时间点同时运行多个程序。
3、Java程序的启动原理:
由java命令启动JVM,JVM启动就相当于启动了一个进程。接着由该进程创建了一个主线程去调用main方法。
4、jvm虚拟机的启动是单线程的还是多线程的?
虚拟机的启动是多线程的原因:垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的。
三、多线程的实现
多线程一共有三种实现方式:
(一)方式一:继承Thread类
1、步骤:
1)编写一个类,继承Thread类
2)重写run()方法
3)创建线程对象
4)启动线程
2、案例代码:
1 | public class MyThread extends Thread { |
1 | public class MyThreadDemo { |
3、需要注意的问题:
1)为什么需要重写run方法?
因为可能并不是所有的代码都需要被线程执行,我们只需要把需要被线程执行的代码放在run方法中就行。
2)为什么不直接调用run方法启动?
因为run()方法直接调用其实就相当于普通的方法调用,这样的话就是单线程的效果。
3)run方法和start方法有什么区别?
run():仅仅是封装被线程执行的代码,直接调用是普通方法
start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
4)调用两次start方法会发生什么?
会抛出IllegalThreadStateException:非法的线程状态异常因为这样相当于是线程被调用了两次。而不是两个线程启动。
(二)方式二:实现Runnable接口
1、步骤:
1)自定义类MyRunnable实现Runnable接口
2)重写run()方法
3)创建MyRunnable类的对象
4)创建Thread类的对象,并把第3)步骤的对象作为构造参数传递
2、案例代码:
1 | public class MyRunnable implements Runnable { |
1 | public class MyRunnableDemo { |
3、需要注意的问题
实现接口方式的好处是什么(为什么有了方式一还需要方式二)?
a. 可以避免由于Java单继承带来的局限性。如果某个子类已经有了一个父类,但是它又想实现多线程,在这种情况下,使用方式一是无法实现的。因为Java只需要单继承。
b. 更好的体现了面向对象的设计思想。使用继承Thread的方式,如果需要创建多个线程对象,就会有多份资源;但是如果使用实现Runnable接口的方式,就只会有一份资源,因为在创建多个线程对象时,传递的都是同一个Runnable对象。
(三)方式三:实现Callable接口
由于篇幅原因,请查看Java基础–多线程【Callable接口】
四、线程调度
(一)线程调度模型
假如我们的计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么是如何对线程进行调用的呢?这就涉及到线程调度模型。
线程有两种调度模型:
分时调度模型 *:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
*抢占式调度模型 : 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。
Java使用的是抢占式调度模型
(二)线程优先级
因为在Java中,使用的是抢占式调度模型,所以,优先级高的线程获取的CPU时间片相对多一些。
那么,如何获取和设置线程的优先级呢?
1 | public final int getPriority():返回线程对象的优先级 |
注意:
线程默认优先级是5
线程优先级的范围是:1-10
线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
如果传递的优先级不在正确范围内,会抛出IllegalArgumentException:非法参数异常。
五、线程控制
(一)线程休眠
线程休眠指的是使用sleep()方法:
1 | public static void sleep(long millis) |
1 | public class ThreadSleep extends Thread { |
一般都是在run方法中使用。
(二)线程加入
线程加入指的是使用join()方法:
1 | public final void join():等待该线程终止 |
1 | public class ThreadJoin extends Thread { |
1 | public class ThreadJoinDemo { |
其他线程会等待使用该方法的线程执行完毕后再执行。
在线程启动后调用该方法,不能在run方法中调用。
(三)线程礼让
线程礼让指的是使用yield()方法:
1 | public static void yield():暂停当前正在执行的线程对象,并执行其他线程 |
1 | public class ThreadYield extends Thread { |
1 | public class ThreadYieldDemo { |
(四)守护线程
守护线程指的是使用setDeamon()方法将线程设置为守护线程与否:
1 | public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程 |
1 | public class ThreadDaemon extends Thread { |
1 | public class ThreadDaemonDemo { |
原理:当正在运行的线程都是守护线程时,Java 虚拟机退出。
注意事项:该方法必须在启动线程前调用。
(五)中断线程
中断线程指的是使用interrupt()方法把线程的状态终止:
1 | public void interrupt():中断线程 |
1 | public class ThreadStop extends Thread { |
1 | public class ThreadStopDemo { |
注意:
如果使用stop()方法,那么线程就会直接停止,run方法后面代码直接不执行。
因此,该方法不安全,已被弃用。
public final void stop()
六、线程生命周期
Java中,线程的生命周期共有5个状态:创建、就绪、运行、阻塞、死亡。
生命周期图如下所示:
七、线程状态转换图
Java新手,若有错误,欢迎指正!