线程与进程
进程
进程是对运⾏时程序的封装,是系统进⾏资源调度和分配的基本单位,实现了操作系统的并发。程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存(例如硬盘上有个程序叫QQ.exe,这是一个程序,当你双击它,登录进去了,这个时候叫做一个进程。进程相对于程序来说它是一个动态的概念)。
线程
线程是进程的⼦任务,是 CPU 调度和分派的基本单位,实现了进程内部的并发。线程本身基本上不拥有系统资源,只是拥有一些在运行时需要用到的系统资源,例如程序计数器,寄存器和栈等。一个进程中的所有线程可以共享进程中的所有资源。
多线程
多线程可以理解为在同一个程序中能够同时运行多个不同的线程来执行不同的任务,这些线程可以同时利用CPU的多个核心运行。多线程编程能够最大限度的利用CPU的资源。如果某一个线程的处理不需要占用CPU资源时(例如IO线程),可以使当前线程让出CPU资源来让其他线程能够获取到CPU资源,进而能够执行其他线程对应的任务,达到最大化利用CPU资源的目的。
进程与线程的区别
进程:有独立内存空间,每个进程中的数据空间都是独立的。
线程:多线程之间堆空间与方法区是共享的,但每个线程的栈空间、程序计数器是独立的,线程消
耗的资源比进程小的多。
并行与并发
并发(Concurrent):同一时间段,多个任务都在执行 ,单位时间内不⼀定同时执行。
并行(Parallel):单位时间内,多个任务同时执行,单位时间内一定是同时执行。并行上限取决
于CPU核数(CPU时间片内50ms)
注意:并发是一种能力,而并行是一种手段。当我们的系统拥有了并发的能力后,代码如果跑在多核
CPU上就可以并行运行。所以咱们会说高并发处理,而不会说高并行处理。并行处理是基于硬件CPU的
是固定的,而并发处理的能力是可以通过设计编码进行提高的。
什么是线程上下文切换
单核CPU内核,同一时刻只能被一个线程使用。为了提升CPU利用率,CPU采用了时间片算法将CPU时间片轮流分配给多个线程,每个线程分配了一个时间片(几十毫秒/线程),线程在时间片内,使用CPU执行任务。当时间片用完后,线程会被挂起,然后把 CPU 让给其它线程。
线程再次运行时,系统是怎么知道线程之前运行到哪里?
- CPU切换前会把当前任务状态保存下来,用于下次切换回任务时再次加载。
- 任务状态的保存及再加载的过程就叫做上下文切换。
任务状态信息保存在哪里呢?
- 程序计数器:用来存储CPU正在执行的指令的位置,和即将执行的下一条指令的位置。
- 他们都是CPU在运行任何任务前,必须依赖的环境,被叫做CPU上下文。
上下文切换过程:
- 挂起当前任务,将这个任务在 CPU 中的状态(上下文)存储于内存中的某处。
- 恢复一个任务,在内存中检索下一个任务的上下文并将在 CPU 的寄存器中恢复。
- 跳转到程序计数器所指定的位置(即跳转到任务被中断时的代码行)。
线程上下文切换会有什么问题呢?
过多的线程并行执行会导致CPU资源的争抢,产生频繁的上下文切换,常常表现为高并发执行时,RT延
长。因此,合理控制上下文切换次数,可以提高多线程应用的运行效率。(也就是说线程并不是越多越
好,要合理的控制线程的数量。)
直接消耗:指的是CPU寄存器需要保存和加载,系统调度器的代码需要执行
间接消耗:指的是多核的cache之间得共享数据,间接消耗对于程序的影响要看线程工作区操作数
据的大小。
线程的创建方式
在Java中,实现线程的方式大体上分为三种,通过继承Thread类、实现Runnable接口,实现Callable接口。
继承Thread类
public class ThreadStyle extends Thread{@Overridepublic void run() {System.out.println("用Thread类实现线程");}public static void main(String[] args) {new ThreadStyle().start();}
}
实现 Runnable 接⼝
public class RunnableStyle implements Runnable{public static void main(String[] args) {Thread thread = new Thread(new RunnableStyle());thread.start();}@Overridepublic void run() {System.out.println("用Runnable方法实现线程");}
}
实现 Callable 接⼝
public class CallableStyle implements Callable<String> {@Overridepublic String call() throws Exception {return "Hello, calling";}public static void main(String[] args) {//创建异步任务FutureTask<String> task = new FutureTask<String>(new CallableStyle());//启动线程new Thread(task).start();try {//等待执⾏完成,并获取返回结果String result = task.get();System.out.println(result);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}
线程的生命周期
在操作系统中,线程被视为轻量级的进程,所以线程状态其实和进程状态是⼀致的。
查看Thread源码,能够看到java的线程有六种状态:
public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}
线程的生命周期可以总结为下图:
New
处于 NEW 状态的线程此时尚未启动。这⾥的尚未启动指的是还没调⽤ Thread 实例的 start() ⽅法。
public class StateThreadNew {public static void main(String[] args) {Thread thread = new Thread(() -> {});System.out.println(thread.getState()); // 输出 NEW}
}
public class NewRunnableTerminated implements Runnable{public static void main(String[] args) {Thread thread = new Thread(new NewRunnableTerminated());//打印出New状态System.out.println(thread.getState());thread.start();System.out.println(thread.getState());try {Thread.sleep(100);}catch (InterruptedException e) {e.printStackTrace();}//打印出Runnable的状态,即使是正在运行,也是Runnable,而不是Running//如果休眠100秒是terminated状态System.out.println(thread.getState());}@Overridepublic void run() {for (int i = 0; i< 1000; i++) {System.out.println(i);}}
}
从上⾯可以看出,只是创建了线程⽽并没有调⽤ start ⽅法,此时线程处于 NEW 状态。
RUNNABLE
可运行状态,可运行状态可以包括:运行中状态和就绪状态。
BLOCKED
阻塞状态,处于这个状态的线程需要等待其他线程释放锁或者等待进入synchronized。
WAITING
表示等待状态,处于该状态的线程需要等待其他线程对其进行通知或中断等操作,进而进入下一个状态。
调⽤下⾯这 3 个⽅法会使线程进⼊等待状态:
Object.wait() :使当前线程处于等待状态直到另⼀个线程唤醒它;
Thread.join() :等待线程执⾏完毕,底层调⽤的是 Object 的 wait ⽅法;
LockSupport.park() :除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度。
TIME_WAITING
超时等待状态。可以在一定的时间自行返回。
调⽤如下⽅法会使线程进⼊超时等待状态:
Thread.sleep(long millis) :使当前线程睡眠指定时间;
Object.wait(long timeout) :线程休眠指定时间,等待期间可以通过 notify() / notifyAll() 唤醒;
Thread.join(long millis) :等待当前线程最多执⾏ millis 毫秒,如果 millis 为 0,则会⼀直执⾏;
LockSupport.parkNanos(long nanos) : 除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度指定时
间;LockSupport 我们在后⾯会细讲;
LockSupport.parkUntil(long deadline) :同上,也是禁⽌线程进⾏调度指定时间;
TERMINATED
终止状态,当前线程执行完毕。