一.多线程
1.什么是多线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程的实际运作单位
简单理解多线程就是应用软件中相互独立,可以同时运行的功能(也可以理解为人体内相互独立,但可以同时运行的器官⌓‿⌓)
我们平时常用的Main方法,就是主线程
2.多线程的作用
单线程运行时,比如我们要创建一个变量,程序是需要等待时间的。而使用多线程,程序可以在多个线程之间来回运行,充分利用等待的时间,从而提高CPU的利用效率
二.多线程的三种实现方式
在Java API中对于Thread类的描述中给出了多线程的两种启动方式
⒈Thread类
线程是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程
下面我们就来结合多线程的启动方式来了解Thread类
⒉多线程的第一种启动方式
将类声明为Thread的子类
该子类应重写Thread类的run方法
接下来可以分配并启动该子类的实例
如图:我们创建两条线程并启用
⑴此方法中的Thread类
①构造方法
Ⅰ.Thread()
Ⅱ. Thread(String name)
其中的参数传递的就是线程的名字,默认为Thread-序号
②常用方法
Ⅰ start
void start() 使该线程开始执行
start方法是Thread类的基础方法,有了它线程才能够启动执行,而它又不像流那样需要close关闭,当线程结束后它会自动关闭
Ⅱ getName
String getName() 返回该线程的名称
当线程我们没有手动命名,getName默认返回的是Thread-序号
Ⅲ sleep
在多条线程执行时,当一条线程抢占到CPU执行权,它的执行时间是不确定的,那么它就可能一直占有着,当执行完毕后才能轮到下一个线程
比如上图,当一条线程执行完毕后才能轮到下一个线程
那我们想要线程轮流执行,那么就可以使用sleep让线程睡一会
如图:当我们执行完打印语句后,就让线程睡1毫秒,将执行权让给另一条线程
如图:看运行结果,两条线程就差不多是交替执行,而不是一条线程执行到底
Ⅳ setPriority
void setPriority(int newPriority) 设置线程的优先级
void getPriority() 获取线程的优先级
在Java中多条线程的执行是随机的,线程的优先级分为10个等级(1--10),优先级高的获取到CPU执行权的概率就越高,Java默认优先级为5
如图:优先级高的抢占到执行权的概率越大,而不是一定是它先执行完,这是概率问题
Ⅴ setDaemon
void setDaemong(boolean on) 将该线程设置为守护线程
守护线程就是当其他非守护线程执行完毕后,守护线程就没有存在的必要了,就会陆续结束,该守护线程可以不执行完结束
如图:我们将线程1设置为守护线程,线程2为非守护线程,当线程2执行完毕时,线程2会陆续结束,可以不会执行完
举个例子,当我们在扣扣的聊天窗口传输数据,聊天窗口就是非守护线程,而传输数据的窗口就是守护线程,当我们把聊天窗口关闭时,数据传输窗口也会陆续关闭
Ⅵ yield
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
yield方法可以出让当前的CPU执行权,但出让之后该线程仍然可能抢占到CPU执行权
因此除了sleep方法,该方法也可以让线程的执行尽可能的均匀些
Ⅶ join
join方法设置的线程,当该线程抢占到CPU执行权之后,在规定的等待时间内就会一直执行完毕后,才会让出CPU的执行权
⒊多线程的第二种启用方式
将类声明实现Runable接口
然后实现run方法
最后创建子类对象并传递给Thread对象
如图:我们创建子类对象并传递给线程执行
⑴此方法中的Thread类
①构造方法
方法传递Runable接口的实现类对象
又因为Runable接口是函数式接口,因此我们可以使用匿名内部类的方式实现
②常用方法
Ⅰ currentThread
static void currentThread() 获取当前线程对象
Ⅱ getName
Runable方法启动线程获取线程名字与Thread方法获取不同
在Runable实现类中我们实现的是Runable接口,该类与Thread是没有关系的,那么我们就不能像第一种启用方式那样直接getName获取到线程的名字了
那怎么解决呢?
我们可以利用currentThread方法获取到当前执行线程的对象,然后再调用getName方法获取到当前线程的名字
4.多线程的第三种启用方式
在前面的两种启用方式中,run方法是没有返回值的
因此我们就需要一种有返回值的启用多线程的方法
FutureTask类可以的get方法可以获取到线程方法中的结果,且FutureTast实现了Runable接口,可以在第二种方式的基础上启用线程
而其中构造方法可以传递一个Callable接口的实现类对象
而Callable接口只有一个方法call可以返回线程计算的结果
因此第三种启用方式就是在第二种方式的基础上稍加修改
首先创建Callable实现类并重写call方法
然后创建FutureTesk对象接受Callable实现类运行的结果
最后创建Thread类对象并启动
5.三种线程启用方式的对比
对于第一种启用方式,操作简单,可以直接使用Thread类中的方法。但是正因为它继承了Thread类,它就不能再继承其他类了,因此它的扩展性较差
对于第二,三种启用方式,它没有继承任何类,因此它的扩展性就强些。但是它的编程相对的就复杂些,不能直接使用Thread类中的方法
第一,二种启用方式无法获取到方法返回值,因此就有了第三种方式可以获取到方法返回值
三.线程的安全问题
在多线程的执行中,若我们有一个共享的变量size在随着线程的执行变化着,因为线程的执行是随时随机抢占的,那么size就会有线程安全问题
如图所示例子:
那么为了维护线程操作共享数据运行时的安全性问题,我们可以将这共享数据的代码用锁锁起来,当线程进入锁后,其他线程在外等候,当锁里面的代码执行完毕后,其他线程才能抢夺执行
1.同步代码块
格式:
synchronized(锁){操作的共享数据}
这里面的锁对象一定要是唯一的,只有相同的一把锁我们才可以让多条线程开锁解锁
这把锁可以是任意类型的共享对象
如:static Object o=new Object;
但是通常我们会使用本类的字节码文件
类名.class
如图:我们利用本类的字节码文件作为唯一的锁对象
2.同步方法
同步方法就是把synchronized关键字加到方法上,表示把这一个方法的所有代码给锁起来
格式:
修饰符 synchronized 返回值类型 方法名(方法参数){}
同步方法的锁对象我们不能自己指定,Java给我们指定好了锁对象
当是非静态方法时,锁对象是this,表示方法调用者
当是静态方式时,所对象是当前类的字节码文件
如图:在之前我们学习的StringBuffer中,我们同用可以看到同步方法的身影,这表示StringBuffer是线程安全的
3. Lock锁
synchronized操作简单但是我们无法进行更多关于锁的操作,而Lock相比于synchronized可以进行更广泛的锁定操作,允许更灵活的结构,可以支持多个相关的Condition对象
⑴Lock类
如图: Lock类是一个接口,不能直接实例化,我们常用它的实现类ReentrantLock(可重入锁)来实例化
①成员方法
Ⅰ lock和unlock
lock开锁, unlock解锁,这两个方法是最基本的锁,同样的锁对象必须是唯一的
利用lock锁有一个小细节
如图:若我们直接lock与unlock,就会遇到下面的问题,有线程拿着钥匙跑了,其他线程结束不了!!!
因此我们需要一个解决办法,无论线程怎样执行,unlock必须执行。那么我们就可以使用try...finally来包裹unlock,让锁必须释放
Ⅱ newCondition
lock锁通常会与Condition类结合使用来进行一些对于锁的操作
比如在阻塞队列中的使用
https://blog.csdn.net/m0_74808313/article/details/132196171
4.死锁问题
当锁嵌套时,通常会遇到死锁问题
如图:当线程1拿到了A锁等待B锁,而同时线程2拿到了B锁等待A锁,这时就导致了死锁
因此为了防止死锁问题,我们好尽量减少锁的嵌套
四.多线程协作
多线程协作就是线程之间相互配合,共同完成某项工作
比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者是消费者
1.生产者和消费者
生产者和消费者模式是一个十分经典的多线程协作的模式,又叫做等待唤醒机制,打破了线程的随机机制,让多个线程轮流执行
所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
2.等待唤醒机制的实现
⑴仓库,生产者和消费者的逻辑
首先我们需要一个仓库,当生产者生产物件后放入仓库,消费者消费物件拿出仓库
这个仓库需要有一把锁
当生产者进入时,若发现仓库中有物件,那么它就会沉睡等待,若发现仓库中没有物件,那么它就会生产物件并叫醒消费者
当消费者进入时,若发现仓库中没有物件,那么它就会沉睡等待;若发现仓库中有物件,那么它就会消费物件并叫醒生产者
⑵wait和notify
在Java的Object类中提供了相对应的方法来帮助我们解决线程的协作问题
注: wait和notify必须使用在同步方法或同步代码块内
⑶阻塞队列实现
首先我们需要一个阻塞队列,这个阻塞队列就代表仓库
然后我们完成生产者与消费者的逻辑
因为阻塞队列的put与take方法就是生产者与消费者的逻辑,因此我们在写生产者与消费者时就直接put,take,不用再进行逻辑的实现
如图:我们写完生产者与消费者的逻辑,传递阻塞队列查看
如图:查看打印语句发现生产者与消费者是轮流执行的,这样就实现了等待唤醒机制的逻辑
细节:因为锁是在put与take方法内部的,而打印语句在锁的外面,但并不影响共享数据的执行,只是不便于我们查看
五.线程状态
在给定的时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所以操作系统线程状态
六.线程池
在以前我们写的多线程有弊端
当我们需要线程时就创建(NEW),当它运行完后就消失(TERMINAED),这样的话浪费操作系统的资源
因此我们需要线程池来优化
1.线程池核心原理
我们需要一个容器,当我们提交线程任务时,容器会创建新的线程对象,任务执行完毕,线程存入到容器,到下次直接拿出使用
若提交任务时容器中没有空闲线程且容器满了,那么其他线程排队等待
2.线程池实现
static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
如图:我们创建一个大小为3的线程池,将前三个任务提交上去
可以看到当前排队的线程为0,工作中的线程为3,而当我们要提交下面的任务时,当前面的线程没结束它就会排队等待,只有当前面的线程运行完毕他们才能工作
3.自定义线程池
当我们查看newFixedThreadPool时,可以看到它的底层是创建了一个ThreadPoolExecutor对象
ThreadPoolExecutor才是真正的线程池对象,它相比于前面的线程池来说更加灵活
⑴ThreadPoolExecutor
ThreadPoolExecutor(int corePoolSize , int maximumPoolSize , long keepAliveTime , TimeUnit unit , BlockingQueue<Runnable> workQueue , ThreadFactory threadFactory , RejectedExecutionHandler handler) 用给定的初始参数创建线程池
其中共有7个参数
参数一:核心线程数
参数二:最大线程数量
参数三:等待的空闲时间
参数四:时间单位
参数五:任务队列
参数六:创建线程工厂
参数七:要执行的任务过多时的解决方案
其中当我们提交的线程多于核心线程,多出的线程会等待
若线程数量超出最大线程数,那么会创建临时线程(最大线程数-核心线程数),让多出的线程工作
若线程数量超出最大线程数+临时线程,那么会对超出的线程进行处理
其中的处理方式有四种,分别被定义为内部类
⑵线程池实现
线程池的大小并不是我们随意规定的,而是需要通过公式计算出来的
①CPU密集型运算
当我们的项目中计算多而读取文件少,就要此方式类计算
最大并行数+1
最大并行数与我们电脑CPU的型号相关,因为操作系统不会把所有的线程给同一个软件,因此我们通常利用Java虚拟机来计算最大并行数
②I/O密集型运算
当我们的项目计算少,读取数据多,那么就使用此类方式计算
最大并行数×期望CPU利用率×(总时间/CPU计算时间)
③代码书写