进程和线程
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
线程和进程两个名词不过是对应的CPU时间段的描述
进程就是包换上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文
CPU在执行的时候没有进行上下文切换的。---网上这句化有问题
- 什么是 CPU 上下文?
CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。
程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。 - 什么是 CPU 上下文切换?
就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
根据任务的不同,可以分为以下三种类型 - 进程上下文切换 - 线程上下文切换 - 中断上下文切换
系统调用
从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。
在这个过程中就发生了 CPU 上下文切换,整个过程是这样的:
- 保存 CPU 寄存器里原来用户态的指令位
- 为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。
- 跳转到内核态运行内核任务。
- 当系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。
所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态-内核态-用户态)
不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。系统调用过程中一直是同一个进程在运行
进程上下文切换
进程是由内核来管理和调度的,进程的切换只能发生在内核态。
所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU 寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
线程上下文切换
线程与进程最大的区别在于:线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。
所以,对于线程和进程,我们可以这么理解: - 当进程只有一个线程时,可以认为进程就等于线程。 - 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。当然,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
在java中,线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作这是很耗时的。
线程安全
所谓多线程安全问题,简单点说就是多个线程操作一个共享变量导致数据不一致的问题。为什么会产生这个问题?这就要说到java的内存模型。
- 内存可见性问题
java内存模型规定,将所有变量都放到主内存中也就是堆,当线程使用变量时,它会把主内存中的变量复制到自己的工作内存(一般为cpu的一级缓存)。 - 原子性操作问题
在一次操作中要么都执行,要么都不执行。
i++
问题 - java指令重排序
java内存模型允许比阿尼器和处理器对指令重排序以提高运行性能,在单线程下没问题,但在多线程下会存在问题. -
伪共享问题
为了解决系统中主内存和cpu缓存中的速度差问题,会在CPU和主内存之间体检一级或多级缓存。而Cache内部时按行存储的,这就会产生一个问题:当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,其他只能等待。所以相比将每个变量放到一个缓存行里,性能会下降,这就是伪共享
如上图,线程1 和线程2第一次读的时候会命中主内存,但在其他时候会命中线程中的变量副本v1或者v2,一旦发生修改,则可能会产生数据不一致问题。
线程状态
线程共包括以下5种状态:
- 初始状态,也就是刚创建new,但并没有启动
- 就绪状态,执行了start()方法之后,注意,这个时候并没有立即启动,它会在一个合适的时间拿到CPU的执行权,才开始执行
- 运行状态,线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态,图上说的阻塞状态是指已经获取到锁但没有继续运行
- run()方法结束
基本函数介绍
主要介绍一下函数:
- sleep()、yield()是Thread的静态方法 ,
- wait()、notify()、notifyAll() 是Object的方法
- join()
方法名 | 出处 | 是否阻塞 | 是否释放锁 | 作用 |
---|---|---|---|---|
wait() | Object | 是 | 是 | 当前线程进入等待队列 |
notify() | Object | 否 | 不涉及 | 唤醒等待队列中的某一个线程,转为锁池 |
notifyAll() | Object | 否 | 不涉及 | 唤醒对象等待队列中的所有线程,转为锁池 |
sleep() | Thread静态方法 | 是 | 否 | 休眠,让出cpu |
yield() | Thread静态方法 | 否 | 否 | 让出cpu时间片,重新进入锁池 |
join() | Thread普通方法 | 是 | 否 | 等待其他线程执行完毕 |
interrupt() | Thread普通方法 | 不涉及 | 不涉及 | 设置中断标志位true,并不实际中断,如果线程正调用wait()、join()、sleep()方法,那么会跑出InterruptException |
isInterrupted() | Thread普通方法 | 不涉及 | 不涉及 | 返回中断标志 |
interrupted() | Thread静态方法 | 不涉及 | 不涉及 | 检测中断标志,但如果为true,还会清除中断标志,注意此方法是获取当前线程而不是调用对象的。 |
举例: wait()方法
-
要点一:调用wait()方法之前,必须获取该对象的监视器锁
如下会报错Thread t =new Thread(new Runnable() { @Override public void run() { System.out.println("testWait begin!"); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); //java.lang.IllegalMonitorStateException // 此时调用wait()的对象是this,但我们并没有获取this的对象锁
获取对象监视器锁
``` // 代码块 synchronized(this|object){ } ``` 上述例子加上对象锁,再wait()则不会报错 输出 testWait begin! ``` // 同步方法 synchronized void add (){ //... } ```
-
要点二:wait() 会被阻塞,直到以下事件发生:
- 其他线程调用了该线程的interrupt()
t.start(); Thread.sleep(200); t.interrupt();
- 其他线程调用了该共享对象的notify()或者notifyAll()方法
注意: 同wait()方法一样,notify和notifyAll调用的前提条件也是获取对象锁。
下面直接调用会报错:
需要改成:Runnable runnable=new Runnable() { @Override public void run() { System.out.println("testWait begin!"); synchronized (this){ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("testWait end!"); } }; Thread t =new Thread(runnable); t.start(); Thread.sleep(200); runnable.notify(); //Exception in thread "main" java.lang.IllegalMonitorStateException
t.start(); Thread.sleep(200); synchronized (runnable){ runnable.notify(); }
- 其他线程调用了该线程的interrupt()