每天学一点之多线程
创始人
2024-05-30 03:22:04
0

多线程

一、相关概念

并发与并行

并行(parallel):指多个事件任务在同一时刻发生(同时发生)。
并发(concurrency):指两个或多个事件在同一个微小的时间段内发生。程序并发执行可以在有限条件下,充分利用CPU资源。
在这里插入图片描述
单核CPU:只能并发
多核CPU:并行+并发

线程与进程

  • 程序:为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。

  • 软件:1个或多个应用程序+相关的素材和资源文件等构成一个软件系统。

  • 进程是对一个程序运行过程(创建-运行-消亡)的描述,系统会为每个运行的程序建立一个进程,并为进程分配独立的系统资源,比如内存空间等资源。

  • 线程:线程是进程中的一个执行单元,负责完成执行当前程序的任务,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这时这个应用程序也可以称之为多线程程序。多线程使得程序可以并发执行,充分利用CPU资源。

    面试题:进程是操作系统调度和分配资源的最小单位,线程是CPU调度的最小单位。不同的进程之间是不共享内存的。进程之间的数据交换和通信的成本是很高。不同的线程是共享同一个进程的内存的。当然不同的线程也有自己独立的内存空间。对于方法区,堆中中的同一个对象的内存,线程之间是可以共享的,但是栈的局部变量永远是独立的。

多线程的优点与应用场景

  • 主要优点:
    • 充分利用CPU空闲时间片,用尽可能短的时间完成用户的请求。也就是使程序的响应速度更快 。
  • 应用场景:
    • 多任务处理。多个用户请求服务器,服务端程序可以开启多个线程分别处理每个用户的请求,互不影响。
    • 单个大任务处理。下载一个大文件,可以开启多个线程一起下载,减少整体下载时间。

线程调度

指CPU资源如何分配给不同的线程。常见的两种线程调度方式:

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java采用的是抢占式调度方式

二、线程的创建与启动

继承Thread类

通过继承Thread类来创建并启动多线程的步骤:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

多线程执行情况分析
在这里插入图片描述
注意事项:

  • 手动调用run方法不是启动线程的方式,只是普通方法调用。

  • start方法启动线程后,run方法会由JVM调用执行。

  • 不要重复启动同一个线程,否则抛出异常IllegalThreadStateException

  • 不要使用Junit单元测试多线程,不支持,主线程结束后会调用System.exit()直接退出JVM;

实现Runnable接口

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

两种创建线程方式比较

  • Thread类本身也是实现了Runnable接口的,run方法都来自Runnable接口,run方法也是真正要执行的线程任务。

    public class Thread implements Runnable {}
    
  • 因为Java类是单继承的,所以继承Thread的方式有单继承的局限性,但是使用上更简单一些。

  • 实现Runnable接口的方式,避免了单继承的局限性,并且可以使多个线程对象共享一个Runnable实现类(线程任务类)对象,从而方便在多线程任务执行时共享数据。

匿名内部类对象创建线程

匿名内部类对象的方式创建线程,并不是一种新的创建线程的方式,只是在线程任务只需执行一次的情况下,我们无需单独创建线程类,可以采用匿名对象的方式:

new Thread("新的线程!"){@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName()+":正在执行!"+i);}}
}.start();new Thread(new Runnable(){@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName()+":" + i);}}
}).start();

三、Thread类

构造方法

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

线程使用基础方法

  • public void run() :此线程要执行的任务在此处定义代码。

  • public String getName() :获取当前线程名称。

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

  • public final int getPriority() :返回线程优先级

  • public final void setPriority(int newPriority) :改变线程的优先级

    • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
    • MAX_PRIORITY(10):最高优先级
    • MIN _PRIORITY (1):最低优先级
    • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

线程控制常见方法

  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

  • public static void sleep(long millis) :线程睡眠,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  • public static void yield():线程礼让,yield只是让当前线程暂时失去执行权,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

  • void join() :加入线程,当前线程中加入一个新线程,等待加入的线程终止后再继续执行当前线程。

    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。

    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

  • public final void stop():强迫线程停止执行。 该方法具有不安全性,已被弃用,最好不要使用。

    • 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
    • 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
  • public void interrupt():中断线程,实际上是给线程打上一个中断的标记,并不会真正使线程停止执行。

  • public static boolean interrupted():检查线程的中断状态,调用此方法会清除中断状态(标记)。

  • public boolean isInterrupted():检查线程中断状态,不会清除中断状态(标记)

  • public void setDaemon(boolean on):将线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。

    • 守护线程,主要为其他线程服务,当程序中没有非守护线程执行时,守护线程也将终止执行。JVM垃圾回收器也是守护线程。
  • public boolean isDaemon():检查当前线程是否为守护线程。

  • volatile的作用是确保不会因编译器的优化而省略某些指令,volatile的变量是说这变量可能会被意想不到地改变,每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份,这样,编译器就不会去假设这个变量的值了。

