概念
进程
- 正在运行的程序,是系统进行资源分配和调用的独立单位
- 每一个进程都有它自己的内存空间和系统资源,一个进程包括由操作系统分配的内存空间,包含一个或多个线程
- 一个进程一直运行,直到所有的非守护线程都结束运行后才能结束
线程
- 线程是进程中的单个顺序控制流,是一条执行路径
- 一个进程如果只有一条执行路径,则称为「单线程程序」
- 一个进程如果有多条执行路径,则称为「多线程程序」
- 一个线程不能独立的存在,它必须是进程的一部分
- 线程是 CPU 调度的最小单位
Java 中对于线程的描述是 Thread
,其中,封装了线程的相关信息,最重要的有需要执行的任务的信息;
线程的交互
互斥和同步:
- 互斥是当一个线程正在运行时,其他的线程只能等待,当完成后就可以运行了。
- 同步是两个或多个线程同时进行运行。
线程的状态
当你需要使用 Java 线程在多线程环境下进行编程时,理解 Java 的线程周期与线程的状态是非常重要的。
线程分为五个阶段:
- 创建(new)状态: 准备好了一个多线程的对象
- 就绪(runnable)状态: 调用了
start()
方法, 它的状态变为Runnable
(可运行的)。控制权被给予线程调度程序来完成它的执行。 是否立即运行此线程或在运行之前将其保持在可运行线程池中,取决于线程调度程序的实现,等待 CPU 进行调度 - 运行(running)状态: 执行
run()
方法,线程正在执行。线程调度程序从可运行线程池中选择一个线程,并将其状态更改为正在运行,然后 CPU 开始执行这个线程。 - 阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用。
- 终止(dead)状态: 一旦线程完成执行,它的状态就变成 Dead,线程销毁
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的 JVM 内存区域划分一篇博文中知道程序计数器、Java 栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取 CPU 执行时间,也许此时 CPU 正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting
(睡眠或等待一定的事件)、waiting
(等待被唤醒)、blocked
(阻塞)。
当由于突然中断或者子任务执行完毕,线程就会被消亡。
在有些教程上将 blocked、waiting、time waiting 统称为阻塞状态,这个也是可以的,只不过这里我想将线程的状态和 Java 中的方法调用联系起来,所以将 waiting 和time waiting 两个状态分离出来。
注: sleep 和 wait 的区别:
- sleep 是 Thread 类的方法,wait 是 Object 类中定义的方法.
- Thread.sleep 不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么 Thread.sleep 不会让线程释放锁.
- Thread.sleep 和 Object.wait 都会暂停当前的线程. OS 会将执行时间分配给其它线程. 区别是, 调用 wait 后, 需要别的线程执行 notify/notifyAll 才能够重新获得 CPU 执行时间.
参考:
多线程的意义
- 多进程的意义? 提高CPU的使用率
- 多线程的意义? 提高应用程序的使用率
Java 程序运行原理
Java 命令会启动 Java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个「主线程」,然后主线程去调用某个类的 main
方法。
所以,main
方法运行在主线程中。未采用多线程时,程序都是单线程的。
扩展阅读
线程实现方式
线程创建方式——继承 Thread 类
- 子类继承 Thread 类,子类中覆盖父类方法的
run
方法,将线程运行的代码放在run
方法中; - 创建子类的实例,线程被创建;
- 调用子类的实例的
start
方法,开启线程;
public class Test {
public static void main(String[] args) {
System.out.println("主线程ID:"+Thread.currentThread().getId());
MyThread thread1 = new MyThread("thread1");
thread1.start();
MyThread thread2 = new MyThread("thread2");
thread2.run();
}
}
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
}
}
执行结果:
主线程ID:1
name:thread2 子线程ID:1
name:thread1 子线程ID:10
同个线程的 start 方法重复调用的话,会出现 java.lang.IllegalThreadStateException
异常
线程创建方式——实现 Runnable 接口
- 子类实现
Runnable
接口,覆盖接口中的run
方法; - 通过
Thread
类创建线程,并将实现了Runnable
接口的子类对象作为Thread
类的参数; - 通过 Thread 类的实例对象调用
start
方法,开启线程;
public class Test {
public static void main(String[] args) {
System.out.println("主线程ID:"+Thread.currentThread().getId());
MyRunnable runnable1 = new MyRunnable("thread1");
Thread thread1 = new Thread(runnable1);
thread1.start();
MyRunnable runnable2 = new MyRunnable("thread2");
Thread thread2 = new Thread(runnable2);
thread2.run();
}
}
class MyRunnable implements Runnable{
private String name;
public MyRunnable(String name){
this.name = name;
}
@Override
public void run() {
System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
}
}
执行结果:
主线程ID:1
name:thread2 子线程ID:1
name:thread1 子线程ID:10
不管是扩展 Thread
类还是实现 Runnable
接口来实现多线程,最终还是通过 Thread
的对象的API 来控制线程的,熟悉 Thread
类的 API 是进行多线程编程的基础。
在 Java 中,这 2 种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承 Thread
类的话,可能比实现 Runnable
接口看起来更加简洁,但是由于 Java 只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现 Runnable
接口。
线程创建方式——实现 Callable 接口
还有一种是实现 Callable 接口,并与 Future、线程池结合使用
Thread 常用方法
getName()
获取当前线程的名称Thread.currenthrad().getName
;
继承Thread类,getName()获取当前线程名称。
实现Runnable接口,Thread.currentThread().getName();获取当前线程名称。
Thread.join()
让其他线程等待当前线程执行完之后再执行,比如,当前线程在另外一个线程的run
方法中,如果不加join
方法,那么,当前线程可能未执行完毕,其他线程就会往下执行了;Thread.sleep(1000)
休眠 1 秒;Thread.yield();
让出处理器时间,「线程们」去竞争吧volatile
关键字,保证了线程可以正确的读取其他线程写入的值,如下示例:
FAQ
启动一个线程是 run() 还是 start()?它们的区别?
start()
启动一个线程;
run
方法封装了被线程执行的代码,直接调用仅仅是普通方法的调用;start
启动线程,并有 JVM 自动调用run
方法;
Thread和Runnable的区别
如果一个类继承 Thread
,则不适合资源共享。但是如果实现了 Runable
接口的话,则很容易的实现资源共享。
总结,实现 Runnable
接口比继承 Thread
类所具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免 Java 中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现
Runable
或callable
类线程,不能直接放入继承Thread
的类
提醒一下大家:main
方法其实也是一个线程。在 Java 中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到 CPU 的资源。
在 Java 中,每次程序运行至少启动 2 个线程。一个是 main
线程,一个是垃圾收集线程。因为每当使用 Java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 实际就是在操作系统中启动了一个进程。
如何正确的停止 Java 中的线程
不要使用 stop
方法,因为不清楚哪些工作已经做了、哪些还没做、清理工作还没做……戛然而止的现象!
正确的做法:设置退出旗标
使用退出标志停止线程执行的方式的好处在于:
- 可以使线程执行完对应的操作后,因不符合继续执行的条件而停止
- 我们可以做一些线程执行结束后的清理工作
- 使线程的结束执行看起来是有次序的,而非戛然而止
interrupt
方法不是停止线程的正确方法,原因是 interrupt()
方法初衷不是用于停止线程。interrupt()
方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被 Object.wait
, Thread.join
和 Thread.sleep
三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException
),从而提早地终结被阻塞状态,然后该线程还是继续运行的