一、 进程
- 进程:系统进行资源分配和调度的基本单位
- 目的使多个程序并发执行以改善资源利用率和提高系统的吞吐量
1.1 进程有3个基本状态:执行、就绪、阻塞
- 就绪态:已分配除了CPU以外的所有资源,就绪队列。
- 执行态:进程获取CPU 资源,处于执行中。
- 堵塞态:等待事件发生(I/O请求,申请缓存空间等),从而从就绪或执行进入堵塞状态。也称等待或睡眠状态。
进程的状态的转换:
5种基本操作:
- 派生、阻塞、激活、 调度、 结束
-
1.2 进程控制块:PCB
- PCB 操作系统重要的数据结构struct ,PCB记录操作系统所需的,用于描述进程情况及控制进程运行所需的全部信息,每一个进程都有唯一的PCB,从而使得进程成为一个独立运行的基本单位。
1.3 进程的并发执行
- 创建进程:分配资源,创建唯一的PCB
- 撤销进程: 回收资源,撤销PCB
- 进程切换: 对进程进行上下文切换时,需要保留当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间。
二、 线程 (将资源和调度进行分离产生了线程)
- 线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。
- 目的减少程序并发执行时所付出的时空开销
2.1 线程的属性:
- 每个线程有一个唯一的标识符和一张线程描述表,线程描述表记录了线程执行的寄存器和栈等现场状态。
- 不同的线程可以执行相同的程序,即同一个服务程序被不同用户调用时操作系统为它们创建成不同的线程。
- 同一进程中的各个线程共享该进程的内存地址空间。
- 线程是处理机的独立调度单位,多个线程是可以并发执行的。在单处理机的计算机系统中,各线程可交替地占用处理机。在多处理机的计算机系统中,各线程可同时占用不同的处理机,若各个处理机同时为一个进程内的各线程服务则可缩短进程的处理时间。
- 一个线程被创建后便开始了它的生命周期,直至终止,线程在生命周期内会经历阻塞状态、就绪状态和执行状态等各种状态变化。
2.2 线程有两个基本类型:
- 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。
- 系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行以及撤消线程。
2.3 理解并行和并发
并行:并行性是指同一时刻内发生两个或多个事件。
并发:并发性是指同一时间间隔内发生两个或多个事件。
并发技术解决的是CPU使用效率上,并不是执行速度上。线程的就绪态这种设计模式应用到很多地方,例如IO多路复用,套接字队列。目的都是解决CPU执行效率上。
2.4 Java的多线程使用
- (1)继承Thread,重写run()方法,
- (2)实现Runnable接口,实现run() 方法
class MyRunnable implements Runnable {
public void run() { // 就绪态
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}
public class Demo {
public static void main(String[]arg){
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.start();
t2.start();
}
}
因为Java继承机制是只允许单继承,子类不可以访问父类私有的属性。多层继承方法重写,容易产生继承菱形,Java摒弃多继承,通过接口来满足需求。所有一般是实现Runnable接口来解决多线程问题。
2.4.1 start () 和run()方法的区别
start方法的注释:
This method is not invoked for the main method thread or "system" group threads created/set up by the VM. Any new functionality added to this method in the future may have to also be added to the VM.
start启用了线程,等待jvm调用该线程的run()方法。(jvm 是多线程)
2.5.1 Thread状态的切换
- 让线程睡眠的sleep()方法 调用线程进入等待状态,参数是计时时间。时间超时后进入的就绪态。释放cpu调度,但拥有该线程所有的监视器资源即资源不释放。
public static native void sleep(long millis) throws InterruptedException;
- 让出cpu执行权的yield()方法 调用线程让别的线程执行,但是不确保真正让出 可能进入等待状态
- 等待线程执行终结的join()方法,调用线程等待该线程执行完毕后才执行别的线程。
/**
* Waits for this thread to die.
**/
public final void join() throws InterruptedException {
join(0);
}
- 线程的通知和等待notify()、notifyAll()和wait() 。wait() 只会释放当前共享变量的的锁
生产者和消费者的问题:
//生产线程
synchronized (queue) {
//消费队列满,则等待队列空闲
while (queue.size() == MAX_SIZE) {
try {
//挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后
获取队列里面的元素
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//空闲则生成元素,并通知消费者线程
queue.add(ele);
queue.notifyAll();
}
}
//消费者线程
synchronized (queue) {
//消费队列为空
while (queue.size() == 0) {
try
//挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生
产元素放入队列
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//消费元素,并通知唤醒生产者线程
queue.take();
queue.notifyAll();
}
}
- interrupt() 使用的是interrupt来请求终止线程,interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了,也就是说:Java设计者实际上是想线程自己来终止,通过上面的信号,就可以判断处理什么业务了。具体到底中断还是继续运行,应该由被通知的线程自己处理。如果该线程因为调用wait(),sleep()、join() 方法进入堵塞态,别的线程调用该线程的interrupt()会抛出中断异常的错误。
-
Thread t1 = new Thread( new Runnable(){ public void run(){ // 若未发生中断,就正常执行任务 while(!Thread.currentThread.isInterrupted()){ // 正常任务代码…… } // 中断的处理代码…… doSomething(); } } ).start();
2.6 线程的上线文切换
- CPU资源的分配采用了时间片轮转的策略,当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
- 线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。
2.7 线程死锁
2.7.1 线程产生死锁的四个必要条件:
- 互斥条件
- 请求并持有条件
- 不可剥夺条件
- 环路等待条件
破坏四个必要条件便不会产生死锁,只有请求并持有和环路等待可以破坏。
2.8 用户线程和守护线程
jvm启用的时候会调用main()里的用户线程,同时启动很多的守护线程,例如垃圾回收机制。当用户线程结束的时候,jvm启动DestoryJavaVM线程终止JVM。因此守护线程守护的是用户线程。
3 多线程并发问题
3.1 线程安全问题
操作系统中系统读取内存的数据顺序如图:
先访问中间高速缓冲区cache,cpu 先看数据是否命中cache,不命中,则将内存的数据复制到cache中,然后再从cache中读取。所以如果多个线程同时读写共享资源,必然导致出现脏数据。Java一般使用同步synchronized进行同步。
3.2 Java共享变量内存可见性问题
线程读写变量的时候也有自己的cache,将内存的变量复制在自己的cache中,然后进行读写操作,显然线程A的cache对线程B的cache是不可见的。如何解决共享变量内存不可见问题,Java可以使用弱形式同步volatile来解决。