基于进程和线程的多任务,其最小的调度单位分别是进程和线程。
在基于线程的环境中,单个进程可以同时处理不同的任务,每个线程共享地址空间。
基于线程的多任务和基于进程的相比,开销小。相互间的通信和上下文切换开销不同。
Java 的线程模型
Java 的运行时系统使用多线程,当某一个线程阻塞时,不影响其他正在执行的线程。如果是单核处理器,线程轮流使用 CPU 时间片;如果是多核处理器,多个线程可以同时处理。
线程有 running、ready、run、suspended、resumed、blocked、terminated 这些状态。
Java 中线程优先级由一个整数指定,某个线程的优先级是相对于其他线程而言的。优先级在上下文切换时使用。上下文切换的情形:
- 一个线程自愿放弃 CPU 的使用
- 高优先级的线程抢占低优先级线程的 CPU 时间。
如果至少有两个相同优先级线程竞争 CPU 时间片,此时根据 OS 的不同有不同的行为。对于部分 OS,相同优先级的线程以 CPU 时间片轮转的方式调度线程;对于另一部分 OS,当某个线程不主动放弃 CPU 的使用,其他同优先级的线程无法运行。
monitor 是一种控制机制,可以将其看作一个小盒子,任一时刻仅允许一个线程位于其中。当一个线程在里面时,所有其他线程必须等待,直到这个线程退出盒子。用来保证共享资源在任何时刻都由一个线程使用。
Java 中,每一个对象隐含有 monitor 机制。某个线程调用一个对象的同步方法时,该对象的其他所有同步方法都不能被其他线程调用。
Java 提供了一种清晰、低成本的方式用于线程间通信。消息系统允许一个线程进入一个对象的同步方法,等待直到另一个线程通知它退出。
Java 的多线程有 Thread 类和 Runnable 接口,Thread 封装了执行的线程。为创建一个线程,要么继承 Thread 类要么实现 Runnable 接口。
主线程
Java 程序启动时,有一个线程立即执行,这个线程称为主线程。所有其他线程都由主线程产生;主线程是最后一个结束执行的线程,因为它需要执行各种终止操作。
public static Thread currentThread()
方法返回当前线程的引用,可以获得主线程的引用。
打印输出线程时,依次打印线程名、优先级和线程组名。线程组是控制作为一个整体的多个线程状态的数据结构。
创建线程
实现 Runnable 接口来新建线程的方式为:首先定义一个实现 Runnable 接口的类,该类必须实现 Runnable 接口中的 public void run()
方法,在 run()
方法里面,定义的是新线程的执行逻辑,可以看作是另一个程序的执行逻辑。在这个方法里面,允许定义类、变量和调用方法等操作。在这个类里面,需要实例化一个 Thread 类对象,构造器之一为 Thread(Runnable threadObj, String threadName)
。新线程创建后,并不会执行,需要调用 Thread 类中的 start()
方法才会开始执行。start()
方法本质上调用了 run()
方法。
// 定义实现了 Runnable 接口的类 A,实现该接口的 run 方法
// run 方法中的内容即为子线程的执行内容
// 在 A 中实例化 Thread 类
// 以上创建完成子线程
// 使用子线程
// 实例化类 A
// 调用类 A 中 Thread 类实例的 start 方法
class A implements Runnable {
Thread t;
A() {
t = new Thread(this, "ThreadName");
}
public void run() {
try {
for (int i = 5; i >= 1; i--) {
System.out.println("child: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("child interrupted");
}
System.out.println("exit child thread");
}
}
public class T {
public static void main(String[] args) {
A a = new A();
a.t.start();
try {
for (int i = 5; i >= 1; i--) {
System.out.println("main: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("main interrupted");
}
System.out.println("exit main thread");
}
}
创建线程的另一种方法是定义 Thread 类的子类,需要实现 Thread 中的 run()
方法。调用该类的 start()
方法就可以启动新线程。
class CustomThread extends Thread {
CustomThread() {
super("ThreadName");
}
public void run() {
try {
for (int i = 5; i >= 1; i--) {
System.out.println("child: " + i);
Thread.sleep(500);
}
} catch (InterruptedException e) {
System.out.println("child interrupted");
}
System.out.println("exit child thread");
}
}
class B {
public static void main(String[] args) {
CustomThread c = new CustomThread();
c.start();
try {
for (int i = 5; i >= 1; i--) {
System.out.println("main: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("main interrupted");
}
System.out.println("exit main thread");
}
}
具体使用上述两种方法中的哪一种取决于实际情况。实现 Runnable 接口则可以继承其他类;如果不重写 Thread 类中的其他方法,继承 Thread 的方式更简单。
创建多线程
和上一部分一样,只不过调用时创建了一个类的多个实例,分别调用每个实例的 start()
方法。
使用 isAlive() 和 join()
调用 Thread 类的 final boolean isAlive()
方法的线程如果正在执行则返回 true,否则返回 false。
如果在线程 A 中,在线程 B 上调用了 final void join()
方法,则 A 线程会等待,直到线程 B 执行完成后,A 线程恢复执行。
线程优先级
当线程调度器决定运行哪个线程时,会选择优先级高的线程运行。
final void setPriority(int level)
方法设置线程的优先级,可选值范围从 MIN_PRIORITY(1) 到 MAX_PRIORITY(10),默认的优先级为 NORM_PRIORITY(5)。
final int getPriority()
方法获得线程的优先级。
同步
当一个资源被多个线程同时访问时,需保证在任何时候,只有一个线程在使用该资源,这种机制称为同步。monitor 是作为互斥锁的对象,当某个线程在某一时刻获得了锁,,则称该线程进入了 monitor。此时,其他尝试进入 monitor 的线程在已经进入 monitor 的线程退出之前状态变为 suspended,也称等待 monitor。进入 monitor 的线程可以再次进入 monitor。
在 Java 中,使用同步比较简单。每个对象都有与之关联的隐式 monitor。当一个对象的 synchronized
方法被调用时,当前线程进入 monitor,任何调用该对象 synchronized 方法的其他线程必须等待,直到当前线程从 synchronized 方法返回。
class Nchr {
synchronized void call(String msg) {
System.out.print("a-" + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("-end");
}
}
class Ca implements Runnable {
Nchr obj;
String msg;
Thread t;
public Ca(Nchr n, String m) {
obj = n;
msg = m;
t = new Thread(this);
}
public void run() {
obj.call(msg);
}
}
public class Out {
public static void main(String[] args) {
Nchr n = new Nchr();
Ca one = new Ca(n, "one");
Ca two = new Ca(n, "two");
Ca three = new Ca(n, "three");
one.t.start();
two.t.start();
three.t.start();
/* 输出
a-one-end
a-two-end
a-three-end
*/
try {
one.t.join();
two.t.join();
three.t.join();
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
}
}
如果想要调用的方法是非 synchronzied 方法,但又想使用同步,可以使用同步块。如下:
// 同步块括号中的引用表示需要同步的对象
// 上一个代码段中 Ca 类的 run 方法改写的等价形式
public void run() {
synchronized(t) {
t.call(msg)
}
}
线程间通信
在 Java 中,Object 类中的 wait()
、notify()
和 notifyAll()
方法用于线程间通信。这三个方法必须在 synchronized 上下文中使用。
final void wait()
:让调用该方法的线程放弃 monitor 进入休眠状态,等待直到另一个线程进入这个 monitor 并调用notify()
或notifyAll()
方法。final void notify()
:唤醒在同一个对象上调用了wait()
方法的一个线程。final void notifyAll()
:唤醒所有在同一个对象上调用了wait()
方法的线程,给予其中某个线程访问权限。
使用 wait()
方法的线程有可能不是通过调用 notify()
或 notifyAll()
方法唤醒的。wait()
方法应该在检查条件的循环中使用。
class Q {
int value;
boolean isSet = false;
// synchronized 方法必须执行完,其他方法才能执行
synchronized void put(int value) throws InterruptedException {
// 值已经设置了,则挂起
while (isSet) {
wait();
}
this.value = value;
System.out.println("put: " + value);
isSet = true;
notify();
}
synchronized void get() throws InterruptedException {
while (!isSet) {
wait();
}
int value = this.value;
System.out.println("get: " + value);
isSet = false;
notify();
}
}
class Producer1 implements Runnable {
Thread thread;
private Q q;
Producer1(Q q) {
thread = new Thread(this);
this.q = q;
}
@Override
public void run() {
int i = 0;
while (i <= 20) {
try {
q.put(i++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer1 implements Runnable {
Thread thread;
private Q q;
Consumer1(Q q) {
thread = new Thread(this);
this.q = q;
}
@Override
public void run() {
while (true) {
try {
q.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class PCFixed {
public static void main(String[] args) {
Q q = new Q();
Producer1 p = new Producer1(q);
Consumer1 c = new Consumer1(q);
p.thread.start();
c.thread.start();
}
}
一个线程 a 进入对象 X 的 monitor 中,同时另一个线程 b 进入对象 Y 的 monitor 中。然后, a 调用 Y 的 synchronized 方法, b 调用 X 的 synchronized 方法。此时,两个线程都需无限等待,发生死锁。
挂起、恢复和停止
线程执行状态的改变通过设置一个状态变量决定。在 run()
方法中根据状态变量使用 wait()
方法挂起,在其他方法中设置状态变量的值,有时需要调用 notify()
方法唤醒。
获得线程状态
Thread.State getState()
方法返回调用该方法时线程所处的状态。由于该方法返回结果之后,线程的状态可能已发生变化,所以不能用于线程同步。Thread.State 的取值有:
值 | 说明 |
---|---|
BLOCKED | 因等待锁挂起 |
NEW | 没有开始执行 |
RUNNABLE | 正在执行或者正等待CPU时间将执行 |
TERMINATED | 完成执行 |
TIMED_WAITING | 挂起一段时间,如 sleep |
WAITING | 因等待事件发生挂起 |
使用工厂方法创建和启动线程
如果需要使用一条语句创建并启动线程,而不是分两步进行,可以使用工厂方法。工厂方法是一个静态方法,在这个方法里面,创建类的实例并启动线程,然后返回对实例的引用。
class NewThread {
public static NewThread createAndStart() {
var t = new NewThread();
t.start();
return t;
}
}
var n = NewThread.createAndStart();
参考
[1] Herbert Schildt, Java The Complete Reference 11th, 2019.