进程与线程
进程
操作系统以进程为单位,分配系统资源(CPU时间片、内存等资源)的最小单位。进程就是用来加载指令、管理内存、管理IO的。
进程间的通信方式
- 管道(pipe)及有名管道(named pipe): 管道可用于具有亲缘关系的父子进程间通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。
- 信号(signal): 信号是在软件层次上对中断机制的一中模拟,它是比较复杂的通信方式,用于通知进程有某件事发生。
- 消息队列(message queue): 息队列是消息的链表,它克服了上两种通信方式中信号量有限的缺点,具有写权限按照一定规则向消息队列中添加消息,对消息队列有读权限可以从消息队列中获取消息。
- 共享内存(shared memory): 它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中的共享内存数据进而更新。可以说这是进程间最有用的通信方式。
- 信号量(semaphore): 主要作为进程之间及同一种进程的不同线程之间的同步互斥手段。
- 套接字(scoket): 这是一种更为一般的进程间的通信机制,它可作用于网络中不同机器之间的进程间的通信,应用非常广泛。
线程
线程有时候也被称为轻量级进程,是操作系统调度(CPU调度)执行的最小单位。线程是进程中的实体、一个进程中可以拥有多个线程,一个线程必须在一个父进程中。
线程的同步互斥
线程同步: 指线程之间具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应该是等待,直到消息到达时才会被唤醒。
线程互斥: 指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一个共享时,任务时候都最多只允许一个线程去进行访问和使用,其他线程必须进行等待。简单来说就是每次访问共享变量只能一个线程,其他线程只能等待。
线程与进程的区别
- 进程基本上是相互独立的,而线程是存在于进程中的。
- 进程拥有共享资源,内存空间等。供其内部线程使用其共享资源。
- 进程的通信比较复杂。 (同一台计算机IPC,不同计算机需要通过网络协议,并共同遵守协议)
- 线程的通信比较简单,因为他们共享进程内部的内存。
- 线程更轻量级,线程上下文切换一般要比进程上下文切换低。
上下文切换
上下文切换指的是一个进程或线程切换到另一个进程或线程。上下文切换只能在内核模式下发生
内核模式和用户模式
内核模式(kernel mode)
在内核模式下,执行代码可以完全并且不受限制的访问底层硬件。他可以执行任何CPU指令和引用任务地址,是操作系统最低级别和最受信任的功能保留,如果内模式下系统崩溃,会导致整个电脑瘫痪。
用户模式(user mode)
在用户模式下执行代码不能直接访问底层硬件或引用内存资源。在用户模式下运行代码必须委托给系统API来访问底层硬件和内存资源。因为有这一层隔离保护一旦用户模式崩溃总是可恢复的,一般运行在计算机上的大多数代码都是用户模式。
操作系统层面的线程生命周期
初始化状态
当创建完一个线程后,但是这个还不允许分配CPU执行。这种状态是属于编程语言特有的,仅仅是编程语言被创建,而对操作系统层面没有真正线程没有被创建。
就绪状态(可运行状态)
是线程可以被CPU执行,这种状态下操作系统层面的线程已经成功的创建,所以可以分配CPU执行。
运行状态
当空闲的CPU,操作系统会将其分配给一个可运行状态的线程,被分配到CPU状态转换为运行状态。
休眠状态
运行状态的线程被一个阻塞API或者等待某个事件,那么线程就转换到休眠状态,同时是否CPU使用权。
终止状态
线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他线程任务状态,进入终止状态就意味着线程的生命周期结束。
Java线程详解
Java创建线程的方式
使用Thread类或继承Thread类
public static void main(String[] args) {
// 创建线程对象
Thread t = new Thread() {
@Override
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
}
实现Runanbe接口配合Thread
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
// 需要执行的任务
}
};
Thread thread = new Thread(runnable);
}
有点:
- 把要执行的任务和线程分隔开
- Thread代表线程,Runnable代表任务
缺点:
- 没有返回值
- 没有抛出异常
使用有返回值的Callable
class CallableTask implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return new Random().nextInt();
}
}
public void method(){
CallableTask task = new CallableTask();
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask);
}
使用lambda
public static void main(String[] args) {
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
}
注意:Java创建线程的本质都是通过new Thread(); 调用Thread#start来启动线程,通过Thread#run来执行任务。
Java线程的调度机制
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身决定,线程的执行时间是可控的,也不会由一个线程导致系统阻塞。
协同式线程调度
线程的执行时间是由线程本身控制的,线程把自己的工作执行完之后,要主动的通知系统切换到下一个线程。最大的好处是实现简单,切换操作是可知的,没有线程同步的问题,缺点就是线程的执行时间是不可控的,一个线程有问题可能导致整个系统阻塞。
Java的线程调度方式
Java的线程调度方式就是抢占式,如果希望给一些线程多分配一点时间,一些线程少分配一点时间可以设置优先级来控制。
Thread的常用方法
sleep()方法
- 调用sleep会让当前线程从Running 进入 TIMED_WAITING状态,不会释放对象锁
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出异常,并且清除中断标志。
- 睡眠结束后的线程未必会立刻得到执行
- sleep当传入参数为0时和yield方法相同
yield()方法
- yield会释放CPU资源,让当前线程从Running进入到Runnable状态,让优先级更高的线程获得执行机会,不会释放对象锁
- 如果当前只有main线程,调用yield之后,main线程会继续运行,没有比main线程更高的优先级
- 具体的实现是依赖于操作系统的任务调度器
join()方法
等待调用join方法结束后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
stop()方法
stop方法已经被JDK废弃了,原因就是stop方法太过于暴力了,强行把执行到一半的线程终止。
Java线程的生命周期
Java 语言中线程共有六种状态,分别是:
- NEW(初始化状态)
- RUNNABLE(可运行状态+运行状态)
- BLOCKED(阻塞状态)
- WAITING(无时限等待)
- TIMED_WAITING(有时限等待)
- TERMINATED(终止状态)
在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。