线程学习

零、概念

在计算机中,我们把一个任务称为一个进程。浏览器是一个进程,word是另一个进程。

一个Java程序实际上是一个JVM进程,JVM用一个主线程来执行main()方法,在main()内部,又可以启动多个线程。另外,还有垃圾回收其他工作线程。

当所有非守护线程执行完毕后,JVM进程退出。

线程是操作系统调度的最小任务单位。

进程和线程的关系:包含关系。一个进程可以包含一个或多个线程,但至少一个线程。

进程间通信比线程间通信慢,因为线程间通信是读取同一变量。

一、创建新线程

Java用Thread对象表示一个线程,线程执行代码写在run()方法中,一个线程对象只能调用一次start()方法。

创建新线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Main {
public static void main(String[] args) {
// 方式一:从Tread派生一个自定义类,覆写run()
Thread t1 = new MyThread();
t1.start(); // 启动新线程,会在内部自动调用实例的run方法

// 方式二:传入一个Runnable实例
Thread t2 = new Thread(new MyRunnable());
t2.start();

// 方式三:Lambda
Thread t3 = new Thread(() -> {
System.out.println("start new thread!");
});
t3.start(); // 启动新线程

}
}

class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}

class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}

必须调用Thread实例的start()方法才能启动新线程,start()方法内部调用了一个 private native void start0() 方法。(调用Thread实例的run()方法相当于调用普通的Java方法,不会启动新线程)。

二、线程的状态

1、六种状态

  • New:(初始)新创建的线程,还没有调用start()方法;
  • Runnable:(运行)运行中的线程,正在执行run()方法的 Java 代码;
  • Blocked:(阻塞)运行中的线程,因为某些操作被阻塞而被挂起;
  • Waiting:(等待)运行中的线程,进入等待状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断);
  • TimeWaiting:(超时等待)运行中的线程,sleep()进入计时等待;
  • Terminated:(终止),线程已终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行完成;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 线程强制终止:对某个线程的 Thread 实例调用 stop() 方法强制终止(强烈不推荐)

通过对另一个线程对象调用join()方法可以等待其执行结束。对已经结束的线程调用join()会立刻返回。

安全地终止线程:thread.interrupt()中断操作或runner.cancel()方法使线程在终止时有机会去清理资源。

三、中断线程

方式一:对目标线程调用 interrupt()方法

对目标线程调用 interrupt() 方法可以请求中断一个线程,目标线程可以通过检测 isInterrupted() 标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到 InterruptedException

目标线程通过检测到 isInterrupted()true 或者捕获了 InterruptedException 都应该立刻结束自身线程;

方式二 :设置标志位:

目标线程中设置标志位变量public volatile boolean running = true;,通过标志位判断是否应该继续进行。

外部线程设置标志位值AThread.running = false,就可以让目标线程中断。

线程间共享变量需要使用volatile标记(涉及到java内存模型,volatile解决了共享变量在线程间的可见性问题),确保每个线程读取到更新后的变量值。

中断与终止不同,中断可以选择不退出。

四、守护线程

守护线程是指为其他线程服务的线程。

所有非守护线程执行完毕后,虚拟机退出(不会关心守护线程是否结束)。

在执行start()启动线程前,线程调用t.setDaemon(true);可以将线程标记为守护线程。

五、wait和notify

waitnotify用于多线程协调运行。

  • 必须在syncronized块中才能调用wait()方法,且只能在锁对象上调用wait()方法 。

wait()方法是定义在Object类的一个native方法。调用wait()方法时,会释放线程获得的锁,线程处于等待状态。wait()方法返回后,线程又会试图获得锁。

  • 等待的线程如何被唤醒?wait()方法什么时候返回?在相同的锁对象上调用notity()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TaskQueue {
Queue<String> queue = new LinkedList<>();

public synchronized String getTask() {
while (queue.isEmpty()) { // 注意此处必须用while,若从wait()返回后还需要判空
// 释放锁
this.wait();
// 重新获取this锁
}
return queue.remove();
}

public synchronized void addTask(String s) {
this.queue.add(s);
this.notify(); // 唤醒正在this锁等待的一个线程(具体哪个依赖操作系统)
// this.notifyAll(); //唤醒所有在this锁等待的线程
}
}

六、sleep、yield、wait、join方法区别

1、sleep

static 修饰,Thread 类的静态方法,针对当前线程

让出CPU,但是不会释放锁。

有超时设置;

可中断方法,可能抛出 InterruptedException。

2、yield

static 修饰,Thread 类的静态方法,针对当前线程

yield () 方法线程礼让,始终都是Runnable 状态。只能让同优先级的线程有执行的机会,是一种建议,不能保证被采纳。 yield () 只是使当前线程重新回到可执行状态(就绪),所以执行 yield () 的线程有可能在进入到可执行状态后马上又被执行。

3、wait

wait () 和 notify ()、notifyAll () 这三个方法都是 java.lang.Object 的方法

Object 类的方法 (notify ()、notifyAll () 也是 Object 对象),必须放在循环体和同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒 (notify 随机唤醒,notifyAll 全部唤醒,线程结束自动唤醒) 即放入锁池中竞争同步锁。

4、join

join () 是由线程对象来调用。

等待调用 join 方法的线程结束,再继续执行。

当前运行线程调用另一个线程的 join 方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。