多线程(Java)

注:本文为本人学习过程中的笔记

1.导入

1.进程和线程

我们希望我们的程序可以并发执行以提升效率,此时引入了多进程编程。可是创建进程等操作开销太大,于是就将进程进一步拆分成线程,减少开销。进程与进程之间所涉及到的资源是相互独立的,不会相互干扰。至于线程之间具体是怎么调度的,我们很难知道,这主要是操作系统随机调度。

进程是操作系统资源分配的基本单位

线程是操作系统调度执行的基本单位

进程是存在父子关系的,而线程不存在

2.多线程代码的简单写法

1.创建线程

1.重写Thread中的run方法

public class Test{public static void main(String[] args){Thread t1 = new MyThread;}
}
public class MyThread extends Thread{public void run() {....}
}

2.重写Runnable接口中的run方法 

public class Test implements Runnable{public static void main(String[] args){Runnable myRunnable = new MyRunnable();Thread t1 = new Thread(myRunnable);}
}
public class MyRunnable{public void run(){...}
}

3.使用lamda表达式 

public class Test{public static void main(String[] args){Thread t1 = new Thread(() -> {public void run(){...} });}
}

2.一些方法

new Thread()

a.Thread()        直接创建线程对象

b.Thread(Runnable target)        使用Runnable对象创建线程对象

c.Thread(String name)        创建线程对象并命名

d.Thread(Runnable target, String name)        使用Runnable对象创建线程对象并命名

run()

run方法是线程的入口方法,进入线程自动就会调用,不需要我们调用

start()

这是启动线程的方法

sleep() 

这个方法要通过Thread类来调用,效果是使当前线程休眠一定时间。在括号里可以设置休眠的时间,单位是毫秒。

Thread.sleep(1000);

使用这个方法时会抛出InterruptedException异常 

因为线程的调度是不可控的,所以这个方法只能保证实际休眠时间大于等于参数设置的时间。代码调用sleep,相当于让当前线程让出cpu资源,后续时间到的时候就需要操作系统内核把这个线程重新调度到cpu上才能继续执行。

sleep(0)是一种特殊写法,意味着让当前线程立即放弃cpu资源等待操作系统重新调度

getId()

Java中会给每个运行的线程分配id,获取id

getName()

获取名字

getState

获取状态

getPriority

获取优先级

isDaemon()

判断是否是后台线程

Java中存在后台线程和前台线程,后台线程随着进程的开启而开启,关闭而关闭,不影响进程的状态,我们创建的线程和main线程是前台线程,可以通过setDaemon方法修改

setDaemon()

在括号里填写true或者false来设置线程是否是后台线程

isAive()

判断线程是否存活

Java代码中创建的Thread对象和系统中的线程是一一对应关系。但是,Thread对象的生命周期和系统中的生命周期是不同的,可能存在Thread对象还存货,但是系统中的线程已经销毁的情况

interrupt()

关闭线程

调用这个方法时,会修改isInterruptted方法内部的标志位将其设为true。如果我们在使用了sleep方法并且唤醒了该方法,那sleep方法就会把isInterruptted的标志位设置为false,这时sleep会抛出Interruptted异常,我们可以修改try-catch语句中的代码,达到我们想要的效果而不是直接关闭线程

isInterruptted()

判断线程是否关闭

Thread.currentThread()

这是一个静态方法,在哪个线程中调用就能获得哪个线程的应用

join()

join能够要求多个线程结束的先后顺序。比如在main线程中调用t.join就会使main线程等待t线程先结束。只要t线程不结束,主线程的join就会一直的等待下去。我们可以在括号里设置最大等待时间,当到达时间join就不会再等待,继续执行下面的代码

wait() / notify()

这两个方法是用来协调线程之间的执行逻辑的顺序。虽然我们不能干预调度器的调度顺序,但是我们可以让后执行的线程进行等待,等到先执行的线程执行完了,通知当前线程,继续执行。

