Java学习笔记——第二十天

多线程

概述

什么是线程

  • 线程(Thread)是一个程序内部的一条执行流程。
  • 程序中如果只有一条执行流程,那这个程序就是单线程的程序。

什么是多线程

多线程是指从软硬件上实现多条执行流程的技术(多条线程由CPU负责调度执行)。

多线程用在哪里,有什么好处

同时处理多个用户的请求。

多线程的创建

Java是通过java.lang.Thread 类的对象来代表线程的。

方法一:继承Thread类

实现步骤

  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法。
  2. 创建MyThread类的对象。
  3. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)。

优缺点

  • 优点:编码简单。

  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

注意事项

  • main方法是由一条默认的主线程负责执行的,main方法内部启动的其他线程都是子线程。
  • 启动线程必须是调用start方法,不是调用run方法。
    • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
    • 只有调用start方法才是启动一个新的线程执行。
  • 不要把主线程任务放在启动子线程之前。
    • 这样主线程一直是先跑完的,相当于是一个单线程的效果了。

方式二:实现Runnable接口

实现步骤

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
Thread类提供的构造器 说明
public Thread(Runnable target) 封装Runnable对象成为线程对象
  1. 调用线程对象的start()方法启动线程

优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
  • 缺点:需要多创建一个Runnable对象,且如果线程有执行结果是不能直接返回的。

方式二的匿名内部类写法

  1. 可以创建Runnable的匿名内部类对象。
  2. 再交给Thread线程对象。
  3. 再调用线程对象的start()启动线程。

方式三:实现Callable接口

  • JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
  • 这种方式最大的优点:可以返回线程执行完毕后的结果。

实现步骤

  1. 创建任务对象
    • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    • 把Callable类型的对象封装成FutureTask(线程任务对象)。
  2. 把线程任务对象交给Thread对象。
  3. 调用Thread对象的start方法启动线程。
  4. 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

FutureTask的API

FutureTask提供的构造器 说明
public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。
FutureTask提供的方法 说明
public V get() throws Exception 获取线程执行call方法返回的结果。
  • 注意:当调用get方法获取返回值时,如果子线程还没有执行完毕,那么主线程的执行会暂停,等待子线程执行完毕后才会获取结果。

优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
  • 缺点:编码复杂一点。

三种线程的创建方式优缺点的对比

方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂

Thread的常用方法

Thread提供的常用方法 说明
public void run() 线程的任务方法
public void start() 启动线程
public String getName() 获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name) 为线程设置名称
public static Thread currentThread() 获取当前执行的线程对象
public static void sleep(long time) 让当前执行的线程休眠多少毫秒后,再继续执行
public final void join() 让调用这个方法的线程先执行完,再执行后面的代码
Thread提供的常见构造器 说明
public Thread(String name) 可以为当前线程指定名称
public Thread(Runnable target) 封装Runnable对象成为线程对象
public Thread(Runnable target, String name) 封装Runnable对象成为线程对象,并指定线程名称

Thread的其他方法

Thread类还提供了诸如yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用。

线程安全

什么是线程安全问题

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

线程安全问题出现的原因

  • 存在多个线程在同时执行。
  • 且同时访问一个共享资源。
  • 且存在修改该共享资源的操作。

线程同步

认识线程同步

线程同步是解决线程安全问题的方案。

线程同步的思想

让多个线程依次访问共享资源,且同时只能有一个线程访问,这样就解决了安全问题。

线程同步的常见方案

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

方式一:同步代码块

作用

把访问共享资源的核心代码给上锁,以此保证线程安全。

格式

synchronized(同步锁) {
    访问共享资源的核心代码
}

原理

每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

注意事项

对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

锁对象的使用规范

  • 锁对象不能随便选择一个唯一的对象,因为会影响其他无关线程的执行。

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象,对于静态方法建议使用字节码(类名.class)对象作为锁对象。

方式二:同步方法

作用

把访问共享资源的核心方法给上锁,以此保证线程安全。

格式

修饰符 synchronized 返回值类型 方法名称(形参列表) {
    操作共享资源的代码
}

原理

每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

同步代码块与同步方法的比较

  • 范围上:同步代码块锁的范围更小,执行效率更高;同步方法锁的范围更大,执行效率稍低。
  • 可读性上:同步方法更好。

方式三:Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。

  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

构造器 说明
public ReentrantLock() 获得Lock锁的实现类对象

Lock的常用方法

方法名称 说明
void lock() 获得锁
void unlock() 释放锁

注意事项

  • 在Lock对象前加上final修饰符,以免锁对象被修改。
  • 将锁定代码和核心代码放进try内,将解锁代码放进finally,防止出现核心代码出现异常导致没有解锁,使其他线程无法获取锁的情况。

线程通信

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  • 生产者线程负责生产数据
  • 消费者线程负责消费生产者生产的数据。
  • 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产。

Object类的等待和唤醒方法

方法名称 说明
void wait() 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法
void notify() 唤醒正在等待的单个线程
void notifyAll() 唤醒正在等待的所有线程
  • 上述方法应该使用当前同步锁对象进行调用。
  • 要先唤醒别人,再等待自己。

线程池

认识线程池

线程池是一个可以复用线程的技术。

不使用线程池的问题

用户每发起一个请求,后台就需要创建一个新线程来处理,当请求过多时,会产生大量的线程出来,而创建新线程的开销是很大的,这样会严重影响系统的性能。

