本文目录:
进程和线程
什么是进程和线程?
在计算机中,一个任务就是一个进程
在进程内部还需要多个子任务,每个子任务被称为线程。
一个进程可以包含一个或多个线程(至少一个)。
进程和线程的关系
- 一个进程可以包含一个或多个线程(至少一个线程)
- 线程是操作系统调度的最小任务单位
- 如何调度线程完全由操作系统决定
实现多任务的方法
- 多进程模式(每个进程只有一个线程)
- 多线程模式(一个进程有多个线程)
- 多进程+多线程(复杂度最高)
多进程 vs 多线程
- 创建进程比创建线程开销大
- 进程间通信比线程间通信慢
- 多进程稳定性比多线程高
Java内置多线程支持
- 一个Java程序实际上是一个JVM进程
- JVM用一个主线程来执行main()方法
- 在main()方法中又可以启动多个线程
多线程编程特点
- 多线程需要读写共享数据
- 多线程经常需要同步
- 多线程编程的复杂度高,调试更困难
Java多线程编程特点
- 多线程模型是Java程序最基本的并发模型
- 网络、数据库、Web等都依赖多线程模型
- 必须掌握Java多线程编程才能继续深入学习
线程创建方式
Thread类
Java为我们提供了一个Thread类来操作线程,基本使用如下
public class ThreadTest {
public static void main(String[] args) {
// 创建线程对象
Thread thread = new Thread();
// 启动线程
thread.start();
}
}
线程启动后虚拟机会自动调用run()方法(自己调用没有任何意义),直接使用Thread类对象启动线程run()方法不会执行任何操作,因此需要我们自己定义Thread的子类并重写run()方法来执行我们自己的操作
class SubThread extends Thread {
@Override
public void run() {
System.out.println("自定义线程运行中。。。")
}
}
public class ThreadTest {
public static void main(String[] args) {
SubThread subThread = new SubThread();
subThread.start();
}
}
Runnable接口
另一种创建线程的方法是实现Runnable接口,并重写run()方法
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread running...");
}
}
但是创建线程对象和启动线程的方法稍有不同
public class Main {
public static void main(String[] args) {
// 创建Runnable的实现类对象
Runnable runnable = new MyThread();
// 通过Runnable实现类对象创建Thread对象
Thread thread = new Thread(runnable);
// 启动线程
thread.start();
}
}
总结
- Java用Thread对象表示一个线程,通过调用start()启动一个线程
- 一个线程对象只能调用一次start()
- 线程的执行代码是run()方法
- 线程调度由操作系统决定,程序本身无法决定
- Thread().sleep()可以把当前线程暂停一段时间
线程的状态
线程状态
- New(新创建)
- Runnable(运行中)
- Blocked(被阻塞)
- Waiting(等待)
- Timed Waiting(计时等待)
- Terminated(已终止)
线程终止原因
- run()方法执行到return语句返回(线程正常终止)
- 因为未捕获的异常导致线程终止(线程意外终止)
- 对某个线程的Thread实例调用stop()方法强制终止(不推荐)
线程等待
当A线程运行时调用了B线程的join()方法,则会等待B线程执行结束后继续执行A线程。
public class Main {
public static void main(String[] args) {
SubThread subThread = new SubThread();
subThread.start();
try {
// 主线程会等待subthread执行完成
subThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束。。。");
}
}
也可以指定等待的时间,超过等待时间线程仍然没有结束则不再等待,如果不指定或指定时间为0则表示一直等待。
public class Main {
public static void main(String[] args) {
SubThread subThread = new SubThread();
subThread.start();
try {
// 等同于subThread.join(0);表示一直等待
subThread.join();
// 主线程最多等待subthread线程1000ms
subThread.join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束。。。");
}
}
如果调用join()方法的线程已经运行结束了,则会立即返回不进行等待。
中断线程
什么是中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。
class MyThread extends Thread {
@Override
public void run() {
// 长时间循环执行任务
while (true) {
// do something...
}
}
}
中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法。需要被中断的线程就是中断线程。
线程中断方法
检测isInterrupted()
中断线程需要通过检测isInterrrupted()标志来决定要不要中断,其他线程通过调用interrupt()方法中断该线程。
class MyThread extends Thread {
@Override
public void run() {
// 检测isInterrupted()标志决定是否继续执行
while (!isInterrupted()) {
System.out.println("running...");
}
}
}
puclic class Main {
public static void main(String[] args) throws Exception {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
// 中断线程
t.interrupt();
}
}
如果线程处于等待状态,该线程会捕获InterruptedException,补货到该异常说明其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。
class MyThread extends Thread {
public void run() {
while(!isInterrupted()) {
System.out.println("running...");
try {
// 线程处于等待状态
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 捕获到中断异常,立即结束线程运行
return;
}
}
}
}
自定义running标志检测
通过检测自定义running标志位来代替检测isInserrupted(),通过修改running的值来决定线程是否中断。
class MyThread extends Thread {
// 自定义一个标识位
public volatile boolean ruuning = true;
public void run() {
// 检测自定义标识位
while (running) {
// do something...
}
}
}
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
// 修改自定义标识位
t.running = false;
}
}
标识位使用volatile来标记,是因为线程间共享变量需要使用volatile标记,以确保线程能读取到更新后的变量值。
volatile简介
Java内存模型
在Java虚拟机中,变量的值保存在主内存中,当线程访问一个变量的时候,会先获取一个副本,并且保存到自己的工作内存中,如果线程修改了变量的值,虚拟机会在某个时刻把值回写到主内存,但这个时间是不固定的。
volatile作用
volatile关键字的目的是告诉虚拟机:
-
每次访问变量是,总是获取主内存的最新值
-
每次修改变量后,立刻回写到主内存
因此volatile关键字解决的是可见性问题:
- 当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值
总结
- 调用interrupt()方法可以中断一个线程
- 通过检测isinterrupted()标识获取当前线程是否已中断
- 如果线程处于等待状态,该线程会捕获InterruptedException
- isInterrupted()为true或者捕获了InterruptedException都应该立刻结束
- 通过标识位判断需要正确使用volatile关键字
- volatile关键字解决了共享变量在线程间的可见性问题
守护线程
Java中所有线程结束后,JVM退出,如果有无限循环(定时任务)的线程,则虚拟机无法退出,这是就需要守护线程。
什么是守护线程
- 守护线程是为其他线程服务的线程
- 所有非守护线程都执行完毕后,虚拟机退出
特点
- 守护线程不能持有任何资源(如打开文件等),因为虚拟机退出时守护线程无法自动释放资源。
创建守护线程
设置线程属性setDaemon(true)即可把该线程变为守护线程。
class TimerThread extends Thread {
@Override
public void run() {
// 无限循环
while (true) {
System.out.println(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("Main start");
TimerThread t = new TimerThread();
t.start();
Thread.sleep(5000);
System.out.println("Main end");
}
}
因为子线程在无限循环地打印,所以当Main end输出后程序并不会结束,子线程还在继续打印时间,如果把子线程变为守护线程则程序就正常退出了
//TimerThread类同上
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("Main start");
TimerThread t = new TimerThread();
// 设为守护线程,必须在线程启动之前,否则会出现异常
t.setDaemon(true);
t.start();
Thread.sleep(5000);
System.out.println("Main end");
}
}
Java中的垃圾回收线程就是一个守护线程,它始终在低级别的状态下运行,实时监控和管理系统中的可回收资源。当所有非守护线程都退出时,守护线程也就没有存在的必要了,乖乖的自己离开。