Java进程线程
进程与线程
基础概念
并发(concurrent):是同一时间应对(dealing with)多件事情的能力
并行(parallel):是同一时间动手做(doing)多件事情的能力
同步:需要等待结果返回,才能继续运行
异步:不需要等待结果返回,就能继续运行
进程
概念
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐等)。
线程
概念
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器北
- 与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈
创建和运行线程
直接使用Thread
1
2
3
4
5
6
7
8// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();使用 Runnable 配合 Thread
1
2
3
4
5
6
7
8
9Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();把线程和任务分开了更灵活,更容易与线程池等高级 API 配合
FutureTask 配合 Thread
1
2
3
4
5
6
7
8// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task, "t1").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task.get();FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
使用线程池获取线程
重点方法
- Sleep(n):让当前线程Running 进入 Timed Waiting 状态,并且不释放锁(阻塞)
- 其它线程可以使用这个线程的interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出InterruptedException
- 睡眠结束后,会进入就绪状态等待cpu调度
- 相比于
Thread.sleep(n)
来说TimeUnit.SECONDS.sleep(n)
可读性更好
- yield():调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- join([long n]):等待线程运行结束,最多等待 n毫秒(该参数可选)
- 例如:
t1.join()
主要是让本线程与t1同步,等待其执行完毕
- 例如:
- interrupt():打断线程的阻塞状态并让其抛出InterruptedException,并且打断标记(在sleep、wait、join状态下记为false,正常运行状态的线程会从false变为true)
- isInterrupted():判断是否被打断,不会清除打断标记
- interrupted():判断是否被打断,会清除打断标记
- setDaemon(true):设置线程为守护线程,守护线程只要其它非守
护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。 - LockSuport.park()/LockSuport.unpark(线程对象):暂停当前线程/恢复某个线程(unpark可以放在park之前同样能恢复,原理是因为底层每个线程关联了一个Parker对象,里面有一个_counter,调用unpark让其变为1,只要其为1,park之后线程任然会被恢复)
线程状态
- 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态:(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 运行状态:指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 阻塞状态:如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态,等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
Java中有6种状态:
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束
转换流程:
NEW --> RUNNABLE
:调用 t.start() 方法
RUNNABLE <--> WAITING
:
wait()/notify()[notifyAll()、interrupt()]
- wait():
RUNNABLE --> WAITING
- notify()[notifyAll()、interrupt()]:如果竞争锁成功则
WAITING --> RUNNABLE
,否则WAITING --> BLOCKED
join():调用join()**时状态变为
RUNNABLE --> WAITING
,当线程结束或者调用interrupt()**会从WAITING --> RUNNABLE
LockSupport.park()/LockSupport.unpark(目标线程):
- LockSupport.park():
RUNNABLE --> WAITING
- LockSupport.unpark(目标线程)/interrupt():
WAITING -->RUNNABLE
RUNNABLE <--> TIMED_WAITING
:
- obj.wait(long n)
- t.join(long n)
- Thread.sleep(long n)
- LockSupport.parkUntil(long millis)
其中wait会释放锁,sleep和park不会
RUNNABLE <--> BLOCKED
:竞争锁失败RUNNABLE --> BLOCKED
,反之BLOCKED --> RUNNABLE
RUNNABLE <--> TERMINATED
:当前线程所有代码运行完毕或者异常中断,进入TERMINATED
两者区别
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist | findstr (进程名,例如java)
查看进程 或者netstat -aon|findstr "8080"
查看端口taskkill /pid 进程号 -t -f
杀死进程
linux
- ps -fe 查看所有进程
- ps -fT -p
查看某个进程(PID)的所有线程 - kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p
查看某个进程(PID)的所有线程
专门查看Java进程:
- jps 命令查看所有 Java 进程
- jstack
查看某个 Java 进程(PID)的所有线程状态 - jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
总结
- 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活。
- 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的:
- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分。
- 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义。
- IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 goMars的学习随记!
评论