Java——多线程

一.多线程

1.什么是多线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程的实际运作单位

简单理解多线程就是应用软件中相互独立,可以同时运行的功能(也可以理解为人体内相互独立,但可以同时运行的器官⌓‿⌓)

我们平时常用的Main方法,就是主线程

2.多线程的作用

单线程运行时,比如我们要创建一个变量,程序是需要等待时间的。而使用多线程,程序可以在多个线程之间来回运行,充分利用等待的时间,从而提高CPU的利用效率

8fbbe4c556f54fc2b81c772714c4b246.png

二.多线程的三种实现方式

在Java API中对于Thread类的描述中给出了多线程的两种启动方式

⒈Thread类

线程是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程

6bac42e8fcab42da94de354d34f65a8e.png

下面我们就来结合多线程的启动方式来了解Thread类

⒉多线程的第一种启动方式

将类声明为Thread的子类

该子类应重写Thread类的run方法

接下来可以分配并启动该子类的实例

f40dfae5e6e84a0ba6e621d5aa1bad3c.png

如图:我们创建两条线程并启用

66e6069a5bec43e2a5cde66eb6e5b51e.png

3d829dfe3fe24549bb15f753e46cf0e8.png

⑴此方法中的Thread类

①构造方法

Ⅰ.Thread()

Ⅱ. Thread(String name)

其中的参数传递的就是线程的名字,默认为Thread-序号

②常用方法

Ⅰ start

void start() 使该线程开始执行

start方法是Thread类的基础方法,有了它线程才能够启动执行,而它又不像流那样需要close关闭,当线程结束后它会自动关闭

Ⅱ getName

String getName() 返回该线程的名称

当线程我们没有手动命名,getName默认返回的是Thread-序号

Ⅲ sleep

20e30a243aa046d981217b33b552cf64.png

在多条线程执行时,当一条线程抢占到CPU执行权,它的执行时间是不确定的,那么它就可能一直占有着,当执行完毕后才能轮到下一个线程

04a653f4f5634a2582c700036068d055.png

比如上图,当一条线程执行完毕后才能轮到下一个线程

那我们想要线程轮流执行,那么就可以使用sleep让线程睡一会 

如图:当我们执行完打印语句后,就让线程睡1毫秒,将执行权让给另一条线程

7933a4c48f6142459f5b6d3c997eff1b.png

如图:看运行结果,两条线程就差不多是交替执行,而不是一条线程执行到底

6da30569c36245988b3a2e2c1916f814.png

Ⅳ  setPriority

void setPriority(int newPriority)  设置线程的优先级

void getPriority()  获取线程的优先级

在Java中多条线程的执行是随机的,线程的优先级分为10个等级(1--10),优先级高的获取到CPU执行权的概率就越高,Java默认优先级为5

如图:优先级高的抢占到执行权的概率越大,而不是一定是它先执行完,这是概率问题

827b803babf44429bf5e18c3d59b3684.png

Ⅴ  setDaemon

 void setDaemong(boolean on)  将该线程设置为守护线程

守护线程就是当其他非守护线程执行完毕后,守护线程就没有存在的必要了,就会陆续结束,该守护线程可以不执行完结束

如图:我们将线程1设置为守护线程,线程2为非守护线程,当线程2执行完毕时,线程2会陆续结束,可以不会执行完

fdf09fc78a2643be85045c4f0769a119.png

250e7b5bc7e343b6ad23d0336a614871.png 

举个例子,当我们在扣扣的聊天窗口传输数据,聊天窗口就是非守护线程,而传输数据的窗口就是守护线程,当我们把聊天窗口关闭时,数据传输窗口也会陆续关闭

Ⅵ  yield

static void yield()  暂停当前正在执行的线程对象,并执行其他线程

yield方法可以出让当前的CPU执行权,但出让之后该线程仍然可能抢占到CPU执行权

因此除了sleep方法,该方法也可以让线程的执行尽可能的均匀些

Ⅶ  join

f2b63552f4a1488f8f5b91e356be307f.png

join方法设置的线程,当该线程抢占到CPU执行权之后,在规定的等待时间内就会一直执行完毕后,才会让出CPU的执行权

⒊多线程的第二种启用方式

将类声明实现Runable接口

然后实现run方法

最后创建子类对象并传递给Thread对象

4decdde05b2c4a22aebcd606c93dab62.png

如图:我们创建子类对象并传递给线程执行

a810a944cf424e79927e6739d405d4cd.png

4698037a794a4ba186ccf0654a19b97c.png

⑴此方法中的Thread类

①构造方法

92750b45c4514c2dac4f1493afbf62a8.png

方法传递Runable接口的实现类对象 

又因为Runable接口是函数式接口,因此我们可以使用匿名内部类的方式实现

f0a8a9aef1b742a396ca635d27fca751.png

②常用方法

Ⅰ currentThread

static void currentThread()  获取当前线程对象

Ⅱ getName

Runable方法启动线程获取线程名字与Thread方法获取不同

