文章目录
1. 进程和线程
1.1 介绍
进程:程序是静止的,进程实体的运行过程就是进程,是系统进行资源分配的基本单位
进程的特征:并发性、异步性、动态性、独立性、结构性
线程:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统独立调度的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,与同属一个进程的其他线程共享进程所拥有的全部资源
关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程
线程的作用:使多道程序更好的并发执行,提高资源利用率和系统吞吐量,增强操作系统的并发性能
并发并行:
- 并行:在同一时刻,有多个指令在多个 CPU 上同时执行
- 并发:在同一时刻,有多个指令在单个 CPU 上交替执行
同步异步:
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
1.2 进程和线程对比
线程进程对比:
-
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
-
进程拥有共享的资源,如内存空间等,供其内部的线程共享
-
进程间通信较为复杂
同一台计算机的进程通信称为 IPC(Inter-process communication)
- 信号量:信号量是一个计数器,用于多进程对共享数据的访问,解决同步相关的问题并避免竞争条件
- 共享存储:多个进程可以访问同一块内存空间,需要使用信号量用来同步对共享存储的访问
- 管道通信:管道是用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件 pipe 文件,该文件同一时间只允许一个进程访问,所以只支持半双工通信
- 匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信
- 命名管道(Names Pipes):以磁盘文件的方式存在,可以实现本机任意两个进程通信,遵循 FIFO
- 消息队列:内核中存储消息的链表,由消息队列标识符标识,能在不同进程之间提供全双工通信,对比管道:
- 匿名管道存在于内存中的文件;命名管道存在于实际的磁盘介质或者文件系统;消息队列存放在内核中,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才被真正删除
- 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 套接字:与其它通信机制不同的是,可用于不同机器间的互相通信
-
线程通信相对简单,因为线程之间共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
Java 中的通信机制:volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer
-
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2. 线程创建
JAVA中创建一个线程的方法主要有三种,三种方法各有优缺点,下面我们一一来进行介绍。
2.1 Thread
Thread 创建线程方式:创建线程类,匿名内部类方式
- start() 方法底层其实是给 CPU 注册当前线程,并且触发 run() 方法执行
- 线程的启动必须调用 start() 方法,如果线程直接调用 run() 方法,相当于变成了普通类的执行,此时主线程将只有执行该线程
- 建议线程先创建子线程,主线程的任务放在之后,否则主线程(main)永远是先执行完
Thread 构造器:
public Thread()
public Thread(String name)
// 创建线程类
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
for(int i = 0 ; i < 100 ; i++ ){
System.out.println("main线程" + i);
}
// main线程输出放在上面 就变成有先后顺序了,因为是 main 线程驱动的子线程运行
}
}
class MyThread extends Thread {
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++ ) {
System.out.println("子线程输出:"+i);
}
}
}
// 使用匿名内部类的方式
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
for(int i = 0 ; i < 100 ; i++ ) {
System.out.println("子线程输出:"+i);
}
}
};
t.start();
for(int i = 0 ; i < 100 ; i++ ){
System.out.println("main线程" + i);
}
}
}
2.2 Runnable
Runnable 创建线程方式:创建线程类,匿名内部类方式
Thread 的构造器:
public Thread(Runnable target)
public Thread(Runnable target, String name)
// 使用类继承的方式
public class ThreadDemo {
public static void main(String[] args) {
Runnable target = new MyRunnable();
Thread t1 = new Thread(target,"1号线程");
t1.start();
Thread t2 = new Thread(target);//Thread-0
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 10 ; i++ ){
System.out.println(Thread.currentThread().getName() + "->" + i);
}
}
}
// 使用匿名内部类
@Slf4j(topic = "c.test")
public class ThreadDemo {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
log.debug("running");
}
};
// 线程名为 t2
Thread t = new Thread(r, "t2");
t.start();
}
}
// 使用lambda简化
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
Runnable r = () -> {log.debug("running");};
Thread t = new Thread(r, "t2");
t.start();
}
}
// 还可以进行简化
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
Thread t = new Thread(() -> {log.debug("running");}, "t2");
t.start();
}
}
Runnable 方式的优缺点:
-
缺点:代码复杂一点。
-
优点:
-
线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性
-
同一个线程任务对象可以被包装成多个线程对象
-
适合多个多个线程去共享同一个资源
-
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
-
线程池可以放入实现 Runnable 或 Callable 线程任务对象
-
2.3 Callable
实现 Callable 接口:
- 定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型
- 重写线程任务类的 call 方法,这个方法可以直接返回执行的结果
- 创建一个 Callable 的线程任务对象
- 把 Callable 的线程任务对象包装成一个未来任务对象
- 把未来任务对象包装成线程对象
- 调用线程的 start() 方法启动线程
public FutureTask(Callable<V> callable)
:FutureTask对象,在线程执行完后得到线程的执行结果
- FutureTask 就是 Runnable 对象,因为 Thread 类只能执行 Runnable 实例的任务对象,所以把 Callable 包装成FutureTask对象
- 线程池部分详解了 FutureTask 的源码
FutureTask
对象的常用方法有如下的几个方法:
get()
:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步,run() 执行完后会把结果设置到 FutureTask 的一个成员变量,get() 线程可以获取到该变量的值get(long timeout, TimeUnit unit)
:用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回 null。isDone()
:方法表示任务是否已经完成,若任务完成,则返回 truecancel(boolean mayInterruptIfRunning)
:用来取消任务,如果取消任务成功则返回 true,如果取消任务失败则返回 false。参数mayInterruptIfRunning
表示是否允许取消正在执行却没有执行完毕的任务,如果设置 true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning
为 true 还是 false,此方法肯定返回 false,即如果取消已经完成的任务会返回 false;如果任务正在执行,若mayInterruptIfRunning
设置为 true,则返回 true,若mayInterruptIfRunning
设置为 false,则返回 false;如果任务还没有执行,则无论mayInterruptIfRunning
为 true 还是 false,肯定返回 true。isCancelled()
:方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
// 实现Callable接口
public class ThreadDemo {
public static void main(String[] args) {
Callable call = new MyCallable();
FutureTask<String> task = new FutureTask<>(call);
Thread t = new Thread(task);
t.start();
try {
String s = task.get(); // 获取call方法返回的结果(正常/异常结果)
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String> {
@Override//重写线程任务类方法
public String call() throws Exception {
return Thread.currentThread().getName() + "->" + "Hello World";
}
}
// 使用匿名内部类
public class ThreadDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建一个FutureTask对象,其返回值是Integer
FutureTask<Integer> task = new FutureTask<>(() -> {
return 100;
});
// 启动线程
new Thread(task, "t3").start();
// 阻塞进程知道获取到执行结果
Integer r = task.get();
System.out.println(r);
}
}
3. 线程原理
3.1 运行机制
Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程
- 线程的 CPU 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park 等方法
程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的
当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
JVM 规范并没有限定线程模型,以 HotSopot 为例:
- Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)
- 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能
Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程
3.2 线程调度
线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度、抢占式线程调度(Java 选择)
协同式线程调度:线程的执行时间由线程本身控制
- 优点:线程做完任务才通知系统切换到其他线程,相当于所有线程串行执行,不会出现线程同步问题
- 缺点:线程执行时间不可控,如果代码编写出现问题,可能导致程序一直阻塞,引起系统的奔溃
抢占式线程调度:线程的执行时间由系统分配
- 优点:线程执行时间可控,不会因为一个线程的问题而导致整体系统不可用
- 缺点:无法主动为某个线程多分配时间
Java 提供了线程优先级的机制,优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用
说明:并不能通过优先级来判断线程执行的先后顺序
3.3 未来优化
内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多个用户线程映射到一个内核级线程),被设计为协同式调度,所以叫协程
- 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程
- 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限
有栈协程中有一种特例叫纤程,在新并发模型中,一段纤程的代码被分为两部分,执行过程和调度器:
- 执行过程:用于维护执行现场,保护、恢复上下文状态
- 调度器:负责编排所有要执行的代码顺序