多线程
1. 1 并发与并行
- 并发:指两个或多个事件在同一时间段内发生。
- 并行:指的是两个或多个事件在同一时刻发生(同时发生)。
1.2 线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序
线程的调度
- 分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。 - 抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度
1.3创建线程类
Java中使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建线程对象
- 调用线程对象的start()方法来启动该线程
实现步骤
- 创建一个Thread类的子类
- 在Thread类的子类中重写Thread类中的run方法,设置线程任务
- 创建Thread类的子类对象
- 调用Thread类中的方法start方法,开启新的线程,执行run方法
void start() 使该线程开始执行。Java虚拟机调用该线程的run方法。
结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
java程序属于抢占式调度,哪个线程的优先级别高,哪个线程优先执行;同一个优先级,随机选择一个执行
Thread类
有关线程的一些方法;
- public Thread():分配一个新的线程对象
- public Thread (String name):分配一个指定的名字的新的线程对象
- public Thread(Runnable target):分配一个带有指定目标新的线程对象。
- public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字
常用方法:
- public String getName(): 获取当前线程名称
- public void start(): 导致此线程开始执行;Java虚拟机调用此线程的run方法
- public void run():此线程要执行的任务在此处定义代码。
- public static void sleep(long mills):使当前正在执行的线程以指定的毫秒数暂停
- public static Thread currentThread()返回对当前正在执行的线程对象的引用。
1.4 创建线程方式二
步骤:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象。该Thread对象才是真正的线程对象
- 调用线程对象的start()方法来启动线程
实现步骤:
- 创建一个Runnable接口的实现类
- 在实现类中重写Runnable接口的run 方法,设置线程任务
- 创建一个Runnable接口的实现类对象
- 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
- 调用Thread类中的start方法,开启新的线程执行run方法
1.5 Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源
- 可以避免java中的单继承的局限性
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
ps: 在java中,每次程序运行至少启动2个线程,一个是main线程,一个垃圾回收的线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM就是一个进程。
线程安全原理图
1.6 线程的同步技术
有以下三种实现方式
- 同步代码块
- 同步方法
- 锁机制
1.6.1 同步代码块
解决线程安全问题的一种方案:使用同步代码块
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
PS:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
- 锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
同步技术的原理图
1.6.2 同步方法
使用步骤:
- 把访问了共享数据的代码抽取出来,放到一个方法中
- 在方法上添加synchronized修饰符
修饰符 synchronized 返回值 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
ps:同步方法也会把方法内部的代码锁住
只让一个线程执行
同步方法的锁对象就是实现类对象 new RunnableImpl也就是this
1.6.3 Lock锁对象
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock()获取锁
void unlock()释放锁
使用步骤:
- 成员位置创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口中的方法Lock获取锁
- 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
1.7 线程的状态
1.7.1线程状态概述
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中有以下六种状态
线程状态 | 导致状态发生的条件 |
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep、Object.wait |
Terminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡, |
线程状态图
Timed Waiting线程状态图
锁阻塞状态图
无限等待状态图
无限等待的状态的,等待和唤醒需用公用同一个锁对象,才能实现。
进入到TimeWaiting(计时等待)有两种方式
- 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
- 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
唤醒的方法:
- void notify() 唤醒在此对象监视器上等待的单个线程。
- void notifyAll()唤醒在此对象监视器上等待的所有线程
1.8等待唤醒机制
1.8.1 线程间的通信
概念:
多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
为什么要处理线程间的通信:
多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同是,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即–等待唤醒机制。
1.8.2 等待唤醒机制
什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但线程间也会有协作机制。
等待唤醒机制就是一个线程在进行了规定操作后,就进入等待状态wait(),等待其他线程执行完他们的指定代码过后再将其唤醒notify()在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程wait/notify就是线程间的一种协作机制。
等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
- wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这是的线程状态即是WAITING。它还要等着别的线程执行一个特别的动作,即是通知(notify)在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的wait set 中的一个线程释放;等候时间最长的,优先释放。
- notifyAll:则释放所通知对象的wait set 上的全部线程
ps:哪怕只通知了一个等待的线程,被通知的线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行
总结如下:
- 如果能获取锁,线程就从WAITING状态变成RUNNABLE 状态;
- 否则,从wait set 出来,又进入entry set,线程就从WAITING状态又变成BLOCKED状态
调用wait和notify方法需要注意的细节
- wait方法的notify方法必须要由同一个锁对象调用,因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
1.9 线程池
1.9.1 线程池思想概述
我们使用线程的时候就去创建一个线程,这样实现起来非常方便,但是会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间
而线程池就是一种可以复用线程的方法,就是执行完一个任务之后,并不销毁,可以继续执行其他的任务。
底层原理图
1.9.2 线程池概念
- 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源。
合理利用线程池能够带来的好处:
- 降低资源消耗。减少创建和销毁线程的此时,每个工作线程都可以被重复利用,可执行多个任务
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而使服务器宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存就越大,最后死机)
1.9.3线程池的使用
在java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executor类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
参数:
int nThreads:创建线程池中包含的线程数量
返回值:
ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收
java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task) 提交一个Runnable任务用于执行
关闭/销毁线程池的方法
void shutdown()
线程池的使用步骤:
- 使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
- 创建一个类,实现Runnable接口,重写run方法,设置线程任务
- 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
- 调用ExecutorService中的方法shutdown销毁线程池