在Java标准库中,每个产生阻塞的方法都会抛出InterrupttedException异常,会被interrupt方法唤醒,wait也是一样

wait和join的区别

join也是等,当join是等另一个线程彻底执行完才继续执行

wait是等另一个线程执行到notify才继续走,不需要等另一个线程执行完

应用场景

当多个线程竞争一把锁的时候,获取锁的线程如果释放了,其他哪个线程能拿到这把锁是不确定,我们不能控制操作系统怎么调度,当我们可以使用wait和notify语句来控制这个顺序

使用方法

使用wait时,线程会释放锁,所以我们要使用wait时线程必须获取锁,否则会报错

wait的等待最关键的一点就是先释放锁,给其他线程获取锁的机会,并且阻塞等待。如果其他线程做完了必要的工作,调用notify唤醒这个wait线程,wait就会解除阻塞,重新获取到锁,然后继续执行

synchronized (locker) {locker.wait();
}synchronized(locker) {locker.notify();
}

这其中的锁对象必须时同一个锁对象才能产生效果,如果多个线程在同一个锁对象上wait,进行notify的时候是随机唤醒其中一个线程,一次notify唤醒一个wait。wait也可以在括号填写超时时间,不死等。

wait和sleep的区别

wait有等待时间,可以用notify唤醒。sleep也有等待时间,可以使用interrupt提前唤醒

wait必须要搭配锁使用,先加锁,才能用wait,sleep不需要

如果都是在synchronized内部使用,wait会释放锁,而sleep不会释放锁

notifyAll()

使用这个方法可以一次唤醒所有相关的wait

3.小工具

1.jconsole

这个小工具在jdk的bin目录下,使用这个工具可以连接java程序,从而观察线程的信息

3.线程状态

NEW

安排了工作,还未开始行动

也就是new了Thread对象,还没start

TERMINATED

工作完成了

内核中的线程已经结束了但是Thread对象还在

RUNNABLE

可工作的,又可以分成正在工作中和即将开始工作

a.线程正在cpu上执行

b.线程随时可以去cpu上执行

TIMED_WAITTING

表示排队等着其他事情,有超时时间

当我们调用join方法,线程就会进入这个状态。

WAITING

表示排队等待其他事情,没有超时时间,死等

BLOCKED

表示排队等待其他事情

这个比较特殊,是由于锁导致的阻塞

总图

4.线程安全

一段代码,如果再多线程并发执行的情况下,出现bug,就称为线程不安全

1.线程安全问题产生的原因

Java中的一行代码对应着cpu上的多条指令,并不是原子的,所以当cpu随机调度的时候就可能出现一些问题,也就会产生线程安全问题,以下是产生线程安全问题的原因

a.(根本原因)操作系统对于线程的调度是随机的,抢占式执行

b.多个线程同时修改一个变量

c.修改操作不是原子的

d.内存可见性,jvm会优化代码

e.指令重排序,jvm会优化代码

2.如何解决线程安全问题

a.针对问题a我们是无法解决的,因为抢占式执行是操作系统的底层设定

b.针对问题b,这个问题和代码结构相关,我们可以调整代码结构,规避一些线程不安全的代码。但是这样的方案是不够通用的,有些情况下,需求上就是需要多线程同时修改一个变量

c.针对问题c,我们可以使用加锁操作,这也是Java中解决线程安全问题最主要的方案,通过加锁操作我们可以将不是原子的操作打包成原子的操作。

d.使用volatile关键字

jvm对于我们的代码会自动的进行一些优化,比如说如果我们写出这样的一个语句

public class Test{int count = 0;public static void main(String[] args) throws InterrupttedException{Thread t1 = new Thread(() -> {while(count == 0) {}});t1.start();Thread.sleep(30000);count = 1;}
}

当我们启动这个代码时,线程t1中的循环会被一直执行下去,这是为什么呢?明明我们在主线程中都修改了count的值

因为这个时候jvm优化了我们的代码,在t1线程中,while语句不断地从内存中读取count的值,这个操作的开销是比较大的,此时jvm会将count的值保留在cpu寄存器中,直接读取寄存器的count值,这就导致了我们修改了count的值而不会被读取到

此时就可以使用volatile关键字修饰count这个变量让jvm不优化我们的代码

注意:在Java的官方文档中这样写道,每个线程,有一个自己的“工作内存”,同时这些线程共享一个“主内存”。当一个线程循环进行上述读取变量操作的时候,就会把主内存中的数据,拷贝到......

这里提到的“工作内存”就是我们所说的cpu寄存器,cpu上还有缓存,因为Java是跨平台的语言,设计者不希望程序员有学习硬件知识的成本,所以将其抽象为“工作内存的概念”。

e.使用volatile关键字

volatile关键字不仅可以解决内存可见性问题,还可以不让jvm进行指令重排序。

3.锁

加锁/解锁本身是系统提供的api,很多编程语言都对这样的api进行了封装,大多数的封装风格都是采取lock和unlock两个函数,Java中采用的是synchronized关键字

synchronized

synchronized(锁对象) {//进入代码块相当于加锁...
}//离开代码块相当于解锁

括号里需要我们填写加锁的对象,这个锁的类型不重要,重要的是是否有几个线程尝试针对同一个锁对象进行加锁。只有两个线程针对同一个锁对象加锁,才能产生互斥效果。即一个线程获取锁之后,另一个线程为了也能获取锁只能阻塞等待第一个线程中的锁释放出来 

synchronized的变种写法

synchronized可以对方法进行加锁

当要加锁的方法是被static修饰时,synchronized修饰static方法就相当于针对类对象进行加锁

死锁

产生死锁的原因
1.互斥

当两个线程争夺同一个锁时会产生互斥,这是锁的基本特性

2.不可剥夺

当一个线程获得锁之后,这个锁是不能被抢走的,只能等待它释放出来,这也是锁的基本特性

3.请求和保持

当一个线程已经获取一把锁之后,继续请求其他的锁

4.循环等待

当一个线程已经获取一把锁之后,继续请求其他锁,且有另一个线程也在获得锁的情况下请求相同的锁,形成循环

解决死锁的办法

针对问题1和问题2是无法解决的,这是锁的基本特性

针对问题3

我们可以避免锁嵌套

针对问题4

可以约定加锁的顺序,使争夺锁的过程形不成循环

2.Java中线程安全的东西

String

系统api没有提供修改String的方法,导致String天然就是线程安全的

5.多线程代码案例

1.单例模式

单例模式是指在某个类,某个程序中只允许有唯一一个实例,不允许有多个实例,不允许new多次。

1.应用场景

比如我们有一个100G的数据库要加载到内存中方便读取,由于这个数据库数据非常多,创建等操作需要的开销都非常大,此时我们希望只创建一次即可,否则将会产生非常多的额外的开销也可能导致服务器的内存不够用。

2.两种写法

懒汉模式和饿汉模式是存在缺陷的,可以通过反射来创建实例,但是反射本身属于非常规的手段,一般编写代码的时候不使用

1.饿汉模式

饿是指尽早创建实例

1.写法
class SingleTon{private static SingleTon instance;//静态成员的初始化是在类加载的时候就触发的,往往程序一启动类就会加载public static SingleTon getInstance(){return instance;}//后续统一使用getInstance这个方法来获取实例private SingleTon{}//由于构造方法是私有的,所以在类外new instance都会编译失败
}
2.线程安全问题

由于饿汉模式中的instance在程序启动的时候就创建好了,所以我们后续的操作都只涉及到读操作,不会产生线程安全问题。 

2.懒汉模式

懒是指尽可能晚地创建实例,延迟创建

1.写法
class SingleTonLazy{private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//懒汉模式创建实例的时机是第一次使用的时候而不是程序启动的时候instance = new SingleTonLazy();}return instance;}private SingleTonLazy{}
}
2.线程安全问题 