在Runable实现类中我们实现的是Runable接口,该类与Thread是没有关系的,那么我们就不能像第一种启用方式那样直接getName获取到线程的名字了

那怎么解决呢?

我们可以利用currentThread方法获取到当前执行线程的对象,然后再调用getName方法获取到当前线程的名字

02b364d27d264ed59cd2a8fcf7b1d255.png

4.多线程的第三种启用方式

在前面的两种启用方式中,run方法是没有返回值的

因此我们就需要一种有返回值的启用多线程的方法

FutureTask类可以的get方法可以获取到线程方法中的结果,且FutureTast实现了Runable接口,可以在第二种方式的基础上启用线程

f5d69d3771ec4f6c9de394fb9eea7a76.png

而其中构造方法可以传递一个Callable接口的实现类对象

e1d75cc6e0734956a6d1d7e56b3139b4.png

而Callable接口只有一个方法call可以返回线程计算的结果

d0529f7729644a6b9d280ac5abb01419.png

因此第三种启用方式就是在第二种方式的基础上稍加修改

首先创建Callable实现类并重写call方法

然后创建FutureTesk对象接受Callable实现类运行的结果

最后创建Thread类对象并启动

dc17b68f954a49a68d5644e89bea05c9.png

ce295bd6ad95406a96c5dfed03c4d9e1.png 

5.三种线程启用方式的对比

对于第一种启用方式,操作简单,可以直接使用Thread类中的方法。但是正因为它继承了Thread类,它就不能再继承其他类了,因此它的扩展性较差

对于第二,三种启用方式,它没有继承任何类,因此它的扩展性就强些。但是它的编程相对的就复杂些,不能直接使用Thread类中的方法

第一,二种启用方式无法获取到方法返回值,因此就有了第三种方式可以获取到方法返回值

三.线程的安全问题

在多线程的执行中,若我们有一个共享的变量size在随着线程的执行变化着,因为线程的执行是随时随机抢占的,那么size就会有线程安全问题

如图所示例子:

8c2786d8231f4875a63ab56c543f930b.png

那么为了维护线程操作共享数据运行时的安全性问题,我们可以将这共享数据的代码用锁锁起来,当线程进入锁后,其他线程在外等候,当锁里面的代码执行完毕后,其他线程才能抢夺执行

1.同步代码块

格式:

synchronized(锁){操作的共享数据}

这里面的锁对象一定要是唯一的,只有相同的一把锁我们才可以让多条线程开锁解锁

这把锁可以是任意类型的共享对象

如:static Object o=new Object;

但是通常我们会使用本类的字节码文件

类名.class

如图:我们利用本类的字节码文件作为唯一的锁对象

e35f9b93837241ecaa07c29428a09151.png

2.同步方法

同步方法就是把synchronized关键字加到方法上,表示把这一个方法的所有代码给锁起来

格式:

修饰符 synchronized 返回值类型 方法名(方法参数){}

同步方法的锁对象我们不能自己指定,Java给我们指定好了锁对象

当是非静态方法时,锁对象是this,表示方法调用者

当是静态方式时,所对象是当前类的字节码文件

如图:在之前我们学习的StringBuffer中,我们同用可以看到同步方法的身影,这表示StringBuffer是线程安全的

611c00140ee8484c9d559364f08bed48.png

3. Lock锁

synchronized操作简单但是我们无法进行更多关于锁的操作,而Lock相比于synchronized可以进行更广泛的锁定操作,允许更灵活的结构,可以支持多个相关的Condition对象

⑴Lock类

如图: Lock类是一个接口,不能直接实例化,我们常用它的实现类ReentrantLock(可重入锁)来实例化

18ddadbc7b1f4c01be269b6861489f66.png

①成员方法

3f57a98e622946109a68ed3a3c49ac97.png

Ⅰ lock和unlock

lock开锁, unlock解锁,这两个方法是最基本的锁,同样的锁对象必须是唯一的

利用lock锁有一个小细节

如图:若我们直接lock与unlock,就会遇到下面的问题,有线程拿着钥匙跑了,其他线程结束不了!!!

1793469074e04509a2655ab32a7a03f3.png

因此我们需要一个解决办法,无论线程怎样执行,unlock必须执行。那么我们就可以使用try...finally来包裹unlock,让锁必须释放

19d4d7d5058e4b419d6de5ca898c9960.png

Ⅱ newCondition

lock锁通常会与Condition类结合使用来进行一些对于锁的操作

比如在阻塞队列中的使用

https://blog.csdn.net/m0_74808313/article/details/132196171

4.死锁问题

当锁嵌套时,通常会遇到死锁问题

如图:当线程1拿到了A锁等待B锁,而同时线程2拿到了B锁等待A锁,这时就导致了死锁

2cfa7efe2a1847489477efe8114fd5ad.png

因此为了防止死锁问题,我们好尽量减少锁的嵌套

四.多线程协作

多线程协作就是线程之间相互配合,共同完成某项工作

比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者是消费者

1.生产者和消费者

生产者和消费者模式是一个十分经典的多线程协作的模式,又叫做等待唤醒机制,打破了线程的随机机制,让多个线程轮流执行

