Java多线程和内存模型(一)
由于java是运行在 JVM上 的,所以需要涉及到 JVM 的内存模型概念,需要理解内存模型,就需要多线程的基础;
而线程是基于载体线程里的,所以我们借由操作系统的进程来讲一讲。
进程
什么是进程?
- 进程是程序的运行实例
- 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
- 进程本身不是基本运行单位,而是线程的容器
- 但进程是系统进行资源分配和调度的一个单位
- 进程需要一些资源才能完成工作,如CPU使用时间、内存、文件以及I/O设备,且为依序逐一进行,也就是每个CPU核心任何时间内仅能运行一项进程。
- 我们常说的进程实体就是PCB块(Process Control Block);
什么是PCB(进程控制块)?
进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。 系统用它来记录进程的外部特征,描述进程的运动变化过程。 同时,系统可以利用PCB来控制和管理进程;
PCB(进程控制块)是系统感知进程存在的唯一标志。
通常我们编写的程序是静止的东西,是不能并发执行的个体,为了使程序(包含数据)能独立运行,便出现了为程序配备PCB(进程控制块)的概念;
由程序段,相关数据段和PCB三部分构成了进程实体
进程的特征
1.动态性
进程拥有自己的生命周期,它由创建而产生,由调度而执行,由撤销而消亡;
容易模糊的概念:程序是不是一个进程?
- 程序本身只是指令、数据及其组织形式的描述,并且存放在某种储存介质上,属于静态的
- 进程才是程序(那些指令和数据)的真正运行实例。
2.并发性
多个进程实例在同一时间间隔内一起运行,宏观上就好像多个进程同时在工作;
实际上却是一种CPU多核处理临界资源的机制
易混点:并发和并行
Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发与并行的区别
3.异步性
进程是按照异步方式运行的,即按各自独立的,不可预知的速度向前推进。若参与并发执行,产生的结果会出现不可再现性。如果需要保持其结果是可再现的,需要配合相应的进程同步机制进行控制。
4.独立性
在传统的OS中,进程实体是一个能独立运行的,独立获得资源和独立接受调度的基本单位;凡未建立PCB的程序都不能作为一个独立的单位参与运行。
进程的三种基本状态
1.就绪态(Ready)
该状态的进程已分配除CPU以外的所有必要资源后,只需要获得CPU,便可立即执行。
所以就绪的前提是运行所需的资源分配完全。
2.执行态(Running)
进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态; 在多处理机系统中,则有多个进程处于执行状态。
3.阻塞态(Block)
正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态或封锁状态。
导致进程阻塞的典型事件:
- I/O请求
- 申请缓冲区
- 等待信件(信号)等
通常将这种处于阻塞状态的进程也排成一个队列。有的系统则根据阻塞原因的不同而把处于阻塞状态的进程排成多个队列。
现在的操作系统中的进程除了以上的三种基本状态,还多了一种 ”挂起状态“
什么是挂起状态?
进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作。
挂起在操作系统中用原语表示为:Suspend ;相应的激活原语为:Active。
挂起和阻塞的区别:
挂起为主动挂起,阻塞是被动阻塞;
挂起产生的五个原因:
1.交换(负荷调节需求):
操作系统需要释放足够的内存空间,以调入并执行处于就绪状态的进程。
2.其他OS原因:
操作系统可能挂起后台进程或工具程序进程,或者被怀疑导致问题的进程。
3.交互式用户请求:
用户可能希望挂起一个程序的执行,目的是为了调试(debug)或与一个资源的使用进行连接。
4.定时:
一个进程可能会周期性地执行(例如记账或系统监视进程),而且可能再等待下一个时间间隔时被挂起。
5.父进程请求:
父进程可能会希望挂起后代进程的执行,以检查或修改挂起的进程,或者协调不同后代进程之间的行为。
加入挂起后的进程状态转换图:
静止阻塞的时候,要是当时被阻塞的进程的需求者来到,那么它就会被释放到静止就绪状态;但是没有获得Active() 激活状态下,它依旧没办法进入活动就绪队列,也就没有办法得到执行
进程既然具有生命周期,必然存在创建和终止的状态
1.创建状态:
- 申请空的PCB
- 向PCB中写入控制和管理进程的信息
- 分配运行时该进程所需要的资源
- 把该进程装入就绪状态并插入就绪队列中
2.终止状态
- 由操作系统进行终止进程的善后工作(主要是保存状态码和一些计时统计数据)
- 善后工作完成后,将PCB清0,并把PCB所占空间返回给系统
创建进程的实质:创建进程实体中的PCB
撤销进程的实质:撤销进程的PCB
需要注意的经典进程同步问题
1.生产者–消费者问题
2.哲学家进餐问题
3.读者–写者问题
线程
什么是线程?
线程是操作系统能够进行运算调度的最小单位;它被包含在进程中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V 及 SunOS 中也被称为轻量进程(lightweight processes),但是轻量进程更多指内核线程(kernel thread),把用户线程(user thread) 称为线程。
线程与进程的关系
- 同一个进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符号 和 信号处理等;
- 同一进程中的多个线程有各自的调用栈,寄存器环境,线程本地储存;
- 区别:
- 调度层面:
- 在引入线程的操作系统中,把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位;
- 在同一个进程中,线程的切换不会引起进程的切换;
- 从一个进程中的线程切换到另一个进程中的线程时,会引起进程的切换;
- 并发性层面:
- 进程之间在并发执行的同时,进程内的若干线程也可以并发执行;
- 拥有资源:
- 进程拥有资源;线程不拥有资源,共享所属进程的资源;
- 系统开销:
- 进程在被创建和撤销时,创建和回收PCB,分配和回收资源,系统的开销明显高与线程;
- 进程切换时,CPU环境的保护以及新被调度的进程CPU环境的配置;而线程的切换仅需要保存和设计少量寄存器内容;
- 通信方面,一个进程中的所有线程具有相同的地址空间,在同步和通信的实现方面线程也比进程容易;
Java中的线程
生命周期
java线程在运行的生命周期会有6种:
状态名称 | 说明 |
---|---|
NEW(new) | 初始状态,线程被构建,但是还没有调用Star()方法 |
RUNNABLE(runnable) | 运行状态,Java线程将操作系统中的就绪(Ready)和运行(Runing)两种状态笼统的称为“运行中” |
BLOCKED(blocked) | 阻塞状态,表示线程阻塞于锁 |
WAITING(waiting) | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或者中断) |
TIME_WAITING(time_waiting) | 超时等待状态,该状态不用与WAITING混淆,它是可以在指定的时间自行返回的 |
TREMINATED(treminated) | 终止状态,表示当前线程已经执行完毕 |
Java线程生命周期流程图
Java线程常用方法
方法 | 描述 |
---|---|
Start方法 | start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会相应的线程分配需要的资源。 |
runf法 | run()方法是不需要永固来调用的,当通过start()方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体中去执行具体的任务。注意:继承Thread类必须重写run方法,在run方法中定义具体要执行的任务 |
sleep方法 | sleep相当于让线程睡眠,交出CPU的占有权,让CPU去执行其他任务,(进入阻塞队列)sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问到这个对象 |
yield方法 | yield方法会让当前线程交出CPU权限,让CPU去执行其他线程。它和Sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程获取CPU执行时间的机会。调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪的状态,它只需要等待重新获取CPU执行时间,只是和sleep方法的区别 |
join方法 | join方法就是往线程中添加东西;join方法可以用于临时加入线程,例如:在一个线程运算过程中,若满足条件,可以临时加入一个线程,让这个线程运算完,另一个线程再继续执行。 |
interrupt方法 | 作中断用,单独调用interrupt方法可以使处于阻塞状态的线程抛出一个异常,也就是说,它可以用来中断一个正处于阻塞状态的线程;直接调用interrupt不能直接中断正在运行的线程,需要用interrupt方法和isInterrupt方法来停止正在运行的线程,一般通过增加一个属性isStop来标志是否结束while循环。 |
suspend(),resume()和stop()方法(已经过期) | 这三个方法分别实现了线程的暂停,恢复和终止工作;suspend方法在调用后,线程不会释放已经占有的资源(比如锁),而是占着资源进入睡眠状态 ,这样容易引发死锁问题;而stop()方法在终结一个线程时不会保证该线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。 |
getId | 获取线程ID |
getName和setName | 用来获取和设置线程的名称 |
getPriority和setPriority | 获取和设置线程的优先级 |
setDaemon和isDeamon | 用来设置线程是否成为守护线程和判断线程是否是守护线程;守护线程和用户线程的区别:守护线程依赖于创建它的线程(也就是父线程),用户线程就不依赖; |
流程图对应ThreadState图解:
关于Suspend方法过期的替代方法:
需要用到suspend的地方基本都刻意替换为wait,notify模式;
如果你想要简单实现,刻意考虑JUC提供的工具LockSupport。下面的例子使用LockSupport工具替换上面例子的suspend和resume方法。
长久以来对线程阻塞与唤醒经常我们会使用object的wait和notify,除了这种方式,java并发包还提供了另外一种方式对线程进行挂起和恢复,它就是并发包子包locks提供的LockSupport。
LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。java锁和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。
LockSupport是不可重入的,如果一个线程连续2次调用LockSupport.park(),那么该线程一定会一直阻塞下去。
LockSupport很类似于二元信号量(只有1个许可证可供使用),
park()获取许可;
如果这个许可还没有被占用,当前线程获取许可并继续执行;
unpark()释放许可;
如果许可已经被占用,当前线程阻塞,等待获取许可。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class SuspendTest2 {
static Object lock = new Object();
public static void main(String[] args) {
Suspend s1 = new Suspend();
Suspend s2 = new Suspend();
s1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(s1);//释放s1许可
s2.start();
LockSupport.unpark(s2);//释放s2许可
}
static class Suspend extends Thread{
@Override
public void run() {
synchronized(lock){
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();//获取线程的许可
}
}
}
}
未完待续。。。
谢谢关注!!