并发之线程
目录
线程与进程
什么是进程
什么线程
CPU和线程的关系(cpu调度线程)
多线程
线程上下文切换
串行、并行、并发
创建线程的三种方式
同步异步、阻塞非阻塞
线程的生命周期
线程执行任务什么时候会让出CPU
线程通信
wait()、notify()、notifyAll()
wait()和sleep()的区别
park、unpark
wait/notify 与park/unpark区别
中断
Wait set 和Entry set
Wait Set(等待集)
Entry Set(入口集)
wait set中被唤醒后的线程做什么
线程与进程
什么是进程
- 进程是指运行中的程序。 比如我们使用钉钉,浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源(占用内存资源)。
什么线程
- 线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段。
进程是操作系统分配的资源,线程是CPU调度的基本单位,一个进程至少有一个或多个线程。
比如Java中main方法启动后就是一个进程,最起码也有main主线程和gc线程,还有其他线程
CPU和线程的关系(cpu调度线程)
一个CPU管理多个线程,CPU在执行一个线程任务的过程中,会出现阻塞的情况(比如网络io或磁盘数据io),这时CPU没事干就可以去执行其他线程任务,这就是多线程并发执行。
但同一时刻一个CPU只会执行一个线程
CPU在执行线程任务的过程中,每个线程执行时都有自己独立的CPU工作内存,CPU从主存中拿到数据后会缓存到CPU工作内存,线程执行写数据时会在CPU工作内存中执行计算,再同步到主存中。
多线程
单个进程中同时运行多个线程。
多线程的目的是为了提高CPU的利用率。可以通过避免一些网络IO或者磁盘IO等需要等待的操作,让CPU去调度其他线程。
多线程的局限
- 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
- 线程安全问题:虽然多线程带来了一定的性能提升,但是再做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题。
线程上下文切换
指CPU的控制权从一个进程或线程转移到另一个进程或线程的过程。 当一个进程或线程需要放弃CPU的控制权时,操作系统会保存其上下文,并恢复另一个进程或线程的上下文,以便继续执行。
常见的上下文信息包括寄存器状态、内存映像、文件描述符和IO状态
串行、并行、并发
串行就是一个一个排队,第一个做完,第二个才能上。
并行就是同时处理。
多线程中的并发概念(CPU调度线程的概念)。CPU在极短的时间内,反复切换执行不同的线程,看似好像是并行,但是只是CPU高速的切换。同一时刻一个cpu指挥执行一个线程。
并行就是多核CPU同时调度多个线程,是真正的多个线程同时执行。
单核CPU无法实现并行效果,单核CPU是并发。
创建线程的三种方式
- 通过继承Thread类来创建并启动线程
- 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
- 通过实现Runnable接口来创建并启动线程
- 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
- 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线程对象。
- 调用线程对象的start()方法来启动该线程。
- 建议实现Runnable接口的方式
- 避免了类的单继承的局限性
- 更适合处理有共享数据的问题
- 实现了代码和数据的分离
- 通过实现Callable接口来创建并启动线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。然后再创建Callable实现类的实例。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
- Runnable接口和Callable接口的区别
- call方法可以有返回值,Callable使用泛型参数,可以指定call方法返回参数类型
- call方法可以用throw的方式处理异常
追其底层,其实只有一种,实现Runnble
同步异步、阻塞非阻塞
- 同步与异步:执行某个功能后,被调用者是否会主动反馈信息
- 阻塞和非阻塞:执行某个功能后,调用者是否需要一直等待结果的反馈。
- 同步阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,需要一直等待水烧开。
- 同步非阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能,但是需要时不时的查看水开了没。
- 异步阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,需要一直等待水烧开。
- 异步非阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能。
- 异步非阻塞这个效果是最好的,平时开发时,提升效率最好的方式就是采用异步非阻塞的方式处理一些多线程的任务。
线程的生命周期
新建状态(New):线程对象被创建,但还没有调用start()方法,因此线程还没有开始执行。
就绪状态(Runnable):调用线程对象的start()方法后,线程进入就绪状态。此时线程位于可运行池中,等待获得CPU的使用权。
运行状态(Running):当就绪状态的线程获得CPU时间片后,开始执行run()方法中的代码,此时线程处于运行状态。
阻塞状态(Blocked):线程因为某种原因放弃CPU使用权,暂时停止运行。阻塞的情况包括等待I/O操作的完成、等待获取一个锁、等待从sleep或wait方法返回等。
死亡状态(Dead):线程执行完run()方法后,或者因为异常退出了run()方法,线程结束生命周期。
线程执行任务什么时候会让出CPU
时间片耗尽:操作系统分配给每个线程一定的CPU时间(时间片)。当时间片耗尽时,线程必须让出CPU,以便其他线程运行。
I/O操作:当线程执行I/O操作(如文件读写、网络通信)时,会进入阻塞状态,从而释放CPU,直到I/O操作完成。
线程阻塞:通过调用sleep()、yield()、wait()等方法,线程可以主动让出CPU。
线程同步:在锁竞争过程中,未获取锁的线程会进入阻塞状态,直到锁被释放,期间线程会让出CPU。
线程优先级:高优先级的线程可以抢占低优先级线程的CPU时间,导致低优先级线程暂时让出CPU。
线程终止:当线程完成任务后调用exit()方法,会释放CPU资源。
操作系统调度:操作系统根据调度策略分配CPU时间,线程可能因调度决策而让出CPU。
线程通信
wait()、notify()、notifyAll()
这三个方法是Object类中声明的方法,必须在同步代码块或同步方法中使用(只有采用synchronized实现线程同步时才能使用这三个方法), 且这三个方法的调用者必须是同步监视器。
- wait():让当前线程进入等待(阻塞)状态,并释放锁(释放对同步监视器的调用)
- notify():唤醒一个正在等待的线程
- notifyAll():唤醒所有正在等待的线程
wait()和sleep()的区别
- sleep()是Thread类中的静态方法,而wait()是Object类中的成员方法;
- sleep()可以在任何地方使用,而wait()只能在同步方法或同步代码块中使用;
- sleep()不会释放锁,而wait()会释放锁(同步监视器),需要通过notify()/notifyAll()唤醒;
park、unpark
park、unpark是LockSupport类名下的方法,park用来暂停线程,unpark用来将暂停的线程恢复。
先park再unpark的方式是容易理解的。park、unpark还可以按照通行令牌走,先执行unpark方法,代表先获得了通行令牌。那么在另一个线程调用park方法时,校验到这个令牌存在,然后消耗掉这个令牌就可以继续往下走。
wait/notify 与park/unpark区别
通行令牌的区别,wait/notify不具备。
wait/notify依赖于锁资源,所以只能在synchronized中来进行使用。park/unpark没有这个限制。
wait/notify的唤醒是随机的,不确定具体唤醒了哪个等待的线程,而park/unpark可以在线程层面上来对特定线程进行唤醒。
中断
interrupt()方法:表示可以中断线程,实际上只是给线程设置一个中断标志,但是线程依旧会执行。
interrupted()方法:Thread类的静态方法。检查当前线程的中断标志,返回一个boolean并清除中断状态,其连续两次调用的返回结果不一样,因为第二次调用的时候线程的中断状态已经被清除,会返回一个false。
Wait set 和Entry set
在Java多线程编程中,wait set(等待集)和entry set(入口集)是两种不同的线程队列,它们用于管理线程同步和通信。它们的主要区别在于它们的用途和工作方式。
Wait Set(等待集)
- 当一个线程调用了对象的 wait() 方法时,它进入该对象的等待集。
- 这通常发生在一个线程需要等待某个特定条件变为真时。例如,当它等待某个资源变得可用或等待某个条件满足时。
- 线程在等待集中等待,直到它被另一个线程通过调用相同对象的 notify() 或 notifyAll() 方法唤醒。
- 在等待集中的线程不会竞争对象锁。
Entry Set(入口集)
- 当多个线程尝试进入一个同步块(即尝试获取对象的锁)时,如果锁已经被其他线程持有,那么这些线程就会进入该对象的入口集。
- 这是一种锁竞争的情况。线程在入口集中等待,直到锁变得可用。
- 一旦锁被释放,处于入口集的线程将尝试获取锁。如果成功,它将离开入口集并进入同步块;如果失败,它将继续留在入口集中等待。
总结来说,等待集用于线程间的协调和条件等待,而入口集用于管理对对象锁的竞争。两者都涉及线程等待的情况,但等待的原因和机制不同。在等待集中的线程是在等待某个条件的变化,而在入口集中的线程是在等待获取锁。
wait set中被唤醒后的线程做什么
当一个线程在等待集(wait set)中等待时,如果另一个线程对同一个对象调用 notify() 或 notifyAll() 方法,等待的线程会被唤醒。但是,唤醒后的行为取决于它如何与对象锁(monitor lock)交互。
- 被唤醒后的流程
- 当一个线程从等待集中被 notify() 或 notifyAll() 唤醒时,它并不会立即执行。
- 被唤醒的线程首先必须重新获得与该对象关联的锁。
- 如果锁当前被其他线程持有,那么这个刚被唤醒的线程将会进入该对象的入口集(entry set),在那里等待直到锁变得可用。
- 等待锁
- 在入口集中的线程会竞争锁。一旦锁变得可用,其中一个线程(由 JVM 的调度策略决定哪一个)将获得锁并继续执行。
- 重新获取锁后的执行
- 当被唤醒的线程获取到锁后,它会从 wait() 方法调用之后的地方继续执行,而不是从同步块的开始执行。
因此,可以说当线程被 notify() 或 notifyAll() 唤醒时,它们会从等待集移动到入口集,直到它们能够重新获得锁。一旦获得锁,它们就会继续执行。