初步了解多线程
系列文章目录
目录
系列文章目录
前言
一、进程
二、线程
1. 线程解决资源开销的方式
2. 线程和进程的联系和区别
三、多线程编程
1. 直观了解多线程
2. 线程的创建方式
1. 继承 Thread 重写 run() 方法
2. 实现 Runable 接口,重写 run() 方法
3. 继承 Thread 重写 run(),使用匿名内部类
4. 实现 Runnable 接口,重写 run(),使用匿名内部类
5. 使用 lambda 表达式
3. Thread 的构造方法
4. Thread 的常见属性
5. 线程的启动
6. 线程的终止
7. 等待线程
8. 获取线程引用
9. 线程的状态
前言
本文系统介绍了Java多线程编程的核心概念。首先对比了进程和线程的区别,指出线程能更高效地解决并发问题。重点讲解了Java中创建线程的5种方式:继承Thread类、实现Runnable接口、匿名内部类及lambda表达式。详细分析了Thread类的关键属性和方法,包括线程状态、启动/终止机制、join等待等。通过多个代码示例演示了线程的随机调度特性、中断处理及状态转换。最后介绍了使用jconsole工具监控线程运行情况的方法,为理解Java并发编程提供了全面的基础指导。
一、进程
多任务的操作系统,指系统能够同时执行多个程序;
单任务的系统,完全不涉及进程,也不需要管理,更不需要调度;
进程本质上是为了解决并发编程这类问题,同时进程也能够很好得解决并发编程的问题;
但是在特定情况下,需要频繁创建和销毁进程的时候,此时使用多进程编程,系统的开销就会很大;
系统的开销主要体现在:创建进程的时候需要申请资源,包括CPU,内存,硬盘,网络带宽等,进程执行结束的时候,会销毁进程并释放资源;
举例说明:在服务器中,服务器如果收到很多的请求,就需要创建很多的进程,创建进程时需要申请资源,一旦这个请求处理完毕,就会销毁这个进程并释放资源。一旦某个时间段中,请求达到一定的数量,系统就会不停得创建和销毁进程,导致系统的压力会非常大。
二、线程
1. 线程解决资源开销的方式
线程就是为了解决上述问题的;
一个进程包含多个线程,线程可以理解为轻量级进程,在进程的基础上进行了改进;
线程保持了独立调度执行,同时省去了申请资源和释放资源带来的额外开销;
进程是资源分配的基本单位,而线程是调度执行的基本单位;
线程实现节省资源申请和释放资源开销的方式:
线程和进程一样,也是通过 PCB(Process Control Block) 结构体进行描述,通过链式的数据结构进行组织;
能够共享资源的多个线程称为线程组,每个进程都会有独立的内存资源和文件描述符表,而一个线程组的多个线程会共享同一份内存和硬盘资源;
这样就意味着首次创建进程的时候从系统分配的资源,后续创建其它线程的时候,就不需要再申请资源,直接共用前面申请的资源即可;
2. 线程和进程的联系和区别
一个进程可以包含多个线程;
每个线程也是一个独立的执行流,可以执行一些逻辑并单独参与到 CPU 的调度中,每个线程都有自己的状态,优先级,上下文和记账信息;
进程是资源分配的基本单位,而线程是调度执行的基本单位;每个进程都有自己的资源,而一个线程组的线程会共享同一份资源(内存空间和文件描述符表);
进程和进程之间不会相互影响,如果同一个进程中的某个线程抛出异常,可能会影响到其它线程,会把进程中的所有线程都终止;
同一个进程中的线程,可能会互相干扰,造成线程安全问题;
线程不是越多越好,如果线程过多,操作系统调度线程时的开销就会非常明显;
三、多线程编程
Java 不推荐多进程编程,Java 进程中要启动 Java 虚拟机,开销会比较大;
系统提供了多线程编程的 API,Java 把这些 API 进行了封装,可以在代码中直接调用;
Java 中多线程编程的类是 Thread 类,由 java.lang 包提供,在 java 程序中默认是导入 java.lang 的,因此不需要手动导入这个包;
如下程序为例:
class MyThread extends Thread{@Overridepublic void run() {// run() 方法是线程的入口方法System.out.println("hello world");}
}public class ThreadDemo1 {public static void main(String[] args) {Thread t = new MyThread();// 调用 Thread 类的 start() 方法,才会真正调用系统 API,在内核中创建线程t.start();}
}
1. 直观了解多线程
操作系统分为内核和应用程序,存放内核的空间称为内核空间,也叫内核态;存放应用程序的空间称为用户空间,也叫用户态;
应用程序针对系统的软硬件资源进行操作都是通过操作系统的 API 进行的,进一步在内核中调用软硬件资源;
划分内核态和用户态的原因:
保证系统稳定运行,防止应用程序出现 bug,进行违法操作,对系统产生危害;
每个线程都是一个独立的执行流,都能独立得去 CPU 上调度执行,下面程序为例:
class MyThread2 extends Thread{@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}public class ThreadDemo2 {public static void main(String[] args) {Thread t = new MyThread2();t.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
上述两个线程都在执行,都能打印信息,两个线程是两个独立得执行流;
当有多个线程的时候,线程执行的先后顺序是不一定的,因为操作系统内核的调度逻辑是随机调度:
- 线程什么时候调度到 CPU 上执行,时机并不确定;
- 线程什么时候从 CPU 上调度下来,时机也不确定;
随机调度的逻辑下,多个线程之间是抢占式执行的;
使用 JDK 的 Jconsole.exe 可以看到线程的执行情况和堆栈信息:
上面显示的堆栈信息是线程运行情况的快照;
2. 线程的创建方式
1. 继承 Thread 重写 run() 方法
class MyThread2 extends Thread{@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
2. 实现 Runable 接口,重写 run() 方法
class MyThread3 implements Runnable{@Overridepublic void run() {while(true){System.out.println("hello runnable");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}public class ThreadDemo3 {public static void main(String[] args) {Runnable runnable = new MyThread3();Thread t = new Thread(runnable);t.start();}
}
3. 继承 Thread 重写 run(),使用匿名内部类
public class ThreadDemo4 {public static void main(String[] args) {Thread t = new Thread(){@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};t.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
大括号里面的内容是 Thread 的子类,但是没有名字。t 是这个匿名的子类实例化的对象;
4. 实现 Runnable 接口,重写 run(),使用匿名内部类
public class ThreadDemo5 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
5. 使用 lambda 表达式
public class ThreadDemo6 {public static void main(String[] args) {Thread t = new Thread(() -> {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
能使用 lambda 表达式的原因在于 Thread 的构造方法中,有一个可以传实现 Runnable 接口类对象的构造方法;
3. Thread 的构造方法
public Thread(){......
}public Thread(Runnable target){......
}public Thread(String name){......
}public Thread(Runnable target, String name){......
}
4. Thread 的常见属性
ID:可以通过 getId() 获取,是 JVM 自动分配的身份标识,是唯一的;
名称:可以通过 getName() 获取,在构造方法中可以设置线程的名字;
状态:可以通过 getState() 获取;
优先级:可以通过 getPriority() 获取,不常用;
是否为后台线程:可以通过 isDaemon() 获取;
- 后台线程运行不会阻止进程结束;
- 前台线程运行能够阻止进程结束;
- 自己创建的线程默认是前台线程;
是否存活:可以通过 isAlive() 获取;
- 表示内核中的线程(PCB)是否还存在;
- new 完线程后,内核中的 PCB 还没有创建,这时候是 false,但是线程的变量存在;
- 只有调用了 start() 方法才会创建,这时候为 true;
- 线程的 run() 方法执行完成之后,内核中的线程就结束了,PCB 被释放了,此时为 false,但是线程的变量可能也还存在;
public class ThreadDemo7 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}});// 还没由执行 start(),为 falseSystem.out.println(t.isAlive());// 执行了 start() 为 truet.start();System.out.println(t.isAlive());try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}// 执行完 run() 方法了,为 falseSystem.out.println(t.isAlive());}
}
是否被中断:可以通过 isInterrupted() 获取;、
5. 线程的启动
Thread类使用 start() 方法,启动一个线程;
对于同一个 Thread 对象来说,start() 方法只能调用一次;
调用 start() 方法,才会调用操作系统的 API,完成创建线程的操作;
public class ThreadDemo8 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});//t.start();t.run();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
在上述代码中,在主线程中使用 t 调用 start() 方法和使用 t 调用 run() 方法有什么区别呢?
- 调用 start() 方法才会调用系统 API,创建新的线程,并执行;
- 调用 run() 方法,只会在 main() 线程中执行,不会创建新的线程;
- 上述代码中,如果调用 start(),则两个线程中的两个循环都可以打印;如果调用 run() 方法,只会在主线程中调用 t 线程中的逻辑进行打印,不会运行主线程中的循环进行打印;
6. 线程的终止
线程中的 run() 方法的逻辑执行完毕,线程就会终止;
如果想让线程提前终止,可以通过引入标志位的方式实现:
public class ThreadDemo9 {private static boolean flag = false;public static void main(String[] args) {Thread t = new Thread(() -> {while(!flag){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("使 t 线程提前终止");flag = true;System.out.println("hello main");}
}
还可以使用线程提供的方法 interrupt():
public class ThreadDemo10 {public static void main(String[] args) {Thread t = new Thread(() -> {while(!Thread.currentThread().isInterrupted()){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("提前终止 t 线程");t.interrupt();}
}
通过 interrupt() 方法,可以将 Thread 类中的内置变量置 true,可以实现线程的提前终止;
但是要注意:如果想让线程提前终止,线程本身必须支持被提前终止,必须有类似关于 Thread.currentThread().isInterrupted() 这样的判断;如果没有这样的判断,线程是不能提前终止的;
注意:
上述代码是不能提前终止的,原因是 interrupt() 方法实际上是提前终止 sleep() 方法,sleep() 终止后,Thread 类中的标志位就会被清除,之后会继续执行 catch() 里面的逻辑;通过这样的方式相比直接终止线程给程序员提供了更大的操作空间:
catch() 里面的逻辑:
- 可以抛出异常;
- 也可以使用 break 终止;
- 也可以继续让程序执行;
- 也可以进行一定的处理,之后再使用 break 终止;
7. 等待线程
多个线程的执行顺序是不一定的,在内核中多个线程是随机调度,线程是抢占式执行的;
虽然内核中的调度是随机的,但是可以在应用程序中,通过 API,影响线程执行的顺序;
join() 是一种影响线程结束顺序的方式;
假设有两个线程 main 线程和 t 线程,在 main 线程中调用 t.join(),main 线程就会等待 t 线程结束后再继续向下执行,就保证了 main 在 t 线程结束后结束;
public class ThreadDemo11 {public static void main(String[] args) {Thread t = new Thread(() -> {for(int i = 0; i < 5; i++){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();try {Thread.sleep(1000);t.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("hello main");}
}
join() 是多线程最核心的 API 之一,经常用于多个线程执行完毕后,在某个线程中汇总结果;
哪个线程调用 join(),哪个线程就阻塞等待;
使用哪个对象调用,哪个线程就是被等待的对象;
join(long millis): void join() 方法的带参数版本,millis 表示超时时间;
join() 方法也可以通过 interrupt() 方法唤醒;
8. 获取线程引用
Thread.currentThread(): 获取当前线程的引用;
如果是继承 Thread,直接使用 this 拿到线程实例;
如果是 Runnable 或者 Lambda 表达式的方式,就不能使用 this 获取 Thread 对象;
class MyThread4 extends Thread{@Overridepublic void run() {System.out.println("ID: " + this.getId() + ", name: " + this.getName());}
}
public class ThreadDemo12 {public static void main(String[] args) {Thread t1 = new MyThread4();Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {Thread tmp = Thread.currentThread();System.out.println("ID: " + tmp.getId() + ", name: " + tmp.getName());}});Thread t3 = new Thread(() -> {Thread tmp = Thread.currentThread();System.out.println("ID: " + tmp.getId() + ", name: " + tmp.getName());});t1.start();t2.start();t3.start();}
}
9. 线程的状态
就绪状态:表示线程可以随时去 CPU 上执行,包含正在 CPU 上执行;
阻塞:线程暂时无法去 CPU 上执行;
Java 中,线程有以下几种状态:
- NEW:Thread 对象创建好了,但是还没有调用 start() 方法,没有在操作系统内核中创建线程;
- TERMINATED:Thread 对象仍然存在,但是操作系统内部的线程已经执行完了;
- RUNNABLE:就绪状态,表示这个线程正在 CPU 上执行,或者准备就绪随时可以去 CPU 上执行;
- TIMED_WAITING:指定时间的阻塞,达到一定的时间后,自动解除阻塞;使用 sleep() 可以进入这个状态,使用带参数的 join() 方法,也能进入这个状态;
- WAITING:不带时间的阻塞(死等),必须满足一定的额条件才会解除阻塞;使用 join() 或者 wait() 都会进入这个状态;
- BLOCKED:由于锁竞争,引起的阻塞;
public class ThreadDemo13 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for(int i = 0; i < 5; i++){System.out.println("t 线程正在执行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});System.out.println(t.getState());t.start();Thread.sleep(500);System.out.println(t.getState());t.join();t.getState();}
}
可以借助 jconsole 查看线程的状态: