Java多线程
个人结合所学知识整理,如有错误理解敬请指正,感谢🙏
0.基础知识
程序、进程、线程
- 程序(program)本质上是为了完成某项特定任务而使用某种语言编写的指令集合
- 进程(process)是程序的一次执行过程,当程序开始运行,会被加载到内存中,执行时占用cpu资源,执行完毕释放资源(产生-存在-消亡的过程即线程的生命周期);进程可以理解为cpu的资源分配单位,系统在运行时会给每个进程分配不同的内存区域
- 线程(thread)进程的进一步细化,是程序的执行路径。如果一个进程能够同步执行多个线程,那么这个进程就是支持多线程的;线程可以理解为cpu调度和执行单位,每个线程拥有独立的运行栈和程序计数器,线程切换开销小
并行、并发
- 并行:多个cpu同时执行多个任务,如多个人做不同的事,但实际上此时一个人只能做一件事
- 并发:一个cpu同时做多个任务,如一个人同时做多个任务,但实际上同一时间并不是做个任务同时在做,而是采用时间片的技术,分时执行
多线程的优点
- 提高程序的响应速度,提高用户的使用体验
- 提高cpu利用率,减少资源闲置
- 改善程序结构,将复杂的程序细化为多个线程独立运行,有利于理解和修改
多线程的应用场景
- 程序需要同时执行多个任务
- 避免程序在执行==需要等待的任务(如用户输入、文件读写操作、网络操作、搜索等)==时阻塞其他任务的运行
- 需要一些后台执行的程序是也需要用到多线程
Java线程的分类
- 用户线程:开发者自己编写的线程
- 后台(守护)线程:用来服务用户线程的线程
1.线程的创建
线程的四种创建方式
-
继承Thread类
//1.创建线程类并继承Thread类,重写run方法 public class MyThread extends Thread { @Override public void run() { System.out.println("myThreadRun"); } } //测试 @Test void testCreateThreadByExtends(){ //创建线程实例 MyThread myThread = new MyThread(); //调用start方法运行线程 myThread.start(); } //运行结果:myThreadRun -
实现Runnable接口(线程无返回值)
//2.实现Runnable接口,重写run方法 public class MyThread implements Runnable { @Override public void run() { System.out.println("myThreadRun"); } } //测试 @Test void testCreateThreadByImplements(){ //创建线程实例 MyThread myThread = new MyThread(); //实例化Thread类,并传入自己的线程类 Thread thread = new Thread(myThread); //调用start方法运行线程 thread.start(); } //运行结果:myThreadRun -
实现Callable接口(线程有返回值)
//3.实现Callable接口,泛型为返回值类型,重写call方法 public class MyCallableThread implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i < 10; i++) { if(i%2==0){ sum += i; } } return sum; } } //测试 @Test void testCreateCallableThread(){ //创建自定义线程 MyCallableThread callableThread = new MyCallableThread(); //创建FutureTask实例并传入自定义线程 FutureTask<Integer> futureTask = new FutureTask<>(callableThread); //创建线程实例并传入FutureTask实例 Thread thread = new Thread(futureTask); //调用start方法启动线程 thread.start(); //获取线程返回的值 try { Integer sum = futureTask.get(); System.out.println("10以内偶数和为:" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } //输出结果:10以内偶数和为:20 -
通过线程池创建
//4.创建自定义线程,通过线程池执行线程 public class MyThread implements Runnable { @Override public void run() { System.out.println("MyThreadRun"); } } //测试 @Test void testThreadPool(){ //创建指定数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor threadPool = (ThreadPoolExecutor) service; //执行指定的线程 threadPool.execute(new MyThread());//适用于实现Runnable接口的线程 //service.submit(CallableThread) 适用于实现Callable接口的线程 //关闭线程池 threadPool.shutdown(); } //输出结果:MyThreadRun
2.四种创建线程方式的比较
Thread类和Runnable接口
-
继承Thread类和实现Runnable接口两种方式都需要重写run()方法,线程执行逻辑都在这个方法中
-
Thread类实际上也实现了Runnable接口
-
Java仅支持单继承,因此如果一个类已经继承了有父类,那么就不能够再继承Thread类,但Java支持同时实现多个接口,因此使用实现Runnable接口创建线程类的方式更加实用
-
Runnable接口实现线程更有益于多线程数据共享,只需要实例化一个线程对象,将这个对象传入Thread的构造器中,多个线程之间的变量不需要使用static关键字,即可达到共享变量的效果
public class MyThread implements Runnable { int num; public MyThread(int num) { this.num = num; } @Override public void run() { System.out.println(this.num); } } //测试 @Test void testRunnableThread(){ //只需要实例化一个自定义线程 MyThread myThread = new MyThread(3); //传入到不同的Thread实例中即可达到共有变量的效果 Thread thread1 = new Thread(myThread); Thread thread2 = new Thread(myThread); thread1.start();//输出3 thread2.start();//输出3 } -
继承Thread的方式需要实现多个Thread子类来创建多个线程,如果线程类中的变量不使用static关键字将其变为类级别变量,那么每个实例对象都会有自己独立的变量,无法共享
public class MyThread extends Thread { int num; static String share; public MyThread(int num) { this.num = num; } public MyThread(int num,String share) { this.num = num; this.share = share; } @Override public void run() { System.out.println("num=" + this.num + ",share=" +this.share); } } //测试 @Test void testThread(){ //需要实例化多个对象创建多个线程 MyThread thread1 = new MyThread(3, "static变量"); MyThread thread2 = new MyThread(1); thread1.start();//输出:num=3,share=static变量 thread2.start();//输出:num=1,share=static变量 }
Callable接口
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
-
相比前两种方式执行逻辑需要写在无返回值的run()方法内,实现Callable接口的线程类执行逻辑放在了支持泛型返回值的call()方法内,且可以抛出异常被外部的操作捕获
-
但这种方式比前两种稍微麻烦,需要借助FutureTask类,如获取返回值,需要在线程启动计算之后借助FutureTask类的get()方法
线程池
- 通过线程池可以提前创建好一定数量的线程,需要使用时直接取出,避免频繁地创建和销毁线程,实现线程重复利用
- 好处是可以减少创建线程的时间,提高响应速度,避免高并发下经常创建和销毁线程,节省资源消耗,且利于管理线程
3.线程的生命周期
生命周期
- 线程的生命周期值线程从创建到销毁经历的过程
- 一般而言,线程有==新建(new)、就绪(runnable)、运行(running)、阻塞(blocked)、死亡(dead)==五种状态
- 新建:当使用new关键字实例化一个线程对象后,该线程就处于新建状态,此时由JVM给其分配内存并初始化成员变量的值
- 就绪:当实例对象调用了start()方法,线程就处于就绪状态,JVM为其创建方法调用栈和程序计数器,等待调用
- 运行:当处于就绪状态的线程获得了cup的使用权,就开始run()方法中的方法体,此时线程处于运行状态
- 阻塞:当运行中的线程被某种原因让出了cpu的使用权而暂时停止运行,线程就处于阻塞状态,直到线程转为就绪状态,才有机会重新获取cpu进入运行状态
- 死亡:线程以某种方式结束后会进入死亡状态,死亡后的线程无法再进入就绪状态,即无法再次运行
阻塞线程的原因
- 阻塞的情况分三种:等待阻塞(o.wait -> 等待队列)、同步阻塞(lock - > 锁池)、其他阻塞(sleep/join)
- 等待阻塞:运行中的线程调用了o.wait方法,JVM会将该线程放入等待队列中,直到有线程调用了notify或者notifyAll方法将其唤醒,才会进入就绪状态
- 同步阻塞:运行的线程在尝试获取同步锁时,锁已经被其他线程占用,JVM会将该线程放入锁池,当线程获取到锁后可以进入运行状态
- 其他阻塞:运行的线程执行了sleep方法或join方法或发生了IO请求,JVM会把程序设置为阻塞状态,直到sleep超时或join等待线程终止或超时,IO请求处理完毕,线程会重新转入就绪状态
线程死亡的原因
- 线程死亡的原因有三种:正常结束、异常结束、调用stop结束
- 正常结束:run()方法或call()方法执行完毕,程序正常结束
- 异常结束:线程抛出了一个未捕获的Error或Exception
- 调用stop:直接调用线程的stop方法结束线程,很可能会导致死锁,不建议使用
终止线程的方式
-
终止线程的方式有四种:正常结束、使用终止标志结束、interrupt()方法结束、stop方法结束(不推荐)
-
一般来说线程中的run方法执行完就会结束,但是有时候一些线程需要在满足特定条件之后才应该结束,此时可以使用终止标志来控制是否结束线程
public class MyThread extends Thread { public volatile boolean exit = false;//设置终止标志 public void run() { //执行逻辑 System.out.println("MyThreadRun"); //通过终止标志控制线程持续执行,直到满足特定条件,终止标志转变 while (!exit){ //do something } } } -
使用interrupt()方法终止线程的方式有两种,当线程处于阻塞状态,调用线程的interrupt()方法使其抛出InterruptException异常,捕获到异常后通过break跳出循环结束执行;当线程未处于阻塞状态,则使用线程的isInterrupt()方法判断中断标志来退出循环
//未阻塞的情况 class MyThread extends Thread { public void run(){ while (!interrupted()){ try { System.out.println("run开始"); System.out.println(isInterrupted()); System.out.println("run休眠5s"); Thread.sleep(5000); System.out.println("run休眠结束"); }catch (InterruptedException e){ System.out.println("捕获到异常"); e.printStackTrace(); break; } } } } //测试 @Test void testThread() throws InterruptedException{ MyThread m1 = new MyThread(); System.out.println("启动m1"); m1.start(); System.out.println("主线线程休眠2s"); Thread.sleep(2000); System.out.println("主线程休眠结束,调用 m1.interrupt()"); m1.interrupt(); } /** 执行结果: 启动m1 主线线程休眠2s run开始 false run休眠5s 主线程休眠结束,调用 m1.interrupt() 捕获到异常 java.lang.InterruptedException: sleep interrupted at java.base/java.lang.Thread.sleep(Native Method) at cn.itcast.hotel.thread.MyThread.run(ThreadTests.java:59) */
4.线程的基本方法
常用方法
- 线程的常用方法有wait()、notify()、notifyAll、sleep()、join()、yield()、interrupt()
- wait:线程等待,Object中的方法,运行中的线程调用wait()方法后会进入等待状态,直到被其他线程唤醒中断才会返回,此方法会释放同步锁
- notify:随机唤醒一个wait的线程,Object中的方法,随机唤醒一个当前对象监视器中处于等待状态的线程,唤醒后的线程需要先与其他线程竞争锁
- notifyAll:唤醒所有wait的线程,Object中的方法,唤醒所有当前对象监视器中处于等待状态的线程,唤醒后的线程需要先竞争锁,谁拿到锁谁先执行
- sleep:线程休眠,Thread中的方法,运行中的线程调用sleep()方法后会暂停指定的时间,让出cpu给其他线程,但当前线程并不会改变监控状态,也不会释放锁资源,等到暂停的时间结束,又会自动回复到运行状态
- join:等待其他线程终止,Thread中的方法,当需要其他线程返回结果时使用,在当前线程中调用一个线程的join()方法,则当前线程转为阻塞状态,直到join的线程结束,当前线程转为就绪状态
- yield:线程让步,Thread中的方法,一般在run()方法中调用,当前线程调用该方法后会让出cpu执行时间片,与其他线程重新竞争cpu
- interrupt:线程中断,Thread中的方法,运行中的线程在调用这个方法之后并不会直接结束线程,而是改变了线程中的中断标识;处于sleep状态的线程调用该方法后会抛出InterruptedException异常。通过interrupt方法和while循环可以巧妙得控制线程是否终止
其他方法
- isAlive:判断线程是否存活
- activeCount:程序中活跃的线程数
- currentThread:得到当前线程
- isDaemon:判断线程是否是守护线程
- setDaemon:设置线程为守护线程
- setName:为线程设置一个名字
5.线程池
- Java中的线程池顶级接口为Executor,但实际上Executor只是一个执行线程的工具,只提供了一个execute方法,真正的线程池接口是ExecutorService
Java中的四种线程池(Executors创建线程的四种方式)
-
newSingleThreadExecutor:创建单线程化线程池
-
单线程化的线程池中只有一个线程,且这个线程的存活时间是无限的
-
单线程化线程池中的任务是按照提交的顺序依次执行的,当唯一的线程正繁忙时,新提交的任务会进入内部的阻塞队列,这个阻塞队列是无界的
-
适用于任务按照次序逐个执行的场景
// 创建单线程-线程池,任务依次执行 ExecutorService newScheduledThreadPool = Executors.newSingleThreadExecutor(); for (int i = 0; i < 5; i++) { //创建任务 Runnable runnable = new Runnable(){ @Override public void run() { System.out.println(Thread.currentThread().getName()); } }; // 将任务交给线程池管理 newScheduledThreadPool.execute(runnable); }
-
-
newFixedThreadPool:创建固定数量的线程池
-
固定数量线程池在线程数没有达到固定数量时,没提交一个任务线程池会创建一个新线程,直到线程池中的线程数量达到固定数量
-
线程池中的线程一旦达到固定数量就会保持不变,如果中途有一个线程发生异常而结束,线程池会补充一个新线程
-
在接收异步任务的执行目标实例时,如果所有线程都处于繁忙状态,新任务会进入无界的阻塞队列
-
适用于需要任务长期执行或cpu密集型任务场景,但由于任务队列为无界的,如果有超过了线程池最大容量的大量任务需要处理,队列会无限扩大导致服务器资源耗尽
// 创建定长线程池 ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2); for (int i = 0; i < 5; i++) { //创建任务 Runnable runnable = new Runnable(){ @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); } }; // 将任务交给线程池管理 newFixedThreadPool.execute(runnable); }
-
-
newCachedThreadPool:创建可缓存进程池
-
可根据需要创建新线程,如果以前构造的线程可用会重用这些线程,但是如果线程池中保有的线程数量多于任务数,会回收空闲时间超过60秒的线程
-
当接收到新的异步任务而所有线程处于繁忙状态,线程池会创建新的线程来处理任务,线程池不会限制限制容量的大小,只要没达到JVM创建线程大小的限制就能一直创建线程
-
适用于需要快速处理突发性强而耗时短的任务,如Netty的NIO处理场景、Rest API接口的瞬时削峰场景
// 创建可缓存线程池 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { //创建任务 Runnable runnable = new Runnable(){ @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); } }; newCachedThreadPool.execute(runnable); }
-
-
newScheduledThreadPool:创建可调度线程池
-
可安排任务在给定时间后运行或定期运行
-
线程数不设上限,可能会导致创建大量的线程而发生OOM
// 创建支持定时线程池 ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(2); for (int i = 0; i < 5; i++) { //创建任务 Runnable runnable = new Runnable(){ @Override public void run() { System.out.println(Thread.currentThread().getName()); } }; // 将任务交给线程池管理,延迟2秒后才开始执行线程池中的所有任务 newScheduledThreadPool.schedule(runnable, 2, TimeUnit.SECONDS); }
-
-
总结:
-
newFixedThreadPool和newSingleThreadExecutor: 阻塞队列无界,会堆积大量任务导致OOM(内存耗尽)
-
newCachedThreadPool和newScheduledThreadPool: 线程数量无上界,会导致创建大量的线程,从而导致OOM
-
建议直接使用线程池ThreadPoolExecutor的构造器
//标准创建形式 public class ThreadPoolExecutor extends AbstractExecutorService { private volatile int corePoolSize;//核心线程数,即使线程空闲也不会被收回 private volatile int maximumPoolSize;//线程的上限 private volatile long keepAliveTime;//线程的最大空闲时长 private final BlockingQueue<Runnable> workQueue;//任务的排队队列 private volatile ThreadFactory threadFactory;//新线程的产生方式 private volatile RejectedExecutionHandler handler;//拒绝策略 }
-
向线程池提交任务的方式
- execute:void execute(Runnable command), Executor接口中的方法
- 只能接收Runnable接口的参数
- 无返回结果
- 无法抛出异常
- submit: Future submit(Callable task)、 Future submit(Runnable task, T result)、Future<?> submit(Runnable task),Executor接口中的方法
- 可以接收Runnable和Callable接口的参数
- Callable接口支持返回结果
- 可以抛出异常
6.拒绝策略
- 拒绝的情况:线程池已关闭或线程池已满且maximumPoolSize(最大线程数)已满
- AbortPolicy:拒绝策略,默认策略,新任务会被拒绝并抛出RejectedExecutionException异常
- DiscardPolicy:抛弃策略,新任务会被直接抛弃且没有异常抛出
- DiscardOldestPolicy:抛弃老任务策略,将最早进入队列的任务抛弃,从而腾出空间再加入新任务
- CallerRunsPolicy:调用者执行策略,新任务被添加到线程池时,如果添加失败,提交线程会自己去执行任务,不再使用线程池中的线程
7.ThreadLocal和ThreadLocalMap
ThreadLocal
-
介绍 :ThreadLocal即线程局部变量,主要作用是同一个线程类的线程实例通过ThreadLocal可以为每个线程实例创建一份自己独有的变量副本,每个Thread实例通过ThreadLocal操作变量时访问的是自己内部的变量副本,在操作这个变量时不会影响到其他线程也不会受到其他线程的影响,从而解决线程安全的问题
-
同一个ThreadLocal变量所包含的对象在不同的Thread中有不同的副本,且该副本只能当前线程访问
-
ThreadLocal的作用只是操作数据,真正存储数据的是ThreadLocalMap,实际上操作数据时也是调用的ThreadLocalMap的方法
-
set方法源代码详解:
public void set(T value) {//调用set方法可以传入一个泛型的参数value Thread t = Thread.currentThread();//获取当前线程 ThreadLocalMap map = getMap(t);//获取当前线程中的ThreadLocalMap if (map != null) {//如果这个ThreadLocalMap不为空 map.set(this, value);//存储数据,map中的key为当前ThreadLocal对象,传入的value则封装为一个Entry } else { createMap(t, value);//如果ThreadLocalMap为空则调用方法初始化map并保存数据 } } -
get方法源代码详解:
public T get() {//调用get方法可以得到当前ThreadLocal在ThreadLocalMap中对应的值 Thread t = Thread.currentThread();//获取当前线程 ThreadLocalMap map = getMap(t);//获取当前线程中的ThreadLocalMap if (map != null) {//如果map不为空 ThreadLocalMap.Entry e = map.getEntry(this);//查找当前ThreadLocal在map中的Entry对象 if (e != null) {//查找到保存的Entry对象 @SuppressWarnings("unchecked") T result = (T)e.value;//返回Entry的值也即之前set保存的value return result; } } return setInitialValue();//map为空时初始化当前ThreadLocal映射的值为null }
ThreadLocal和Sychronized的区别
- ThreadLocal和Sychronized有着本质的区别
- ThreadLocal用于线程之间的数据隔离,每个线程操作的数据是自己线程内部的数据副本,多个线程可以同时操作自己内部的数据副本
- Sychronized作用在多线程之间数据共享的场景,每个线程在操作数据时会阻塞其他线程,同时只能用一个线程操作这个数据
示例
-
图示
-
代码
public class ThreadTests {
protected static ThreadLocal<String> name = new ThreadLocal<>();//多个子线程都需要操作name这个变量,但不能互相影响,此时name的值为空
//打印输出
public void print(String threadName){
System.out.println(threadName + ":"
+ ThreadTests.name.get());
}
@Test
void testThread() {
new Thread(new Runnable() {
@Override
public void run() {
name.set("lucy");//通过set将name变量的值设置为lucy
print("thread-1");
//name.remove();
//print("Thread-1移除name");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
//name.set("jack");
print("thread-2");
}
}).start();
}
}
/**
第一次输出,此时线程2没有设置name:
thread-1:lucy
thread-2:null
第二次输出,线程1移除name的值,线程2将name设置为lucy
thread-1移除前:lucy
Thread-1移除name:null
thread-2:jack
*/
本文详细介绍了Java多线程的基础知识,包括程序、进程、线程的概念,以及并行、并发的区别。接着讨论了多线程的优点和应用场景,并列举了线程的四种创建方式:继承Thread、实现Runnable、Callable接口和线程池。文章还分析了线程的生命周期、状态转换,以及线程的常用方法。此外,探讨了Java的四种线程池和拒绝策略,并对比了ThreadLocal和Synchronized的区别。
441

被折叠的 条评论
为什么被折叠?



