原创技术文章,版权归作者所有,若转载请标明出处
公众号,待定,原公众号长期试灰已被冻结
进程和线程
我们经常说到进程和线程,那么到底两者有什么区别呢?
所谓进程
,就是操作系统进行资源分配和调用的最小单位。比如我们做什么事情,什么活动,为了这个事情,我们需要哪些资源,这整个可以理解为一个进程。而线程
则是CPU调度的最小单位,它依赖进程而存在,它就相当于我们拿什么资源做什么事情的控制流。
在操作系统中,进程
的执行需要加载上下文、执行任务、保存上下文,为了提升效率,操作系统引入了线程,进程内的多个线程可以共享进程资源,而我们通常所说的多线程是指用户线程
,需区分于操作系统内核线程
。
CPU是如何执行的
目前我们使用的如Windows、Linux、MacOs等,都是分时操作系统,就是多任务多用户的。
而对于单核CPU来说,同一时间只能做一件事,为了能达到多用户多任务的目标,则使用了时间片,就是同一时间段,执行一项任务。比如,宿舍里面有四个哥们,但是只有一台电脑,大家都要查资料上网,那么每人只能使用五分钟,时间到了就让下一个人使用,自己去后面继续排队。
而至于说,是有序,还是优先,有不同的调度算法,如RR(时间片轮转)
、FCFS(先到先服务)
、SPN(最短作业优先)
、SRT(最短剩余时间优先)
、HRRN(高响应比优先)
。下面举例RR算法,其他算法后续专栏会深入探讨。
RR调度,时间片轮转法(Round-Robin),主要用于分时操作系统。
前面提到,CPU调度的基本单位是线程,而时间片轮转,则是划分多个时间片,操作系统会保存一个就绪进程的FIFO队列,当CPU处于空闲时,从头部拿出一个就绪的进程,将CPU分派给该进程,此进程则享有CPU的执行权,在时间片内执行该进程的任务。
时间片通常是10~100ms左右,当时间片执行完成,则中断当前进程,会将该进程放入就绪队列尾部,调度器再将CPU分派给队列头部进程。
如果在时间片内执行完成,则分派下个进程。
JAVA线程调度
上面提到,线程调度,其实是操作系统分配CPU使用权的过程,而主要的调度方式有两种,一种是协作式线程调度
,一种是抢占式线程调度
协作式线程可以控制线程的执行时间,执行完成后,可以告诉操作系统下一个递交的线程,就是可以从一个线程主动切换到另一个线程,但是如果执行的线程出现异常,一直占用CPU资源,则会导致系统阻塞。
抢占式线程的执行,则依赖我们上述的时间片机制,即使出现异常也不会阻塞系统
复制代码
java中是使用抢占式线程调度,如果想让线程执行的时间长点,可以设置线程等级,但是并不靠谱。而协作式线程在LUA中协同例程
中有所体现,java中也有Quasar可以支持,如果习惯于异步编程,并且对性能要求极高,可以考虑一试。
JAVA线程状态
直接上图(纯手工制作)
上图表述了线程的各个状态及状态间切换的操作,主要的五种状态如下,
- 新建(New),此时线程已创建,但是未启动
- 运行(Runable),此状态包括Running和Ready,线程启动后,是Ready状态,前面讲到的CPU分派,当线程拥有CPU执行权后,则变为Running状态,若运行的线程yield后,则让出执行权,重新变为就绪状态
- 等待(Waiting),分无限期等待和有限期等待,wait()、join()、park(),需要被其他线程显示唤醒,如果未被唤醒则会一直等待,直到进程终止
- 阻塞(Blocked),严格来讲,阻塞和等待有一些区别,等待会造成当前线程阻塞,如果是同步调用,需要被唤醒,才可继续执行。而阻塞也包含其他阻塞,比如在获取一个排他锁时,如果其他线程已经获取资源锁,则当前线程被阻塞,等他释放锁
- 结束(Terminalted),线程已经结束
JAVA中线程使用
JAVA中使用线程主要有三种方式
Thread
# 继承Thread
static class ThreadTest extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ", Thread example.....");
}
}
复制代码
Runnable
# 实现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
,两人最终总计搬运和将与原货物总量不一致。
所以人多了,事也多了,并发编程时,我们需要考虑线程安全,上下文切换等常见问题,这些将在后续章节呈现。