懒汉模式这里getInstance里面给instance赋值这个操作,等号是原子的,但是当它和if语句组合在一起的时候,就变得不是原子的了,此时我们就可以给if语句和内部的赋值语句加锁,让它变成原子的。

class SingleTonLazy{Object locker = new Object();private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){synchronized(locker){if(instance == null){instance = new SingleTonLazy();}return instance;}}private SingleTonLazy{}
}

这里加锁之后,我们使用getInstance就是线程安全的了。可是,这又引出了一个新的问题,当我们第一次调用过getInstance语句之后,instance就创建好了,我们之后再调用getInstance都只需要使用return就好,可是我们在这里加了锁,如果有多个线程同时调用这个方法,就会产生阻塞,影响程序的效率。 这个时候,我们就可以再加一个if语句

class SingleTonLazy{Object locker = new Object();private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//这个if是来判断是否需要加锁synchronized(locker){if(instance == null){//这个if是来判断是否需要创建instanceinstance = new SingleTonLazy();}return instance;}}}private SingleTonLazy{}
}

进行了这么多处理之后,代码依然存在一些问题,instance = new SingleTonLazy()这个操作可能涉及指令重排序问题,jvm可能会更改指令的顺序,new这个操作涉及到申请内存空间,初始化对象,将内存地址赋值给引用变量。如果这个new操作先把内存地址赋值给引用变量,再进行变量初始化的话,这时另一个线程使用getInstance方法时,instance这个实例就已经存在了,可是还没有初始化,这又构成了线程安全问题,此时我们就可以使用volatile关键字来修饰instance,阻止jvm进行指令重排序

class SingleTonLazy{Object locker = new Object();private static volatile SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//这个if是来判断是否需要加锁synchronized(locker){if(instance == null){//这个if是来判断是否需要创建instanceinstance = new SingleTonLazy();}return instance;}}}private SingleTonLazy{}
}

这样我们的代码就是线程安全的了。 

2.阻塞队列

阻塞队列其实就是一种更复杂的队列,它是线程安全的

1.特性

a.队列为空时,尝试出队列,出队列操作就会阻塞,阻塞到其他线程添加元素为止

b.队列为满时,尝试入队列,入队列操作也会阻塞,阻塞到其他线程取走元素为止

2.应用场景

1.生产者消费者模型

简单来说就是生产者生产产品,消费者消费产品,他们在一个消费场所进行这些操作,而这个消费场所就是阻塞队列

1.该模型的优点
1.解耦合

这里的解耦合不一定是两个线程之间,也可以是两个服务器之间。

如果是A直接访问B,此时A和B的耦合就会更高。编写A的代码的时候,多多少少会有一些和B相关的逻辑,编写B的代码的时候,也会有一些A的相关逻辑。 

此时添加一个阻塞队列,让A和队列交互,让B和队列交互,这样A和B之间就解耦合了。A和B是业务服务器,所以经常会涉及到改动,而阻塞队列并不会经常修改,所以A,B分别和阻塞队列耦合是没什么问题的。阻塞队列非常重要,有时甚至会把队列单独部署成一个服务,称为“消息队列”。

2.削峰填谷

在实际的应用场景中,服务器接收到的请求并不是稳定的,有时候会很多,有时又很少,当没有阻塞队列,两个服务器直接进行交互时

当A遇到一波流量激增,此时它会把每个请求都转发给B,B也会承担一样的压力,此时就很容易把B给搞挂了。 

一般来说A这种上游的服务器,尤其是入口的服务器,干的活更简单,单个请求消耗的资源较少,而像B这种下游的服务器,通常承担更重的任务量(复杂的计算/存储工作),单个请求消耗的资源更多。

如果有阻塞队列的话,压力就可以由阻塞队列来承担,B可以不关心数据量的多少,按照自己的节奏慢慢处理队列里的数据即可。由于大量的请求一般都是突发的,时间也不长,所以B可以趁着峰值过去了继续消费数据,利用波谷的时间,来消费之前积压的数据。