线程池的工作原理

  1. 预先设定工作线程(WorkThread)和任务队列(WorkQueue)的最大数量。
  2. 当新的任务需要执行时,线程池会查看是否有空闲线程。
  3. 如果有空闲线程,则分配一个工作线程执行任务。
  4. 如果没有空闲线程,则任务会放入任务队列等待执行。
  5. 如果没有空闲线程,且线程池没有达到最大线程数,则创建一个新的临时线程来执行任务。
  6. 如果临时线程数和任务队列都已满,则根据策略拒绝新的任务或者抛出异常。
  7. 当临时线程的空闲时间达到存活时间时会被关闭。

线程池的创建

谁代表线程池

JDK 5.0起提供了代表线程池的接口:ExecutorService。

如何得到线程池对象

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自己创建一个线程池对象。

  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

ThreadPoolExecutor构造器

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
  • 参数一:corePoolSize : 指定线程池的核心线程的数量。
  • 参数二:maximumPoolSize:指定线程池的最大线程数量。
  • 参数三:keepAliveTime :指定临时线程的存活时间。
  • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)。
  • 参数五:workQueue:指定线程池的任务队列。
  • 参数六:threadFactory:指定线程池的线程工厂。
  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)。

核心线程数量配置

  • 计算密集型任务:核心线程数量 = CPU的核数 + 1
  • IO密集型任务:核心线程数量 = CPU的核数 * 2

线程池的注意事项

  • 临时线程创建的时机:新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  • 拒绝新任务的时机:核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

线程池处理Runnable任务

ExecutorService处理Runnable任务的常用方法

方法名称 说明
void execute(Runnable command) 执行 Runnable 任务
void shutdown() 等全部任务执行完毕后,再关闭线程池
List<Runnable> shutdownNow() 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

新任务拒绝策略

策略 说明
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常(默认策略
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常(这是不推荐的做法)
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的run()方法从而绕过线程池直接执行

线程池处理Callable任务

ExecutorService处理Callable任务的常用方法

方法名称 说明
Future<T> submit(Callable<T> task) 执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果

Executors工具类实现线程池

Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

方法名称 说明
public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它
public static ExecutorService newSingleThreadExecutor() 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务

注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

Executors使用可能存在的陷阱

大型并发系统环境中使用Executors如果不注意可能会出现系统风险:

  1. FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致内存溢出。
  2. CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致内存溢出。

并发、并行

进程

  • 正在运行的程序(软件)就是一个独立的进程。
  • 线程是属于进程的,一个进程中可以同时运行很多个线程。
  • 进程中的多个线程其实是并发和并行执行的。

并发

  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

  • 简单地说就是:CPU分时轮询的执行线程。

并行

  • 在同一个时刻上,同时有多个线程在被CPU调度执行,依赖CPU的核数。

  • 简单地说就是:同一个时刻同时在执行。

多线程是怎么在执行的

并发和并行同时进行的。

线程的生命周期

线程的生命周期就是线程从生到死的过程中,经历的各种状态及状态转换。

Java线程的状态

Java总共定义了6种状态,这6种状态都定义在Thread类的内部枚举类中。

线程状态 说明
NEW(新建) 线程刚被创建,但是并未启动
Runnable(可运行) 线程已经调用了start(),等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态
Terminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

线程的6种状态互相转换

线程的6种状态互相转换

  • 当线程刚被创建出来时,其处于New状态。
  • 当处于New状态的线程调用了start()方法后,其会进入Runnable状态。
  • 当处于Runnable状态的线程执行完毕或出现异常时,其会进入Terminated状态。
  • 当处于Runnable状态的线程未竞争获得锁对象时,其会进入Blocked状态。
  • 当处于Blocked状态的线程竞争获得了锁对象时,其会进入Runnable状态。
  • 当处于Runnable状态的线程在获得锁对象之后调用了wait()方法时,其会进入Waiting状态,且会释放锁对象。
  • 当处于Waiting状态的线程被其他线程notify并竞争获得了锁对象时,其会进入Runnable状态。
  • 当处于Waiting状态的线程被其他线程notify单未竞争获得锁对象时,其会进入Blocked状态。
  • 当处于Runnable状态的线程调用了sleep(毫秒)或wait(毫秒)方法时,其会进入Timed Waiting状态,且wait(毫秒)方法会释放锁对象,而sleep(毫秒)方法不会释放锁对象。
  • 当处于Timed Waiting状态的线程sleep时间到了或wait时间到了,且竞争获得了锁对象或wait时间没到,被其他线程notify,且竞争获得了锁对象时,其会进入Runnable状态。
  • 当处于Timed Waiting状态的线程wait时间到了,但未竞争获得锁对象或wait时间没到,被其他线程notify,但未竞争获得锁对象时,其会进入Blocked状态。

补充:乐观锁

悲观锁

在执行核心代码前加锁,每次都只能有一个线程进入,访问完毕后,再解锁。

特点

线程安全,性能较差。

乐观锁

不上锁,所有线程一起执行,同时检测每一次的执行结果,如果结果没问题,则说明没有出现线程安全的问题,反之如果有问题,则进行处理。

特点

线程安全,性能较好。

原理

CAS(compare and set,比较和修改)算法:

  1. 当一个线程访问到共享内容时,先记住当前共享内容的值。
  2. 然后执行预定的操作,执行完毕后先不赋值。
  3. 将之前记住的共享内容的值与当前的共享内容的值进行比较,如果相等,则说明不会出现线程安全问题,赋值即可。
  4. 如果不相等,则说明会出现线程安全问题,丢弃掉这次操作即可。

代码实现

  • 使用原子类提供的对象。

  • 修改整型变量的原子类为AtomicInterger。

AtomicInterger提供的方法 说明
public final int incrementAndGet() 以原子方式递增当前值
posted @   zgg1h  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示