一、进程和线程
进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当启动 main 方法时其实就是启动了一个 JVM 的进程,而 main 方法所在的线程就是这个进程中的一个线程,称为主线程。
进程是资源分配的基本单位。每一个进程都有它自己的内存空间和系统资源。
线程
线程与进程相似,但线程是一个比进程更小的执行单位,是进程中的单个顺序控制流,是一条执行路径。
一个进程如果只有一条执行路径,则称为单线程程序;一个进程如果有多条执行路径,则称为多线程程序。
一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java 程序是多线程程序,这是因为 JVM 启动至少了主线程和垃圾回收线程。
进程和线程的区别
-
资源
一个进程中可以有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器、虚拟机栈 和本地方法栈。
-
是否独立
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。
-
开销
线程执行开销小,但不利于资源的管理和保护,而进程正相反。
思考 1:程序计数器线程私有?
程序计数器是主要有以下两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
思考 2 :Java 虚拟机栈、本地方法栈线程私有?
- Java 虚拟机栈中每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈和虚拟机栈所发挥的作用非常相似,它们之间的区别只不过是本地方法栈为本地方法服务。
所以,为了保证线程中的局部变量不被别的线程访问到,Java 虚拟机栈和本地方法栈是线程私有的。
多进程与多线程
多进程的目的是提高 CPU 的使用率。
多线程的目的是提高应用程序的使用率。
多线程共享同一进程资源(堆内存和方法区),但是栈内存是独立的,JVM 中一个线程对应一个线程栈。多线程抢夺的是 CPU 资源,一个时间点上只有一个线程运行。
上下文切换
线程在执行过程中会有自己的运行条件和状态,即上下文,比如程序计数器,栈信息等。当出现以下几种情况的时,线程会从占用 CPU 状态中退出:
- 主动让出 CPU,比如调用了
sleep()
,wait()
等。 - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场,并加载下一个将要占用 CPU 的线程上下文。这就是上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
并发与并行
并发:一个处理器同时处理多个任务,是逻辑上的同时发生。
并行:多个处理器或者多核处理器同时处理多个不同的任务,是物理上的同时发生。
下图反映了一个包含 8 个操作的任务在一个有两核心的 CPU 中创建四个线程运行的情况。 假设每个核心有两个线程,那么每个CPU中两个线程会交替并发,两个CPU之间的操作会并行运算。
单就一个 CPU 而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高, 多 CPU 的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。
多线程的好处
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
多线程带来的问题
并发编程的目的就是为了能提高程序的执行效率,从而提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
二、创造线程
三种创造线程的方式
Java 有 3 种创建线程的方式:
- 继承 Thread 类,重写 run() 方法
- 实现 Runnable 接口,重写 run() 方法
- 实现 Callable 接口,重写 call() 方法
继承 Thread 类
需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
实现 Runnable 接口
需要实现接口中的 run() 方法。使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
实现 Callable 接口
需要实现接口中的 call() 方法。 call() 方法有返回值,返回值通过 FutureTask 进行封装。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 1;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc); // 返回值通过 FutureTask 进行封装
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
选择线程创建方式
实现接口会更好一些:
-
避免单继承带来的局限:
Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
-
代码能够被多个线程共享
-
适合多个相同程序的线程处理同一资源的情况
-
使用接口实现的线程可以对线程进行复用(线程池中创建的线程就是实现 Rubnnable 接口的):
Runnable 是轻量级的,重复 new 不会耗费太多资源。
Thread 是重量级的,并且线程执行结束完后就销毁了,无法再次利用。
注意:一个线程只能 start 一次,如果多次 start,会出现 java.lang.IllegalException
。
Runnable 和 Callable 的区别
-
Callable 的实现方法是 call()
Runnable 的实现方法是 run()
-
Callable 任务执行后有返回值。Callable 与 Future 配合使用,运行 Callable 任务和获取一个 Future 对象 Future 表示异步计算后的结果;
Runnable 任务执行后是没有返回值的
-
Callable 的 call() 方法可以抛出异常
Runnable 的 run() 方法不能抛出异常
实现处理线程的返回值
方式一:主线程等待法
如果未获取到值,则主线程等待,一直到获取值为止。
public class CycleWait implements Runnable{
private String value;
@Override
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "we have data now";
}
public static void main(String[] args) throws InterruptedException {
CycleWait cycleWait = new CycleWait();
Thread t = new Thread(cycleWait);
t.start();
//TODO:如果未获取到值,则主线程等待,一直到获取值为止。
while (cycleWait.value == null){ //主线程等待法
Thread.sleep(1000);
}
System.out.println("value:"+cycleWait.value);
//输出结果:
//value:we have data now
}
}
方式二:join()
使用 Thread 的 join() 阻塞当前线程以等待子线程处理完毕。
public class CycleWait2 implements Runnable{
private String value;
@Override
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "we have data now";
}
public static void main(String[] args) throws InterruptedException {
CycleWait2 cycleWait = new CycleWait2();
Thread t = new Thread(cycleWait);
t.start();
//TODO:使用 Thread 的 join 方法阻塞当前线程,等待子线程执行完毕
t.join(); //阻塞当前主线程,直到 t 线程执行完毕
System.out.println("value:"+cycleWait.value);
//输出结果:
//value:we have data now
}
}
方式三:Callable + FutureTask
实现 Callablee 接口,使用 FutureTask 接收 call() 方法返回的数据。
public class CycleWait3 implements Callable<String>{
private String value;
@Override
public String call() throws Exception {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "we have data now";
return value;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
CycleWait3 cycleWait = new CycleWait3();
FutureTask ft = new FutureTask(cycleWait);
Thread t = new Thread(ft);
t.start();
while (!ft.isDone()){
Thread.sleep(1000);
}
// FutureTask 的 get() 方法会一直阻塞,一直会等待任务执行完毕,才返回。
System.out.println("value:"+ft.get());
}、
方式四:线程池 + Future
线程池执行任务,返回的结果使用 Future 接收。
public class CycleWait4 implements Callable<String>{
private String value;
@Override
public String call() throws Exception {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "we have data now";
return value;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService service = Executors.newCachedThreadPool();
CycleWait4 cycleWait = new CycleWait4();
//将任务提交给线程池处理
Future<String> future = service.submit(cycleWait);
while (!future.isDone()){
Thread.sleep(1000);
}
System.out.println("value:"+future.get());
//关闭线程池
service.shutdown();
}
}
三、基础线程机制
Executor
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
主要有三种 Executor:
- CachedThreadPool:一个任务创建一个线程;
- FixedThreadPool:所有任务只能使用固定大小的线程;
- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new MyRunnable());
}
executorService.shutdown();
}
Daemon
Java 中有两类线程:
- 用户线程(User Thread):运行在前台的线程,比如主线程。
- 守护线程(Daemon Thread):程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
一旦所有的用户线程都退出了,虚拟机也就退出了,因此不要在守护线程中执行业务逻辑操作,因为随时都可能中断(甚至无法执行 finnally 中的语句)。
注意:在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
start() & run()
-
start()
启动一个新线程,新线程会执行 run() 方法;
start 不能被重复调用,如果重复调用,会抛
java.lang.IllegalException
。 -
run()
和一个成员方法相同,可以被重复调用;
单独调用 run() 不会启动新线程
sleep() & wait()
sleep() 和 wait() 的区别:
-
持有者
sleep() 是 Thread 的静态方法;
wait() 是 Object 的成员方法,所以任何对象都可以调用 wait()
-
锁是否会释放
sleep() 会一直持有同步锁,继续占用 CPU 资源;
wait() 会释放同步锁,使得其他线程可以使用同步块或者同步方法
-
使用范围
wait() 只能在同步代码块或同步方法中使用,否则,会抛
java.lang.IllegalMonitorStateException
;sleep() 可在任何地方使用
yield() & wait()
Thread.yield() 声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
yield() 和 wait() 的区别:
-
yield() 是 Thread 的静态方法
wait() 是 Object 的成员方法
-
yield() 让当前线程由“运行状态”进入“就绪状态”;
wait() 让当前线程由“运行状态”进入“等待状态”
-
yield() 不会释放同步锁;
wait() 会释放同步锁,使得其他线程可以使用同步块或者同步方法
notify() & notifyAll()
锁池:线程 A 已经拥有某个对象的锁,线程 B、C 想要调用这个对象的同步方法,此时线程 B、C 就会被阻塞,进入一个地方去等待锁的释放,这个地方就是该对象的锁池。
等待池:假设线程 A 调用了某个对象的 wait() 方法,线程 A 释放该对象的锁,同时线程 A 进入等待池,进入等待池的线程不会竞争该对象的锁。
notifyAll() 让所有处于等待池的线程进入锁池去竞争锁。
四、线程生命周期和状态
6 种线程状态
新建 (new)
线程被创建,但是还没有调用 start() 方法
可运行 (Runnable)
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的运行状态(Running)和就绪状态( Ready)。
调用 start() 方法后开始运行,线程这时候处于 Ready 状态。Ready 状态的线程获得了 CPU 时间片后就进入Running 状态。
阻塞 (Bloced)
等待获取一个排它锁,如果其他线程释放了锁就会结束此状态。
无期限等待 (Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法 | 退出方法 |
---|---|
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
LockSupport.park() 方法 | LockSupport.unpark(Thread) |
期限等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 时间结束 |
设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() 方法 | LockSupport.unpark(Thread) |
LockSupport.parkUntil() 方法 | LockSupport.unpark(Thread) |
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于:
阻塞是被动的,它是在等待获取一个排它锁。
等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
死亡(Terminated)
可以是线程完成任务后自己结束,或者产生了异常而结束。
线程生命周期
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换:
线程创建之后它将处于NEW 状态,调用 start()
方法后开始运行,线程这时候处于 READY 状态。Ready 状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING 状态。
当线程执行 wait()
方法之后,线程进入 WAITING 状态。进入 WAITING 状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。
当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED 状态。
线程在执行 Runnable 的run()
方法之后将会进入到 TERMINATE 状态。
五、终止线程的几种方式
1、退出标志
使用退出标志,使线程正常退出,也就是 run 方法完成后线程终止。
需要 while() 循环以某种特定的条件下退出,最直接的方法就是设计一个 boolean 类型的标志,并且通过设置这个标志来控制循环是否退出。 一般需要加上 volatile 来保证标志的可见性。
2、Thread.stop()
Thread.stop() 强制终止线程(不建议使用)。
3.interrupt() 中断线程
-
线程处于阻塞状态,使用 interrupt() 则会抛出 InteruptedException 异常。
(线程处于阻塞状态)
-
使用 while(! isInterrupted()) {...} 来判断线程是否被中断,使用 interrupt() 则线程终止。
(线程处于运行状态)
综合线程处于“阻塞状态”和“运行状态”的终止方式,通用的终止线程的形式如下:
try{
while(!isInteruppted()){ //isInteruppted() 保证只要中断标记为 true,就终止线程
//...
}
}catch(InteruptedException e){
//保证当发生 InteruptedException 时,线程被终止。
}
举例如下:
public class InterruptExample implements Runnable{
@Override
public void run() {
int i = 0;
try {
//isInteruppted() 保证只要中断标记为 true,就终止线程
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(100);
i++;
System.out.println(Thread.currentThread().getName() + " (" +
Thread.currentThread().getState() + ") loop " + i);
}
} catch (InterruptedException e) {
// 保证当发生 InteruptedException 时,线程被终止。
System.out.println(Thread.currentThread().getName() + " (" +
Thread.currentThread().getState() + ") catch InterruptedException.");
}
}
public static void main(String[] args) throws InterruptedException {
Runnable interruptTask = new InterruptExample();
Thread t1 = new Thread(interruptTask, "t1");
System.out.println(t1.getName() +" ("+t1.getState()+") is new.");
t1.start(); // 启动“线程t1”
System.out.println(t1.getName() +" ("+t1.getState()+") is started.");
// 主线程休眠300ms,然后主线程给t1发“中断”指令。
Thread.sleep(300);
t1.interrupt();
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");
// 主线程休眠300ms,然后查看t1的状态。
Thread.sleep(300);
System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
}
}
t1 (NEW) is new.
t1 (RUNNABLE) is started.
t1 (RUNNABLE) loop 1
t1 (RUNNABLE) loop 2
t1 (TIMED_WAITING) is interrupted.
t1 (RUNNABLE) catch InterruptedException.
t1 (TERMINATED) is interrupted now.