线程生命周期

传统线程模型的五种线程状态
在这里插入图片描述

JDK定义的六种线程状态
java.lang.Thread类内部定义了一个枚举类用来描述线程的六种状态:

    public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;}

在这里插入图片描述
说明:当从WAITINGTIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。

线程安全

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,但是如果多个线程中对资源有读和写的操作,就会出现前后数据不一致问题,这就是线程安全问题。

线程安全问题引出

  • 局部变量不能共享:局部变量是每次调用方法都是独立的,那么每个线程的run()的数据是独立的,不是共享数据。
  • 不同对象的实例变量不共享 :不同的实例对象的实例变量是独立的。
  • 静态变量是共享的
  • 同一个对象的实例变量共享

总结:线程安全问题的出现因为具备了以下条件

  1. 多线程执行
  2. 共享数据
  3. 多条语句操作共享数据

线程安全问题解决方式

在这里插入图片描述
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

public synchronized void method(){可能会产生线程安全问题的代码
}

同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。

synchronized(同步锁){需要同步操作的代码
}

锁对象选择

首选this其次是类.class也可以是" "空字符串对象

同步锁对象:

  • 锁对象可以是任意类型。
  • 多个线程对象 要使用同一把锁。

同步代码块的锁对象

  • 静态代码块中:使用当前类的Class对象
  • 非静代码块中:习惯上先考虑this,但是要注意是否同一个this

锁的范围太小:不能解决安全问题,要同步所有操作共享资源的语句。

锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。

单例设计模式的线程安全问题

1、饿汉式没有线程安全问题

饿汉式:上来就创建对象

2、懒汉式线程安全问题

public class Singleton {private static Singleton ourInstance;public static Singleton getInstance() {//一旦创建了对象,之后再次获取对象,都不会再进入同步代码块,提升效率if (ourInstance == null) {//同步锁,锁住判断语句与创建对象并赋值的语句synchronized (Singleton.class) {if (ourInstance == null) {ourInstance = new Singleton();}}}return ourInstance;}private Singleton() {}
}

等待唤醒机制

在一个线程满足某个条件时,就进入等待状态(wait()/wait(time)), 等待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定wait的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协作机制。

  1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING或TIMED_WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”或者等待时间到,在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
  2. notify:则选取所通知对象的 wait set 中的一个线程释放;
  3. notifyAll:则释放所通知对象的 wait set 上的全部线程。

注意:
被通知线程被唤醒后也不一定能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下:

  • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;
  • 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用,并且必须要通过锁对象调用这2个方法。

释放锁操作与死锁

1、释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。

  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。

  • 当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

2、死锁

不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

3、面试题:sleep()和wait()方法的区别

(1)sleep()不释放锁,wait()释放锁

(2)sleep()指定休眠的时间,wait()可以指定时间也可以无限等待直到notify或notifyAll

(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明

因为我们调用wait()方法是由锁对象调用,而锁对象的类型是任意类型的对象。那么希望任意类型的对象都要有的方法,只能声明在Object类中。

练习

要求两个线程,同时打印字母,每个线程都能连续打印3个字母。两个线程交替打印,一个线程打印字母的小写形式,一个线程打印字母的大写形式,但是字母是连续的。当字母循环到z之后,回到a。

在这里插入图片描述

public class PrintLetterDemo {public static void main(String[] args) {// 2、创建资源对象PrintLetter p = new PrintLetter();// 3、创建两个线程打印new Thread("小写字母") {public void run() {while (true) {p.printLower();try {Thread.sleep(1000);// 控制节奏} catch (InterruptedException e) {e.printStackTrace();}}}}.start();new Thread("大写字母") {public void run() {while (true) {p.printUpper();try {Thread.sleep(1000);// 控制节奏} catch (InterruptedException e) {e.printStackTrace();}}}}.start();}
}// 1、定义资源类
class PrintLetter {private char letter = 'a';public synchronized void printLower() {for (int i = 1; i <= 3; i++) {System.out.println(Thread.currentThread().getName() + "->" + letter);letter++;if (letter > 'z') {letter = 'a';}}this.notify();try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}public synchronized void printUpper() {for (int i = 1; i <= 3; i++) {System.out.println(Thread.currentThread().getName() + "->" + (char) (letter - 32));letter++;if (letter > 'z') {letter = 'a';}}this.notify();try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}
} 

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...