Java 线程
何谓线程
在单线程程序中,“在某一时间点执行的处理”只有一个。如果有人问起“程序的哪部分正在执行”,我们能够指着程序中的某一处回答说“这里,就是这儿”。这是因为,在单线程程序中,“正在执行程序的主体”只有一个。
单线程程序
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
System.out.print("Good!");
}
}
}
多线程程序
假设有人问起”程序的哪部分正在执行“,而我们需要指出程序位置,并回答”这里,就是这儿“。name在多线程的情况下,一根手指根本不够用,这时需要和线程个数一样多的手指。
当规模大到一定程度时,应用程序中便会自然而然地出现某种形式的多线程。以下便是几种常见示例。
- GUI 应用程序
几乎所有的 GUI 应用程序中都存在多线程处理。假设用户在使用文本工具编辑较大的文本文件时执行了文字查找操作。
- 执行查找
- 显示按钮,并在按钮被按下时停止查找
这两个操作是分别交给不同的线程来执行的,1的操作线程专门执行查找,而2的操作线程则专门执行 GUI 操作。
- 耗时的 I/O 处理
将执行 I/O 处理的线程和其他处理的线程分开。
- 多个客户端
当客户端连接到服务器时,我们会为该客户端准备一个线程。
Thread 类的 run 方法和 start 方法
public class MyThread extends Thread{
public void run(){
for (int i = 0; i < 10000; i++){
System.out.print("Nice!");
}
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
for (int i = 0; i < 10000; i++) {
System.out.print("Good!");
}
}
}
这两个线程是并发运行的,所以结果会混在一起。
并发:用于表示“将一个操作分割成多个部分并且允许无序处理”。比如将十个操作分成相对独立的两类,这样便能开始并发处理了。如果 CPU 只有一个,那么并发处理就是顺序执行的,而如果有多个 CPU,那么并发处理就可能会并行运行。
线程的启动
利用 Tread 类的子类
public class PrintThread extends Thread {
private String message;
public PrintTread(String message) {
this.message = message;
}
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.print(message);
}
}
}
public class Main {
public static void main(String[] args) {
new PrintThread("Good!").start();
new PrintThread("Nice!").start();
}
}
start 方法会启动新的线程,然后由启动的新线程调用 PrintThread 类的实例的 run 方法。
主线程在 Main 类的 main 方法中启动了两个线程。随后 main 方法便会终止,主线程也会跟着终止。但整个程序并不会随之终止,因为启动的两个线程在字符串输出之前是不会终止的。直到所有的线程都终止后,程序才会终止。
Java 程序的终止是指除守护线程以外的线程全部终止。守护线程是执行后台作业的线程。我们可以通过 setDaemon 方法把线程设置为守护线程。
创建 Thread 的子类,创建子类的实例,调用 start 方法——这就是利用 Tread 类的子类启动线程的方法。
利用 Runnable 接口
public class Printer implements Runnable {
private String message;
public Printer(String message) {
this.message = message;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.print(message);
}
}
}
public class Main {
public static void main(String[] args) {
new Thread(new Printer("Good!")).start();
new Thread(new Printer("Nice!")).start();
}
}
创建 Runnable 接口的实现类,将实现类的实例作业参数传给 Thread 的构造函数,调用 start 方法——这就是利用 Runnable 接口启动线程的方法。
利用 ThreadFactory 新启动线程
public class Main {
public static void main(String[] args) {
ThreadFactory factory = Executors.defaultThreadFactory();
factory.newThread(new Printer("Good!")).start();
for (int i = 0; i < 10000; i++) {
System.out.print("Nice!");
}
}
}
线程的暂停
线程 Thread 类中的 sleep 方法能够暂停线程运行。sleep 方法是 Thread 类的静态方法。
public class Main {
public static void main(String[] args) {
for (int i=0;i<10;i++){
System.out.print("Good!");
try {
// 将当前的线程(执行这条语句的线程)暂停1000毫秒。
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
InterruptedException 异常能够取消线程的处理。
如果要中途唤醒休眠的线程,则可以使用 interrupt 方法。
线程的互斥处理
线程 A 和线程 B 同时操作时,有时线程 B 的处理可能会插在线程 A 的“可用余额确认”和“从可用余额上减掉扣款金额”这两个处理之间。
这种线程 A 和线程 B 之间互相竞争而引起的与预期相反的情况称为数据竞争或竞态条件。
Java 使用关键字 synchronized 来执行线程的互斥处理
synchronized 方法
如果声明一个方法时,在前面加上关键字 synchronized,那么这个方法每次就只能由一个线程运行,也称为同步方法。
public class Bank {
private int money;
private String name;
public Bank(String name, int money) {
this.money = money;
this.name = name;
}
// 存款
public synchronized void deposit(int m) {
money += m;
}
// 取款
public synchronized boolean withdraw(int m) {
if (money >= m) {
money -= m;
return true;
} else {
return false;
}
}
public String getName() {
return name;
}
}
一个实例中的 synchronized 方法每次只能由一个线程运行,而非 synchronize 方法则可以同时由多个线程运行。
如果有一个线程正在运行 deposit 方法,其他线程就无法运行 deposit 方法和 withdraw 方法。
流程:某一线程获取锁——>运行方法——>该线程释放锁——>其他线程竞争锁——>没抢到锁的进程继续等待。
每个实例拥有一个独立的锁。
锁和监视:线程的互斥机制称为监视。另外,获取锁有时也称为“拥有监视”或“持有锁”。
当前线程是否已经获取某一对象的锁可以通过 Thread.holdsLock 方法来确认。当前线程已获取对象 obj 的锁时,可使用 assert 来表示。
assert Thread.holdsLock(obj);
synchronized 代码块
如果只是想让方法中的某一部分由一个线程运行,而非整个方法,则可以使用 synchronized 代码块。
synchronized (表达式) {
...
}
表达式为获取锁的实例。
synchronized void method() {
...
}
等效于
void method() {
synchronized (this) {
...
}
}
synchronized 的实例方法是使用 this 的锁来执行线程的互斥处理的。
如果是静态方法,则是使用该类的类对象的锁,如Something.class。
线程的协作
Java 提供了用于执行线程控制的 wait 方法、notify 方法和 notifyAll 方法。wait 是让线程等待的方法,notify 方法和 notifyAll 方法是唤醒等待中的线程的方法。
等待队列——线程休息室
所有实例都拥有一个等待队列,它是实例的 wait 方法执行后停止操作的线程的队列。
在执行 wait 方法后,线程便会暂停操作。除非发生下列某一情况,否则线程会一直在等待队列中休眠。
- 有其他线程的 notify 方法来唤醒线程。
- 有其他线程的 notifyAll 方法来唤醒线程。
- 有其他线程的 interrupt 方法来唤醒线程。
- wait 方法超时。
wait 方法
obj.wait();
当前线程暂停运行,并进入实例 obj 的等待队列中。这叫做“线程正在 obj 上 wait”。
实例方法中的 wait(); 和 this.wait(); 等价。
若要执行wait 方法,线程必须持有锁。但如果线程进入等待队列,便会释放其实例的锁。
notify 方法
将等待队列中的一个线程取出。
obj.notify(); 将等待队列中的一个线程选中和唤醒,退出等待队列。
notify 以后唤醒的线程并不会马上运行,唤醒线程的顺序并没有规定。
notifyAll 方法
将等待队列中的所有线程都唤醒。
唤醒的线程都在等待获取锁,处于阻塞状态。
未持有锁的线程调用这三个方法则会抛出 InterruptedException 异常。
使用 notifyAll 较为稳妥,使用 notify 速度更快但有时会发生问题。
wait、notify、notifyAll 是 Object 类的方法
这三个方法是针对实例的等待队列的方法,每个实例都有等待队列。
线程的状态迁移(生命周期)
各个状态的值都可以通过 Thread 类的 getState 方法获取。
练习题