1、进程与线程
1.1 进程与线程的基本概念
进程是程序基于特定数据集合的运行过程,是操作系统分配资源的基本单位。比如说我们打开QQ,操作系统就会基于QQ.exe这个程序创建一个进程,给它分配内存、端口等资源。
线程是进程的逻辑执行单元,也是CPU调度的基本单位。比如说QQ进程启动之后,我们需要把QQ的界面按照设计好的样子绘制出来,这个绘制操作就是在线程中执行的。在GUI编程中,通常(非绝对)只能在特定的线程进行绘制UI的操作,这个线程称为主线程或者UI线程。
1.2 多进程与多线程
多进程:操作系统中同一时间会存在多个进程,比如说我们打开了QQ、又打开了浏览器等等;或者我们经常会在Windows下多次打开QQ,登录不同的QQ,那么操作系统中也会存在多个基于QQ.exe创建的进程;又或者我们只打开了QQ,但是QQ的开发团队觉得哪怕只打开了一次、只登录一个QQ号也需要创建多个进程来为使用者提供更好的服务,那么也可以由程序员在代码中启动多个进程。多个进程之间可以根据需要使用管道、Socket、信号等方式进行通信以便协作完成某些任务。
多线程:在一个进程中,也可能会存在多个线程。还是用QQ作为例子,主线程负责绘制和更新UI,IO线程负责接收和发送消息。这是因为网络IO操作往往是非常耗时的,如果放在主线程去做就可能导致UI更新不及时而造成卡顿的现象。那么IO线程收发数据,但是展示数据是在住线程,所以线程间往往也是需要进行通信的。不同语言不同平台线程间通信有所不同,比如Java有等待唤醒机制、Android的Handler机制等。
2、串行、并行和并发
2.1 概念
串行:多个任务排队依次执行,同一时刻只有一个任务在执行;
并行:同一时刻,多个任务都在执行;
并发:在一个时间段内,多个任务都能执行,但是同一时刻可能只有一个任务在执行,也可能有多个任务同时在执行。
对于一个单核的CPU或者多核CPU的一个核心来说,同一时刻只能执行一个线程的任务,所以其他线程需要等待调度,这个过程可以看作是串行的。而多核的CPU,同一时刻每个核心都可以执行一个线程任务,所以这些正在执行的线程(不包括等待中的线程)是并行执行的。
CPU单个核心同一时刻只能执行一个线程,所以如果正在执行的线程是非常耗时的,比如IO线程等,在各个线程必须执行完才轮到下一个线程执行的模式下就可能会产生非常恶劣的影响,比如UI卡顿等。这时候就需要制定一种调度策略根据线程的不同状态对这些线程进行调度,使得一段时间内,多个线程都能获得执行权,这就是线程的并发。举个例子,假设现在有A、B、C三个线程,它们的执行情况可能是A先获得执行权,执行一段时间后,A线程的任务还没有完成,B获得执行权,一段时间后C获得执行权,在一段时间后A才重新获得执行权继续执行剩余的任务。这其中某一时刻谁会获得执行权是由线程状态和具体的调度策略决定的。
2.2 线程并发问题
多线程并发执行并非没有任何负面作用,首先线程调度本身就需要消耗时间和资源,其次在多个线程需要对同一资源进行操作的场景下如果操作不当就很可能导致异常的情况发生。假设A线程和B线程都需要操作打印机,A线程会先输出“AB“,然后执行一定的操作后再输出”CD“,B线程会输出”EF“,而我们预期的结果是打印机依次打印”ABCDEF“。但是在并发执行的情况下,A执行一段时间后输出”AB“,B可能会获得执行权,输出”EF“,然后A重新获得执行权,输出”CD“,这并不符合我们的预期。这就需要制定一种策略来防止类似的事情发生,这种策略就是锁策略。一个线程必须获取到锁才可以操作对应的资源,完成操作后再释放锁,然后其他的线程才能获取锁进而操作资源。
不正确地进行加锁和解锁操作,又可能造成死锁或者程序运行效率低下等后果。所以,多线程并发还有一个弊端就是增加了程序的复杂度,对开发者的要求也相对更高。但是总体来说,多线程并发对于大多数的应用场景来说是必不可少的。
3、Java中的线程基础
3.1 创建线程
Java中创建线程的方式有很多,这里只讨论最简单的几种。Java中线程的实体类是java.lang.Thread。在介绍如何创建线程之前,先介绍一个接口Runnable:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
这是一个函数式接口,它只定义了一个run方法。从命名上理解,Runnable是可运行接口,运行逻辑在run方法中。Thread类实现了Runnable接口,所以线程的执行逻辑在它的run方法中,我们要创建线程去执行任务,实际上就是规定Thread的run方法中应该执行什么。先来看一下Thread的run方法:
/* What will be run. */
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
可以看到,Thread类的run的方法非常简单,如果target对象不为空,调用它的run方法,否则啥也不干。由此得出创建线程的两种方式:
- 新建一个类继承自Thrad,重写它的run方法来定义执行逻辑,用这个新的类去实例化线程对象。
- 不新建Java类,创建Thread类对象并给target属性传值,在target的run方法中定义执行逻辑。
示例代码:
package com.sahooz.jucdemo;
public class ThreadCreationDemo {
public static void main(String[] args) {
// 使用继承的方式新建线程
MyThreadClass t1 = new MyThreadClass("t1");
// 使用传递Runnable的方式新建线程, ()->{}为lambda表达式方式简写函数式接口实现
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" + i);
try {
// 等待10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2");
t1.start();
t2.start();
}
}
class MyThreadClass extends Thread {
public MyThreadClass(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName() + "输出:" + i);
try {
// 等待10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
其中Thread.sleep()是线程等待执行的毫秒数,然后再继续接下来的逻辑。
输出:
t1输出:0
t2输出:0
t1输出:1
t2输出:1
t2输出:2
t1输出:2
t2输出:3
t1输出:3
t2输出:4
t1输出:4
可以看到t1和t2两个线程是并发执行的,而不是t1线程完全执行完成之后再执行t2线程。
3.2 线程状态
java.lang.Thread.State类定义了Java线程的几种状态:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
从注释中我们就可以得出这几种状态所代表的意义:
- NEW:尚未开始运行,也就是start()方法还没调用。
- RUNNABLE:已经开始运行了(已经调用start()了),可能正在执行也可能在等待CPU等资源。
- BLOCKED:阻塞状态,在等待监视器锁。处于阻塞状态的线程可能是在等待监视器锁以进入同步代码块或同步方法,也可能是调用了Object.wait()方法后等待再次进入同步代码块或同步方法。
- WAITING:等待状态,处于等待状态的线程在等待其他线程执行特定的操作,比如在一个线程中调用了Object.wait()后线程会等待其他线程执行Object.notify()或者Object.notifyAll()。
- TIMED_WAITING:有限时间的等待状态,与WAITING的区别是等待是不是有超时限制。
- TERMINATED:任务已经执行完成,线程已经结束了。
用一个示例来演示线程状态的变化:
package com.sahooz.jucdemo;
public class ThreadStateDemo {
private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// t1已经运行,所以这里输出应该是RUNNABLE
System.out.println(Thread.currentThread().getState().name());
try {
synchronized (obj) {
obj.wait(); // 在同步代码块中wait
// 运行到这里,其他线程已经调用了obj.notify()或者obj.notifyAll(),而且t1重新获得了监视器锁
// 所以这里的输出应该是RUNNABLE
System.out.println(Thread.currentThread().getState().name());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "MyThread-1");
// 只是new了对象,还没有调用start(),这里应该输出NEW
System.out.println(t1.getState().name());
t1.start();
// 等待t1执行到obj.wait()
Thread.sleep(100);
Thread t2 = new Thread(() -> {
// 此时t1已经调用了obj.wait(),正在等待其他线程调用obj.notify()或者obj.notifyAll()
// 所以这里输出应该是WAITING
System.out.println(t1.getState().name());
synchronized (obj) {
obj.notify();
// obj.notify()已经被调用,t1需要等待监视器锁以再次进入同步代码块
// 所以这里输出应该是BLOCKED
System.out.println(t1.getState().name());
}
}, "MyThread-2");
t2.start();
// 确保所有线程执行完成
Thread.sleep(500);
// 到这里,t1已经执行完成,输出应该是TERMINATED
System.out.println(t1.getState().name());
}
}
输出:
NEW
RUNNABLE
WAITING
BLOCKED
RUNNABLE
TERMINATED
其中synchronized会在后面的文章具体介绍,读者如果上不了解synchronized的用法,可以在了解后再回头看一次加深线程状态的理解。
3.3 线程的终止与中断
在一个线程任务的执行过程中,如果外部条件发生变化而导致任务没有执行下的的必要时,我们该如何终止线程的执行呢?
Thread.stop()
既然启动线程的方法是Thread.start(),那么是不是有终止线程对应的Thread.stop()呢?没错,是有的。但是Thread.stop()被标记为Deprecated,也就是被废弃了。同样被标记为废弃的还有一系列方法:
- Thread.stop():终止线程
- Thread.stop(Throwable):终止线程并抛出指定的异常
- Thread.destroy():销毁线程,并且不做任何清理操作
- Thread.suspend():挂起线程
- Thread.resume():与挂起线程对应,恢复线程
这里面有些方法还可以用,有些调用后会直接抛出异常而不执行任何逻辑。所以这一些列方法知道即可,甚至不知道也无所谓。
至于为什么要废弃这一系列方法,查阅这个说明。总的来说,就是这些方法可能会带来不可预测的异常后果或者要避免潜在的异常需要非常复杂的流程。
使用Thread.stop()去终止一个线程,有可能会造成以下不可预期的后果:
- 调用Thread.stop()线程立即终止运行了,某些资源可能没有正确地释放,比如各种IO流、数据库连接等。
- Thread.stop()会立即释放线程持有的锁,某些数据可能处于某种中间状态而被其他线程访问,可能导致非预期的结果。
示例代码:
package com.sahooz.jucdemo;
public class ThreadStopDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" + i);
try {
// 等待10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
t1.start();
Thread.sleep(20);
t1.stop();
}
}
手动控制线程
前文说到无论是通过继承的方式还是通过传递Runnable对象的方式创建线程,本质上都是定义run方法的执行逻辑。既然run的逻辑是我们自己定义的,那么我们完全可以通过条件判断是否应该继续执行某些代码,让不满足执行条件的代码不执行,让该执行的代码继续执行,而不是强制地直接终止运行。
示例代码
package com.sahooz.jucdemo;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
public class ThreadControlDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 打开输出流");
for (int i = 0; i < 10; i++) {
try {
// 模拟输出操作
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 写入文件");
}
System.out.println(Thread.currentThread().getName() + " 关闭输出流");
}, "t1");
t1.start();
AtomicBoolean shouldDoOutput = new AtomicBoolean(true);
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 打开输出流");
for (int i = 0; i < 10 && shouldDoOutput.get(); i++) {
try {
// 模拟输出操作
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 写入文件");
}
System.out.println(Thread.currentThread().getName() + " 关闭输出流");
}, "t2");
t2.start();
Thread.sleep(10 + new Random().nextInt(50));
// 外部条件发生变化,不应该再写入文件
// 通过stop让t1停止写入文件
t1.stop();
// 通过设置标志位让t2停止写入文件
shouldDoOutput.set(false);
// 等待线程运行结束
t1.join();
t2.join();
}
}
输出结果:
t1 打开输出流
t2 打开输出流
t1 写入文件
t2 写入文件
t1 写入文件
t2 写入文件
t1 写入文件
t2 写入文件
t1 写入文件
t2 写入文件
t2 写入文件
t2 关闭输出流
线程中断
上面的例子中,我们自定义了一个标志位来控制线程执行逻辑,实际上Thread也提供了一个特殊的标志位:中断标记。我们可以通过Thread.interrupt()来设置中断标记。需要理解的是,Thread.interrupt()仅仅是设置中断标记而不是把线程的运行逻辑直接中断掉。
示例:
package com.sahooz.jucdemo;
public class ThreadInterruptDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
System.out.println("输出: " + i);
}
});
t.start();
t.interrupt();
}
}
运行结果:
输出: 0
输出: 1
输出: 2
...
...
输出: 9997
输出: 9998
输出: 9999
响应中断
可以看到,如果做任何额外处理直接调用Thread.interrupt()在很多情况下对线程运行不会产生影响。
Thread中定义了两个重载方法来获取中断标记
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
其中ClearInterrupted参数指定是否返回后重置线程中断标记。虽然带参数的重载方法是private的,但是我们可通过Thread.interupted()静态方法来间接调用它。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
示例代码
package com.sahooz.jucdemo;
public class ThreadInterruptDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
Thread.currentThread().interrupt(); // 设置中断标记
System.out.println("通过Thread.isInterrupted()获取线程中断标记:" + Thread.currentThread().isInterrupted());
System.out.println("再次通过Thread.isInterrupted()获取线程中断标记:" + Thread.currentThread().isInterrupted());
System.out.println("通过Thread.interrupted()获取线程中断标记:" + Thread.interrupted());
System.out.println("再次通过Thread.interrupted()获取线程中断标记:" + Thread.interrupted());
});
t.start();
}
}
输出结果:
通过Thread.isInterrupted()获取线程中断标记:true
再次通过Thread.isInterrupted()获取线程中断标记:true
通过Thread.interrupted()获取线程中断标记:true
再次通过Thread.interrupted()获取线程中断标记:false
响应中断的示例
package com.sahooz.jucdemo;
public class ThreadInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 10000 && !Thread.currentThread().isInterrupted(); i++) {
System.out.println("输出: " + i);
}
});
t.start();
Thread.sleep(1);
t.interrupt();
}
}
输出结果:
输出: 0
输出: 1
输出: 2
...
...
输出: 34
输出: 35
输出: 36
中断异常
当线程处于睡眠或者等待等状态时,调用Thread.interrupt()方法会抛出中断异常InterruptedException,示例代码
package com.sahooz.jucdemo;
public class ThreadInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程响应中断");
}
System.out.println("线程继续运行了");
System.out.println("线程中断标记:" + Thread.currentThread().isInterrupted());
});
t.start();
Thread.sleep(100);
t.interrupt();
}
}
输出结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.sahooz.jucdemo.ThreadInterruptDemo.lambda$main$0(ThreadInterruptDemo.java:7)
at java.lang.Thread.run(Thread.java:748)
线程响应中断
线程继续运行了
线程中断标记:false
可以看到,抛出InterruptedException后,线程的中断标记被重置为false了。
3.4 用户线程和守护线程
Java中的线程可以分为用户线程和守护线程。在Thread对象创建后可以通过通过Thread.setDaemon(false)设置为用户线程,通过Thread.setDaemon(true)设置为守护线程,不设置的话默认是用户线程。需要注意的是,Thread.setDaemon()必须在Thread.start()方法之前调用,否则会抛出IllegalThreadStateException异常。
用户线程的主要特征是,主线程结束后用户线程还存活的话,JVM不会退出;守护线程的主要特征是,主线程结束后,如果没有用户线程存活,无论守护线程是否存活JVM都会退出。这是由两者扮演的作用决定的,用户线程的作用是执行必要的任务,所以用户线程结束前如果JVM退出可能会造成异常的后果;而守护线程的作用是在后台默默执行一些服务性操作,当所有用户线程都执行完成了,守护线程服务的对象也就没有了,那么JVM也该退出了。JVM中的GC线程就是典型的守护线程。
示例代码:
package com.sahooz.jucdemo;
public class ThreadCategoryDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true);
});
// 设置为守护线程
// t.setDaemon(true);
t.start();
System.out.println("主线程执行完啦");
}
}
t.setDaemon(true)这一行注释掉和没注释掉的运行结果如下:
注释后(用户线程):
没注释(守护线程):
4、总结
本文讲述了进程了线程的概念、串行、并行和并发的概念以及它们之间的区别和联系。另外还介绍了Java中线程的基本创建方法、线程的状态以及一些基本概念。
进程与线程:
- 进程:程序基于特定数据集合的运行过程,CPU分配资源的基本单位。
- 线程:线程属于进程,是CPU调度的基本单位,具体任务的载体。
串行、并行与并发:
- 串行:多个任务排队依次执行,同一时间只执行一个。
- 并行:同一时刻多个任务一起执行。
- 并发:一段时间内多个任务都获得执行。
Java中的线程:
- 可以通过继承Thread或者传递Runnable对象的方式创建
- 有NEW、RUNNABLE、WAITING、BLOCKED、TERMINATED五种状态
- 应该通过响应中断或者设置标志位的方式正确退出线程和释放资源/锁
用户线程和守护线程:
- 用户线程承载具体任务,用户线程存活,JVM不退出。
- 守护线程提供服务性功能,只有守护线程存活,JVM退出。