所谓生产者消费者问题,实际上主要是包含了两类线程:

​ 一类是生产者线程用于生产数据

​ 一类是消费者线程用于消费数据

生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

2.等待唤醒机制的实现

⑴仓库,生产者和消费者的逻辑

首先我们需要一个仓库,当生产者生产物件后放入仓库,消费者消费物件拿出仓库

这个仓库需要有一把锁

当生产者进入时,若发现仓库中有物件,那么它就会沉睡等待,若发现仓库中没有物件,那么它就会生产物件并叫醒消费者

765cf1c6968640a4b80ae4ca7d65d170.png

当消费者进入时,若发现仓库中没有物件,那么它就会沉睡等待;若发现仓库中有物件,那么它就会消费物件并叫醒生产者

83c30897c80e4a1cb612d0b54ebcb3d4.png

⑵wait和notify

在Java的Object类中提供了相对应的方法来帮助我们解决线程的协作问题

d4833395a4a74d35903c82e865975ee0.png

d79d5bc661504b5da30f2f7b2d9851c1.png

注: wait和notify必须使用在同步方法或同步代码块内

⑶阻塞队列实现

首先我们需要一个阻塞队列,这个阻塞队列就代表仓库

然后我们完成生产者与消费者的逻辑

因为阻塞队列的put与take方法就是生产者与消费者的逻辑,因此我们在写生产者与消费者时就直接put,take,不用再进行逻辑的实现

af7e1868d9314b83a15b0e6bff146505.png

如图:我们写完生产者与消费者的逻辑,传递阻塞队列查看

37df77784e7440a094ef797112beae20.png

40e3152bb1424072943a435cd102f472.png

fae3ee76baa84c13b095ee98aad70d7a.png 

如图:查看打印语句发现生产者与消费者是轮流执行的,这样就实现了等待唤醒机制的逻辑

8f7769570f344cf7960da4dad72569e7.png

细节:因为锁是在put与take方法内部的,而打印语句在锁的外面,但并不影响共享数据的执行,只是不便于我们查看

五.线程状态

在给定的时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所以操作系统线程状态

73ca8c85b22d4bdbbf312adac17d8828.png

六.线程池

在以前我们写的多线程有弊端

当我们需要线程时就创建(NEW),当它运行完后就消失(TERMINAED),这样的话浪费操作系统的资源

因此我们需要线程池来优化

1.线程池核心原理

我们需要一个容器,当我们提交线程任务时,容器会创建新的线程对象,任务执行完毕,线程存入到容器,到下次直接拿出使用

若提交任务时容器中没有空闲线程且容器满了,那么其他线程排队等待

2.线程池实现

static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池

static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池

如图:我们创建一个大小为3的线程池,将前三个任务提交上去

6f7c491f282a4bf7af97f7f8e8e96454.png

可以看到当前排队的线程为0,工作中的线程为3,而当我们要提交下面的任务时,当前面的线程没结束它就会排队等待,只有当前面的线程运行完毕他们才能工作

73f8b0002b5c4970af5c1517a898f162.png

3.自定义线程池

当我们查看newFixedThreadPool时,可以看到它的底层是创建了一个ThreadPoolExecutor对象

b919557680d9410ba9b9797ec938adc8.png

ThreadPoolExecutor才是真正的线程池对象,它相比于前面的线程池来说更加灵活

⑴ThreadPoolExecutor

ThreadPoolExecutor(int corePoolSize ,  int maximumPoolSize ,  long keepAliveTime ,  TimeUnit unit , BlockingQueue<Runnable> workQueue ,  ThreadFactory threadFactory ,  RejectedExecutionHandler handler)  用给定的初始参数创建线程池

其中共有7个参数

参数一:核心线程数

参数二:最大线程数量

参数三:等待的空闲时间

参数四:时间单位

参数五:任务队列

参数六:创建线程工厂

参数七:要执行的任务过多时的解决方案

其中当我们提交的线程多于核心线程,多出的线程会等待

若线程数量超出最大线程数,那么会创建临时线程(最大线程数-核心线程数),让多出的线程工作

若线程数量超出最大线程数+临时线程,那么会对超出的线程进行处理

6afe4488384c49e0b8b4ca54a38d6c78.png

其中的处理方式有四种,分别被定义为内部类

311b241d720b434ea1a5dc9ab1a13212.png

⑵线程池实现

线程池的大小并不是我们随意规定的,而是需要通过公式计算出来的

①CPU密集型运算

当我们的项目中计算多而读取文件少,就要此方式类计算

最大并行数+1

最大并行数与我们电脑CPU的型号相关,因为操作系统不会把所有的线程给同一个软件,因此我们通常利用Java虚拟机来计算最大并行数

408ae63b49d6468f8c21190eaaa12702.png

②I/O密集型运算

当我们的项目计算少,读取数据多,那么就使用此类方式计算

最大并行数×期望CPU利用率×(总时间/CPU计算时间)

③代码书写

90b3cf11352d4889a4da588327d801fd.png

 

 

 

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汤姆大聪明

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值