2.该模型的缺点

1.引入队列之后,整体的结构会更复杂,此时就需要更多的机器进行部署,生产环境的结构也会更加复杂,管理起来更麻烦

2.效率会有影响

3.使用

Java标准库中提供了阻塞队列,我们可以直接使用

1.put()/take()

阻塞队列继承了Queue接口,所以我们可以使用offer()和pull()来存取数据,但是想要达到阻塞效果的话必须使用take()和put()方法。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/76432.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在 Dev-C++中编译运行GUI 程序介绍(三)有趣示例一组

在 Dev-C中编译运行GUI程序介绍(三)有趣示例一组 前期见 在 Dev-C中编译运行GUI 程序介绍(一)基础 https://blog.csdn.net/cnds123/article/details/147019078 在 Dev-C中编译运行GUI 程序介绍(二)示例&a…

【高校主办】2025年第四届信息与通信工程国际会议(JCICE 2025)

重要信息 会议网址:www.jcice.org 会议时间:2025年7月25-27日 召开地点:哈尔滨 截稿时间:2025年6月15日 录用通知:投稿后2周内 收录检索:EI,Scopus 会议简介 JCICE 2022、JCICE 2023、JCICE 2…

【Linux】Linux 操作系统 - 03 ,初步指令结尾 + shell 理解

文章目录 前言一、打包和压缩二、有关体系结构 (考)面试题 三、重要的热键四、shell 命令及运行原理初步理解五、本节命令总结总结 前言 本篇文章 , 笔者记录的笔记内容包含 : 基础指令 、重要热键 、shell 初步理解 、权限用户的部分问题 。 内容皆是重要知识点 , 需要认真理…

Python: sqlite3.OperationalError: no such table: ***解析

出现该错误说明数据库中没有成功创建 reviews 表。以下是完整的解决方案: 步骤 1:创建数据库表 在插入数据前,必须先执行建表语句。请通过以下任一方式创建表: 方式一:使用 SQLite 命令行 bash 复制 # 进入 SQLite 命令行 sqlite3 reviews.db# 执行建表语句 CREATE T…

VSCode CLine 插件自定义配置使用 Claude 3.7 模型进行 AI 开发

一个互联网技术玩家,一个爱聊技术的家伙。在工作和学习中不断思考,把这些思考总结出来,并分享,和大家一起交流进步。 本文介绍如何在 Visual Studio Code (VSCode) 中安装和自定义配置 CLine 插件,并使用 Claude 3.7 模…

【VSCode配置】运行springboot项目和vue项目

