文章目录
多线程基础
一、基本概念
1、程序、进程、线程
1)程序与进程
# 区别
1. 程序是一组指令的有序集合,是一个静态的实体。
2. 进程是程序从创建、运行到消亡的一次执行过程,是一个动态的实体。
# 联系
1. 程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行,
这种执行的过程称之为进程。
2. 进程可以视为程序的一个实例,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),
部分只可以运行一个实例进程(例如360安全卫士)
# 包含关系
1. 当一个指令被运行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程
一个程序没有对应的进程 -> 程序没有执行
一个程序有多个对应进程 -> 程序运行在不同的数据集
2. 一个进程有且只有一个与之对应的程序。
2)进程与线程
# 区别
1. 进程是 操作系统 资源分配和调度的基本单位。
2. 线程是 处理器 任务调度和执行的基本单位。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
# 内存分配
进程之间的地址空间和资源是相互独立的。
同一进程的线程共享本进程的地址空间和资源。
# 影响关系
一个进程崩溃后,在保护模式下不会对其他进程产生影响。
一个线程崩溃后,整个进程都会死掉。
# 包含关系
一个进程至少包含一个线程,若有多个线程,则执行过程是多条线共同完成的。
线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
# 执行过程
每个独立的进程有程序运行的入口、顺序执行序列和程序出口。
但线程不能独立执行,必须依存于应用程序,由应用程序提供多个线程执行控制。
# 资源开销
每个进程都有独立的代码和数据空间(程序上下文),程序间的切换开销较大。
每个线程都有自己独立的运行栈和程序计数器(PC),线程间的切换开销较小。
3)Java中的进程和线程
- 进程:
- Java程序的执行就是一个进程的开启,该进程至少包含两个线程:
- 主线程/main()方法线程
- 垃圾回收机制线程
- 有独立的内存空间,进程间的数据存放空间是独立的。
- Java程序的执行就是一个进程的开启,该进程至少包含两个线程:
- 线程:
- 堆空间、方法区共享,栈空间独立
- 线程消耗的资源比进程小的多,因此在开发多任务运行时,通常考虑创建多线程而非多进程。
- Java中线程的执行取决于JVM的调度,这也造成了多线程的随机性(线程之间存在上下文切换)
4)查看进程和线程
【Windows】
任务管理器 可以查看进程和线程数,也可以用来杀死进程
tasklist 查看进程
taskkill 杀死进程
【Linux】
ps -ef 查看所有进程
ps -fT -p [pid] 查看某个进程(pid)的所有线程
kill 杀死进程
top 实时显示进程状态
top -H -p [pid] 查看某个进程(pid)的所有线程
【Java】
jps 列出正在运行的 Java 进程
jstack [pid] 生成指定 Java 进程的线程转储信息
jconsole 用于监视和管理 Java 虚拟机的图形化工具
2、并发 & 并行
引用 Rob Pike 的一段描述:
- 并发(concurrent)是同一时间处理(dealing with)多件事情的能力,
- 并行(parallel)是同一时间做(doing)多件事情的能力
举个例子:
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这就是并发。
- 雇了3个保姆,一个做饭、一个打扫卫生、一个喂奶,互不干扰,这就是并行。
- 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行。
图示说明:
单核 cpu 下,线程实际还是
串行执行
的。这种多个线程轮流使用 CPU 的做法称为并发(concurrent)
任务调度器
将 cpu 的时间片分给不同线程使用,只是由于线程间的切换非常快(时间片很短),给人感觉是同时运行的。
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行(parallel)的。
3、同步 & 异步
- 同步:需要等待结果返回,才能继续运行
- 异步:不需要等待结果返回,就能继续运行
多线程可以让方法执行变为异步的,将一些耗时操作异步执行,避免阻塞主线程。
二、线程的运行原理
1、栈与栈帧
Java 虚拟机栈 描述的是 Java方法 执行的内存模型:
- 每个线程 在创建时 都会创建一个虚拟机栈(线程私有),其内部保存一个个的栈帧(Stack Frame)。
- 每个方法 被调用时 都会创建一个栈帧(Stack Frame),存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 每个线程 只能有一个活动栈帧/当前栈帧,对应着当前正在执行的那个方法(即当前方法)。
2、执行图解
public class FrameTest {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}
方法执行结束,对应的栈帧就会出栈(对应的内存也会被释放掉),并更新程序计数器。
3、上下文切换 Context Switch
以下原因会导致 CPU 不再执行当前的线程,转而执行另一个线程的代码:
- 线程的 CPU 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了
sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法
当上下文切换(Context Switch)发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态。
- JVM 中
程序计数器
(线程私有)记录了下一条 jvm 指令的执行地址。
频繁的上下文切换(Context Switch)会影响性能。
三、线程相关API
1、构造方法
Java使用java.lang.Thread
类代表线程
,所有的线程对象都必须是Thread
类或其子类的实例。
构造函数 | 功能说明 |
---|---|
Thread() | 创建一个新的线程 |
Thread(String name) | 创建一个新的线程,并指定线程名 |
Thread(Runnable target) | 创建一个新的线程,带有指定任务 |
Thread(Runnable target, String name) | 创建一个新的线程,带有指定任务,并指定线程名 |
2、常用API
public class Thread implements Runnable {
// 获取当前线程对象
public static native Thread currentThread();
// 判断线程是否处于活跃状态(true:正在运行 false:运行完毕)
public final native boolean isAlive();
// 启动线程,JVM调用此线程的run方法
public synchronized void start() {...}
// 获取当前线程id(id唯一,默认是'Thread-编号',编号从0开始)
public long getId() {...}
// 获取当前线程名称
public final String getName() {...}
// 设置当前线程名称
public final synchronized void setName(String name) {...}
// 获取线程的状态(Thread.State枚举类,6种)
public State getState() {...}
// 获取活跃线程数(默认只有 main线程 和 垃圾回收线程)
public static int activeCount() {...}
}
3、守护线程
// 设置当前线程为守护线程(必须在线程start之前设置)
public final void setDaemon(boolean on) {...}
// 判断当前线程是否为守护线程
public final boolean isDaemon() {...}
Java中的线程分为两种:用户线程
和 守护线程
守护线程:
程序运行时,在后台提供一种通用服务的线程,比如垃圾回收线程。(不属于程序中不可或缺的部分)
进程何时关闭:
进程中的所有线程都执行完毕 或 进程中只剩下守护线程
注意事项:
1. 正在运行的常规线程不能设置为守护线程,否则会抛出IllegalThreadStateException异常。
2. 在Daemon线程中产生的新线程也是Daemon的。
3. 守护线程不应该访问固有资源,如文件、数据库。因为它会在任何时候甚至在一个操作的中间发生中断。
当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。
public class DaemonThreadTest {
public static void main(String[] args) {
// 守护线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(200);
System.out.println("t1 输出" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.setDaemon(true);
t1.start();
// 用户线程
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 输出" + i);
}
});
t2.start();
}
}
4、线程的优先级
// 设置当前线程的优先级1~10(设置的只是概率,不是一定的)
public final void setPriority(int newPriority) {...}
// 返回当前线程的优先级
public final int getPriority() {...}
线程的调度:
- 多个线程执行时,线程会被JVM控制以某种方式执行,我们把这种情况称之为线程调度。
- JVM采用的是抢占式调度(竞争CPU时间片),因此多个线程之间会发生上下文切换。
线程的优先级:
- 线程的优先级从低到高被划分为1-10级,线程提供了3个常量来表示线程的优先级:
- 最低:
Thread.MIN_PRIORITY
1 - 默认:
Thread.NORM_PRIORITY
5 - 最高:
Thread.MAX_PRIORITY
10
- 最低:
- 线程优先级会提示(hint)调度器优先调度该线程,但它 仅仅是一个提示,调度器可以忽略它。
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 空闲时,优先级几乎没作用。
public class ThreadPriorityTest {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
try {
System.out.println("thread 输出" + i);
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
}
四、线程的创建(三种方式)
1、继承Thread类
public class InheritCreate {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
// 继承Thread类,重写run方法
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("输出打印"+i);
}
}
}
}
2、实现Runnable接口(无返回值)
public class RunnableCreate1 {
public static void main(String[] args) {
MyRunnable task = new MyRunnable(); // 任务对象
Thread thread = new Thread(task);
thread.start();
}
// 实现Runnable接口,重写run方法(不带返回值的任务)
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("输出:"+i);
}
}
}
}
public class RunnableCreate2 {
public static void main(String[] args) {
// 匿名内部类 - 创建Runnable实例
Thread t = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("输出"+i);
}
}
});
t.start();
}
}
public class RunnableCreate3 {
public static void main(String[] args) {
// lambda表达式 - 创建Runnable实例
Thread t = new Thread(()->{
for (int i = 0; i < 1000; i++) {
System.out.println("输出"+i);
}
});
t.start();
}
}
线程对象调用 start()
方法,最终执行的都是 重写的 run()
方法。(源码比较简单,这里就省略了)
3、实现Callable接口(有返回值)
Callable接口类似于Runnable,区别在于Runable执行后,不能返回结果;而 Callable 如果有结果,可以返回。
public class CallableCreate {
public static void main(String[] args) {
// FutureTask包装Callable任务,FutureTask可以用于获取执行结果
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
// 创建线程执行Callable任务(结果会被缓存,提高效率)
new Thread(futureTask, "A").start();
// 获取线程的执行结果(可能会阻塞,放到后面 或 使用异步处理)
try {
Integer num = futureTask.get();
System.out.println("得到线程处理结果:" + num);
} catch (Exception e) {
e.printStackTrace();
}
}
// 实现Callable接口,实现call()方法(带返回值的任务)
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
int num = 0;
for (int i = 0; i < 1000; i++) {
System.out.println("输出" + i);
num += i;
}
return num;
}
}
}
FutureTask
是RunnableFuture
的实现类,而RunnableFuture
是Runnable
的子接口
public class FutureTask<V> implements RunnableFuture<V> {
// FutureTask构造方法可以传入Callable
public FutureTask(Callable<V> callable) {}
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
4、源码分析
如果创建Thread
时指定了Runnable target
,则最终执行的都是 target
重写的 run()
方法。
5、创建方式的比较
继承Thread类:多个线程分别完成各自的任务
- 存在单继承的局限性
实现Runnable/Callable接口:多个线程共同完成一个任务 或 多个线程分别完成不同的任务
- 避免了Java单继承的局限性。
- 实现 线程代码(Thread) 和 任务代码模块(Runnable/Callable) 的解耦合,提升了代码的扩展性。
- 体现数据共享的概念(JMM内存模型图),代码可以被多个线程共享,代码和数据独立