我们经常说到进程和线程,那么到底两者有什么区别呢?
所谓 进程
,就是操作系统进行资源分配和调用的最小单位。比如我们做什么事情,什么活动,为了这个事情,我们需要哪些资源,这整个可以理解为一个进程。而 线程
则是CPU调度的最小单位,它依赖进程而存在,它就相当于我们拿什么资源做什么事情的控制流。
在操作系统中, 进程
的执行需要加载上下文、执行任务、保存上下文,为了提升效率,操作系统引入了线程,进程内的多个线程可以共享进程资源,而我们通常所说的多线程是指 用户线程
,需区分于操作系统 内核线程
。
目前我们使用的如Windows、Linux、MacOs等,都是分时操作系统,就是多任务多用户的。
而对于单核CPU来说,同一时间只能做一件事,为了能达到多用户多任务的目标,则使用了时间片,就是同一时间段,执行一项任务。比如,宿舍里面有四个哥们,但是只有一台电脑,大家都要查资料上网,那么每人只能使用五分钟,时间到了就让下一个人使用,自己去后面继续排队。
而至于说,是有序,还是优先,有不同的调度算法,如 RR(时间片轮转)
、 FCFS(先到先服务)
、 SPN(最短作业优先)
、 SRT(最短剩余时间优先)
、 HRRN(高响应比优先)
。下面举例RR算法,其他算法后续专栏会深入探讨。
RR调度,时间片轮转法(Round-Robin),主要用于分时操作系统。
前面提到,CPU调度的基本单位是线程,而时间片轮转,则是划分多个时间片,操作系统会保存一个就绪进程的FIFO队列,当CPU处于空闲时,从头部拿出一个就绪的进程,将CPU分派给该进程,此进程则享有CPU的执行权,在时间片内执行该进程的任务。
时间片通常是10~100ms左右,当时间片执行完成,则中断当前进程,会将该进程放入就绪队列尾部,调度器再将CPU分派给队列头部进程。
如果在时间片内执行完成,则分派下个进程。
上面提到,线程调度,其实是操作系统分配CPU使用权的过程,而主要的调度方式有两种,一种是 协作式线程调度
,一种是 抢占式线程调度
协作式线程可以控制线程的执行时间,执行完成后,可以告诉操作系统下一个递交的线程,就是可以从一个线程主动切换到另一个线程,但是如果执行的线程出现异常,一直占用CPU资源,则会导致系统阻塞。 抢占式线程的执行,则依赖我们上述的时间片机制,即使出现异常也不会阻塞系统 复制代码
java中是使用抢占式线程调度,如果想让线程执行的时间长点,可以设置线程等级,但是并不靠谱。而协作式线程在LUA中 协同例程
中有所体现,java中也有Quasar可以支持,如果习惯于异步编程,并且对性能要求极高,可以考虑一试。
直接上图(纯手工制作)
上图表述了线程的各个状态及状态间切换的操作,主要的五种状态如下,
JAVA中使用线程主要有三种方式
# 继承Thread static class ThreadTest extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName() + ", Thread example....."); } } 复制代码
# 实现Runnable接口,实现run() static class RunnableTest implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + ", Runnable example....."); } } 复制代码
Callable如果我们需要返回值,则使用Callable
# 实现Callable接口,实现call(),并返回相应的值 static class CallableTest implements Callable<String> { @Override public String call() throws Exception { System.out.println(Thread.currentThread().getName() + ", Callable example....."); return "call result"; } } 复制代码
public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadTest threadTest = new ThreadTest(); threadTest.start(); Thread runnableTest = new Thread(new RunnableTest()); runnableTest.start(); FutureTask<String> futureTask = new FutureTask<String>(new CallableTest()); Thread callableTest = new Thread(futureTask); callableTest.start(); String result = futureTask.get(); System.out.println("callable result :" + result); } 复制代码
Thread-0, Thread example..... Thread-1, Runnable example..... Thread-2, Callable example..... callable result :call result 复制代码
直接上例子,马上双十一了,加入仓库有一堆货物,目前只有一个人搬(甲),暂定有1000000件,如果甲只能一次搬一件,那么总共需要搬运1000000次,如果能加入一个乙与甲一起搬,假定两人搬运时间一样,那么两人各搬500000次,就能搞定。
static class Carry extends Thread{ /** 搬运人*/ private String peopleName; /** 搬运次数*/ private int carryNum; public Carry(String peopleName) { this.peopleName = peopleName; } @Override public void run() { while (!isInterrupted()) { synchronized (cargoNum) { if (cargoNum.get() > 0) { cargoNum.addAndGet(-1); carryNum++; } else { System.out.println("搬运完成,员工:" + peopleName + ",搬运:[" + carryNum + "]次"); interrupt(); } } } } } 复制代码
/** 货物个数*/ static AtomicInteger cargoNum = new AtomicInteger(1000000); public static void main(String[] args) { Carry carry1 = new Carry("甲"); carry1.start(); } # 结果 搬运完成,员工:甲,搬运:[1000000]次 复制代码
/** 货物个数*/ static AtomicInteger cargoNum = new AtomicInteger(1000000); public static void main(String[] args) { Carry carry1 = new Carry("甲"); Carry carry2 = new Carry("乙"); carry1.start(); carry2.start(); } # 结果 搬运完成,员工:乙,搬运:[272708]次 搬运完成,员工:甲,搬运:[727292]次 复制代码
上面我们模拟了两个搬货的情况,甲乙两人一起搬运时,能并行工作,效率更高,如果再加丙、丁....则会更快。
现在的CPU一般都支持超线程技术,即一个物理核可当作两个逻辑核,且单CPU基本都是多核,少则几核,多则几十上百,如果我们单独只让一个工作,那么其他的则会处于空闲状态,比如我现在有十个工人可以搬货,但是我只让甲去,那么其他人力成本就相当于浪费掉了。
所以,并发编程最直接的好处就是能合理利用机器资源,提升处理效率。特别是在异步编程,能充分压榨CPU的性能。
例子中用到了 synchronized
和 AtomicInteger
,是为了保证货物剩余总量在多人搬运下,数值也是对的,可以尝试将 AtomicInteger
换成 Integer
或者去除 synchronized
,两人最终总计搬运和将与原货物总量不一致。
所以人多了,事也多了,并发编程时,我们需要考虑线程安全,上下文切换等常见问题,这些将在后续章节呈现。