目录 安装VSCode安装软件安装插件VSCode配置user的全局设置setting.jsonworkshop的项目自定义设置setting.jsonworkshop的项目启动配置launch.json 安装VSCode 官网下载 安装软件 git安装1.1.12版本,1.2.X高版本无法安装node14以下版本 nvm安装(github…

linux shell编程之条件语句(二)

目录 一. 条件测试操作 1. 文件测试 2. 整数值比较 3. 字符串比较 4. 逻辑测试 二. if 条件语句 1. if 语句的结构 (1) 单分支 if 语句 (2) 双分支 if 语句 (3) 多分支 if 语句 2. if 语句应用示例 (1) 单分支 if 语句应用 (2) 双分支 if 语句应用 (3) 多分支 …

榕壹云在线商城系统:基于THinkPHP+ Mysql+UniApp全端适配、高效部署的电商解决方案

项目背景:解决多端电商开发的痛点 随着移动互联网的普及和用户购物习惯的碎片化,传统电商系统面临以下挑战: 1. 多平台适配成本高:需要同时开发App、小程序、H5等多端应用,重复开发导致资源浪费。 2. 技术依赖第三方…

神经动力学系统与计算及AI拓展

大脑,一个蕴藏在我们颅骨之内的宇宙,以活动脉动,如同由电信号和化学信号编织而成的交响乐,精巧地协调着思想、情感和行为。但是,这种复杂的神经元舞蹈是如何产生我们丰富多彩的精神生活的呢?这正是神经动力…

K8s常用基础管理命令(一)

基础管理命令 基础命令kubectl get命令kubectl create命令kubectl apply命令kubectl delete命令kubectl describe命令kubectl explain命令kubectl run命令kubectl cp命令kubectl edit命令kubectl logs命令kubectl exec命令kubectl port-forward命令kubectl patch命令 集群管理命…

本地化部署DeepSeek-R1蒸馏大模型:基于飞桨PaddleNLP 3.0的实战指南

目录 一、飞桨框架3.0:大模型推理新范式的开启1.1 自动并行机制革新:解放多卡推理1.2 推理-训练统一设计:一套代码全流程复用 二、本地部署DeepSeek-R1-Distill-Llama-8B的实战流程2.1 机器环境说明2.2 模型与推理脚本准备2.3 启动 Docker 容…

单片机方案开发 代写程序/烧录芯片 九齐/应广等 电动玩具 小家电 语音开发

在电子产品设计中,单片机(MCU)无疑是最重要的组成部分之一。无论是消费电子、智能家居、工业控制,还是可穿戴设备,小家电等,单片机的应用无处不在。 单片机,简而言之,就是将计算机…

【位运算】两整数之和

文章目录 371. 两整数之和解题思路:位运算 371. 两整数之和 371. 两整数之和 ​ 给你两个整数 a 和 b ,不使用 运算符 和 - ,计算并返回两整数之和。 示例 1: 输入:a 1, b 2 输出:3示例 2&#xff1…

使用Python从零实现一个端到端多模态 Transformer大模型

嘿,各位!今天咱们要来一场超级酷炫的多模态 Transformer 冒险之旅!想象一下,让一个模型既能看懂图片,又能理解文字,然后还能生成有趣的回答。听起来是不是很像超级英雄的超能力?别急&#xff0c…

新闻推荐系统(springboot+vue+mysql)含万字文档+运行说明文档

新闻推荐系统(springbootvuemysql)含万字文档运行说明文档 该系统是一个新闻推荐系统,分为管理员和用户两个角色。管理员模块包括个人中心、用户管理、排行榜管理、新闻管理、我的收藏管理和系统管理等功能。管理员可以通过这些功能进行用户信息管理、查看和编辑用…

游戏引擎学习第218天

构建并运行,注意一下在调试系统关闭前人物的移动速度 现在我准备开始构建项目。如果我没记错的话,我们之前关闭了调试系统,主要是为了避免大家在运行过程中遇到问题。现在调试系统没有开启,一切运行得很顺利,看到那个…

基于混合编码器和边缘引导的拉普拉斯金字塔网络用于遥感变化检测

Laplacian Pyramid Network With HybridEncoder and Edge Guidance for RemoteSensing Change Detection 0、摘要 遥感变化检测(CD)是观测和分析动态土地覆盖变化的一项关键任务。许多基于深度学习的CD方法表现出强大的性能,但它们的有效性…

Go语言从零构建SQL数据库(6) - sql解析器(番外)- *号的处理

番外:处理SQL通配符查询 在SQL中,SELECT * FROM table是最基础的查询之一,星号(*)是一个通配符,表示"选择所有列"。虽然通配符查询看起来简单,但在解析器中需要特殊处理。下面详细介…

浅析Centos7安装Oracle12数据库

Linux下的Oracle数据库实在是太难安装了,事贼多,我都怀疑能安装成功是不是运气的成分更高一些。这里操作系统是Centos7,Oracle版本是Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production。 Oracle下载链接: https…

02-redis-源码下载

1、进入到官网 redis官网地址https://redis.io/ 2 进入到download页面 官网页面往最底下滑动,找到如下页面 点击【download】跳转如下页面,直接访问:【https://redis.io/downloads/#stack】到如下页面 ​ 3 找到对应版本的源码 https…