线程是操作系统调度的最小单元, 也叫轻量级进程。它被包含在进程之中, 是进程中的实际运作单位。
同一进程可以创建多个线程, 每个线程都有自己独立的一块内存空间, 并且能够访问共享的内存变量。
1 线程的分类
在 Java 中, 线程可以分为 2 种
- 守护线程: 守护线程是为用户线程服务的线程, 在后台默默地完成一些系统性的服务, 如垃圾回收等
- 用户线程: 真正完成业务的工作线程
在一个应用程序中, 如果用户线程全部结束了, 意味着程序需要完成的业务操作已经结束, 系统可以退出了。
所以当系统只剩下守护进程的时候, Java 虚拟机会自动退出。
反之, 如果程序中的用户进程还在执行中, Java 虚拟机会等待器执行完成才结束。
2 线程的创建
2.1 继承 Thread 类, 重写 run 方法
// 定义自己的线程逻辑
public class MyThread extend Thread {
@Override
public void run() {
System.out.println("Thread is running " + Thread.currentThread().getName());
}
}
// 启动线程
new MyThread().start()
2.2 实现 Runnable 接口
public void createThread() {
// 定义自己的线程逻辑
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Thread is running " + Thread.currentThread().getName());
}
});
thread.start();
}
2.3 实现 Callable 接口
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable 接口和 Runnable 接口类似, call 方法里面就是需要线程执行的逻辑, 不同的是 Callable 有返回值。
同时还有一个隐藏的不同点, Callable 内部可以抛出异常, 同时这个异常是可以被捕获的, 所以可以通过异常再做一次容错处理。
public void createThread() {
// 实现 Callable 实现线程
Callable<String> callable = new Callable<>() {
@Override
public String call() throws Exception {
System.out.println("Thread is running " + Thread.currentThread().getName());
Thread.sleep(3000L);
return "finish";
}
};
// 这里借助线程池来实现线程
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 提交任务到线程池, Future 是对执行结果的封装
Future<String> future = executorService.submit(callable);
try {
// 尝试获取执行结果
// 注意: Future.get 方法是一个阻塞方法。如果对应的线程这时候还没有执行完成, 调用这个方法, 会阻塞当前线程
String result = future.get();
System.out.println("Thread's result:" + result);
} catch(Exception e) {
e.printStackTrace();
} finally {
// 关闭线程池
executorService.shutdown();
}
}
2.4 创建 FutureTask 实例
FutureTask 的 UML 图:
可以发现: FutureTask 还实现了 Runnable 接口, 所以可 FutureTask 也可以当做 Runnable 使用。
同时其还实现了一个 Future 的接口。
Futrue 接口的定义如下
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
Futrue 主要是用于子线程将自己的执行结果返回给主线程:
- 主线程将任务提供给子线程时, 会立即返回一个 Future 的返回值
- 一开始这个对象的返回值会是空的, 后续子线程执行完成后, 会把返回值翻到这个对象内
- 主线程就可以主动通过这个对象获取到执行结果
- 因为主线程主动获取执行结果时, 可能子线程还未执行完成, 所以获取返回结果的方法是阻塞的, 主动获取返回结果时, 如果还未有返回值时, 主线程将会被阻塞等待到有返回结果
通过 Future 的声明, 可以知道 Future 除了获取返回结果外, 还具备了取消任务, 获取任务是否完成等功能。
public void createThread() {
Callable<String> callable = new Callable<>() {
@Override
public String call() throws Exception {
System.out.println("Thread is running " + Thread.currentThread().getName());
Thread.sleep(3000L);
return "finish";
}
};
// 创建 FutureTask 实例
FutureTask<String> futureTask = new FutureTask<String>(callable);
// 创建线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
// FutrueTask 执行方式一
// 此处可以不要返回值, 通过入参的 futureTask 获取执行结果
Future<?> future = executorService.submit(futureTask);
// FutureTask 执行方式二, 当 Runnable 使用
// new Thread(futureTask).start();
try {
// 同样的会阻塞当前线程
// 通过 FutureTask 创建的, 返回结果放在自身上, 不在 executorService.submit 的返回值
// 通过 future 和 futureTask 都会阻塞当前线程
String result = futureTask.get();
System.out.println("Thread's result:" + result);
} catch (Exception e) {
e.printStackTrace();
} finish {
executorService.shutdown();
}
}
4 种方式的对比
- Thread: 编写简单, 但是不能再继承其他父类, 其内部实际还是通过 Runnable 实现的
- Runnable: 多个线程可以共用一个 Runnable 对象, 适合多线程处理同一个任务的情况。没法知道线程的执行情况
- Callable: 具备和 Runnable 一样的优势的同时, 可以知道线程的执行情况。但是需要借助 ExecutorService, 没法指定工作线程执行
- FutureTask: 具备了 Runnable 和 Callable 的所有优点, 缺点就是编写复杂
3 线程的属性
3.1 tid
线程 Id, 用于标记不同的线程 (某个编号的线程结束后, 这个编号可能别后续创建的线程使用)。
3.2 name
线程名称, 面向人的一个属性, 用于区分不同的属性, 默认为 Thread-数字
。 在实际开发中, 尽可能的自定义自己的线程名, 这样在后续的问题定位排查有帮助
(同时记得不要重复, Java 允许存在线程名相同的情况, 但是这会影响到后面问题的定义)。
3.3 priority
用于系统的线程调度用的, 表示希望某个线程能够优先得到运行。Java 定义了 1 - 10 个级别, 值越大, 优先级越高, 默认为 5。在实际使用中, 尽可能的不要自定义优先级, 可能会出现意想不到的问题, 比如线程饥饿。
3.4 daemon
是否为守护线程。一个线程是用户线程还是守护线程, 通过这个属性进行区分。
true: 表示这个线程为守护线程, 否则为用户线程, 这个属性的设置需要在线程启动之前进行设置才有效, 默认为 false, 用户线程。
3.5 threadStatus
线程状态, 标识当前线程的处于什么样的一样状态, 具体的取值后面分析。
其实 Thread 身上还有其他几个属性, 基本不是什么重要的属性, 就不展开了。
4 线程状态
4.1 状态取值
线程状态 | 说明 |
---|---|
NEW | 初始状态。线程已创建, 但是未启动, 既未调用 start 方法 |
RUNNABLE | 运行状态。她包括 2 个状态: 准备状态 和 运行状态 |
BLOCKED | 阻塞状态。线程阻塞于锁 或者 发起了阻塞式 I/O 操作 (Socket 读写) |
WAITING | 等待状态。当前线程需要等待其他线程执行一下特定的操作(通知, 中断) |
TIME_WAITING | 超时等待状态。和 WAITING 类似, 区别就是这个状态的等待是有时间限制的 |
TERMINATED | 终止状态。线程的需要执行的任务已完成。 |
4.2 线程状态的转换
如图:
4.2.1 New
通过 new Thread() 创建出 Thread 实例, 实例的默认的状态就是 New
4.2.2 Runnable
Java 中的 Runnalbe 状态实际可以再细分为 Ready 和 Running。线程处于 Runnable 不一定就是在执行中的, 也有可能是在 Ready 中,
具体什么时候从 Ready 变为 Running, 完全取决于系统的调度。
4.2.3 Waiting
等待中状态, 处于等待状态的线程, 正在等待其他线程去执行一个特定的操作。
从 Runnable 转到 Waiting 的方式有
- Object.join()
- Ojbect.wait()
- Lock.lock(), 尝试获取锁, 获取锁失败时
- LockSupport.park()
从 Waiting 转到 Runnable 的方式有
- Object.notify()
- Ojbect.notifyAll()
- LockSupport.uppark(Thread)
4.2.4 Time_Waiting
带超时时间的等待状态。
从 Runnable 转到 Timed_waiting 的方式有
- Thead.sleep(long)
- Object.wait(long)
- Thread.join(long)
- Lock.tryLock(long, TimeUnit)
- LockSupport.parkNanos()
- LockSupport.parkUntil()
从 Timed_Waiting 转到 Runnable 的方式有
- Object.notify()
- Ojbect.notifyAll()
- LockSupport.uppark(Thread)
4.2.5 Blocked
阻塞状态, 此时线程无任何的处理能力
从 Runnable 转到 Blocked 的方式有
- 获取 synchronized 锁失败
从 Blocked 转到 Runnable 的方式有
- 获取 synchronized 锁成功
备注(待考证):
WAITING 和 BLOCKING 之间也存在着转换, 当多个线程阻塞于同一个锁时, 他们都处于 WAITING 状态, 当有一个线程释放锁了, 上面的线程会同时争取锁, 争取到锁的线程会进入到 RUNNABLE, 没有争取到的会进入到 BLOCKED。
4.2.6 Terminated
终止状态。
线程中的业务业务代码执行完成, 结束逻辑。
5 线程的一些基本操作
5.1 sleep
sleep(long mills) 是 Thread 的一个静态方法。
可以让当前线程进入休眠, 休眠的时间由指定的参数决定。
调用这个方法会导致线程状态变为 Timed_waiting。
5.2 wait
wait() / wait(long mills) 是 Object 的一个方法, 可以让执行这个方法的线程暂停 (进入到 Waiting / Timed_waiting)。
wait() / wait(long mills) 在使用之前需要先获取到锁, 才能进入暂停。即只能在同步代码块中使用, 同时内部要调用代码块锁住的对象的 wait()
Object lock = new Object();
Object lock2 = new Object();
// 没有在代码块中, 抛异常
//lock.wait();
// 锁住了 lock 对象
synchronized (lock) {
try {
// 没有锁住 lock2, 调用 lock2.wait 方法会抛异常
//lock2.wait();
// 正常沉睡
lock.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
wait 和 sleep 的区别
- wait 是 Object 的一个方法, sleep 是 Thread 的一个静态方法
- wait 需要在获取到对应的锁的时候才能使用(也就是在同步代码块, 或者同步方法内), sleep 则不需要
- wait 方法在执行时, 会释放自身的拥有的锁, 而 sleep 如果拥有锁, 则不会释放
- wait(long mills) / sleep 方法会在指定的休眠时间达到后, 重新运行。但是 wait() 方法需要其他线程调用对应的锁对象的 notify() 或者 notifyAll() (这 2 个方法也都是需要先获取到对应的锁), 进行通知后, 才有可能继续执行 (有可能同时多个线程在等待, 但是锁只有一个, 只能在等待的线程中选择一个进行唤醒)
5.3 join
join() / join(long mills) 是 Thread 的一个方法。主要用于让当前线程等待指定的线程执行完成。
Thread waitThread = new Thread(() -> {
try {
Thread.sleep(30000L);
System.out.println("son thread finish");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
waitThread.start();
try {
// 当前线程(主线程)进入暂停状态, 等待 t 线程执行完。
waitThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread finish");
5.4 yeild
yeild() 是 Thread 的一个静态方法。
作用: 使当前线程主动放弃其对处理器的占用, 这可能导致当前线程被暂停。这个方法是不可靠的, 有可能调用之后, 前线程还在继续执行。
5.5 interrupt
interrupt() 是 Thread 的一个方法, 调用这个方法, 可以向指定的线程发送一个信号, 让其终止, 但是最终是否能够终止, 由线程内部决定。
原理:
- 线程内部维护了一个 isInterrupted 的变量 (这个变量不在 Java 代码里面维护, 而是在 JVM 的代码里面), 取值范围为 0 (false), 1 (true)
- 调用线程的 interrupt() 方法, 会把这个标志符设为 1
- 当线程的状态从 Runnable 变为其他的状态时, 检测到这个标识为 1, 就会抛出 InterruptedException 异常, 同时把标志重新恢复为 0
- 线程的 wait/sleep/join 等方法, 都可以改变线程的状态
复位 (中断标识从 true 恢复回 false):
- 可以直接调用 Thread 的 静态方法 interrupted() 可以将中断标志恢复为 false.
- 线程抛出 InterruptedException 异常。