多线程之补充说明知识杂点
进程和线程
进程在计算机中就是可运行的程序
线程是计算机中的最小执行单元,也是程序中执行的最小单元。
进程中可以有多个线程,用来线程来解决效率问题。
java中将内存划分区间
三大主要作用域:堆、方法区和栈空间
对于线程来说,堆和方法区是线程所共享的区域,而栈空间是每个线程所独有的空间
栈空间对于线程来说,也叫工作区间。
所谓的主存我觉得也可以称之为堆和方法区来理解。
线程调度
在计算机中的线程调度通常来说,常见的分为两种状态
抢占式调度模型
哪个线程的执行优先级比较高,抢到的CPU时间片的概率就高一点,java采用的就是这种模式
均分式调度模型
平均分配CPU时间片,每个线程占据的CPU时间片长度是一样的。平均分配,java采用的不是这种机制。
线程并发
区分并发和并行
在这里区分并发和并行,其实用一张图来进行理解是比较合适的。
就是排队做核酸检测,如果有多个核酸检测人员一起来做核酸,那么就是并行的;
而相对于每个核酸人员来说,后面都有来进行排队的人员,排队的人员来说,叫做并发
同步和异步 线程编程模型
所谓的线程同步和线程异步
线程同步:多线程排队执行;比如:一个线程先执行,执行完成,然后另外一个线程再执行;
线程异步:每个线程各自执行各自的,其实这也是线程执行的原始状态;
并发情况下的数据安全问题
线程并发条件下出现的数据安全问题
前提:数据安全是需要第一保证的,其次才是效率。尽管在保证数据安全的条件下,效率上是有一定的影响的,但是这不得不在我们的考虑范围之内。
三大条件
并发情况下出现数据安全问题的三大条件:
1、多线程条件下;
2、多线程操作共享数据;
3、多线程操作共享数据,并非是所有的操作都会导致并发条件下出现线程安全问题。
比如说多线程查询条件下,因为都是查询操作,不会出现数据安全问题;
而如果是多线程条件下修改共享资源,那么就可能会出现问题。
所以需要考虑到修改共享数据问题。
共享数据
共享数据在java中指的是在堆内存和方法区中的存储的数据。
而最常见的就是操作堆中的数据
解决方式
保证线程是在同步操作的即可,线程同步也就是保证线程是排队进行操作的即可。
java中推荐使用的是syncronized关键字和lock锁来进行操作。
syncronzied关键字
对于syncronized关键字来说,这里需要特别注意
使用方式有两种:1、加在方法上;加在方法内部(保证哪里需要保证线程问题,就在哪里添加);
syncronized关键字修饰的代码又叫同步代码块!这里再次理解什么叫做同步代码块
对于syncronized关键字所修饰的代码块来说,syncronized关键字如果修饰的是同步代码块,那么括号中书写的是一个对象。
注:java中的任何一个对象都有一把锁,10000个不同的对象有一万个不同的锁
syncronized关键字所需要使用的是变量是共享数据(即共享对象),那么也就是说多线程之间要有共享的对象才能够保证并发性,如果没有共享对象,那么多线程之间将还会是异常来进行执行。
也就是说想要保证多个线程之间保证数据安全问题,那么就要保证线程同步
而线程同步就需要保证线程排队执行代码,这里来举一个例子来讲解一下这里的原理,如下所示:
public class SyncronziedTestOne {
public static void main(String[] args) {
// 锁对象
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj) {
try {
// 先让线程休眠两秒钟
Thread.sleep(8888);
System.out.println("当前线程" + Thread.currentThread().getName() + "开始执行了......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (obj) {
try {
// 先让线程休眠两秒钟
Thread.sleep(8888);
System.out.println("当前线程" + Thread.currentThread().getName() + "开始执行了......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
t1.start();
System.out.println("当前线程"+Thread.currentThread().getName()+"开始执行了......");
}
}
结合着示例来进行分析背后实现的原理:
在上面的代码中,在主线程中创建了两个线程:t1和t2,当两个线程启动的时候,某个线程执行到了syncronzied关键字所修饰的同步代码块中,发现了对象obj,那么这个时候首先会去找到obj对象所关联的对象锁,当前线程持有了obj对象所关联的对象锁之后,锁对象就被标记上了是哪个线程占据了当前的锁;那么对于另外的线程来说,在进行执行的时候,发现了obj对象所关联的对象锁已经被其他线程占据了,只能够排队来进行等待获取得到obj对象所关联的对象所,只有当syncronized关键字修饰的同步代码块中的内容执行完成之后,释放了当前对象的对象锁之后,也就是obj对象所关联的对象锁中的标记为空之后,其他的线程会去竞争(如果当前还有多个线程)可以继续去获取得到obj对象的对象锁。(这里这里的对象锁就是一个标记,表示的是哪个线程占据了的当前的对象的锁对象)
那么画个图来进行表示:
比如说:当前有这么几个线程:t1,t2,t3,t4,t5等五个线程
如果对于共享数据t来说,是t1,t2,t3三个线程所共享的,对于t4和t5线程来说,是不共享的,
那么当使用syncronized关键字的时候,t1,t2,t3线程能够线程线程同步,而对于t4和t5来说,不能够实现线程同步
的效果。
这里注意一下这里的线程状态的更改状态
共享对象
共享对象在java中也就是对应的共享数据,那么来举例子来看一下:
Demo1
public class SyncronziedTestTwo {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
show();
}).start();
}
System.out.println("当前线程" + Thread.currentThread().getName() + "开始执行了......");
}
public static void show() {
Object object = new Object();
synchronized (object) {
try {
Thread.sleep(5000);
System.out.println("hello,world " + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
可以看到上面的方法中,两个线程在执行的时候,会进入到show()方法中来,然后使用syncronized,但是因为每个线程在各自的线程栈中创建的对象,所以就会导致ojbect对象是两个线程不共享的数据。那么这个时候线程就不会同步,而是各自执行各自的。
而对于object对象来说,每个线程在执行show()方法的时候,都将会创建出来一个新的object对象,那么将会导致这里没有共享对象。
Demo2
也就是上面提到的第一个案例:
public class SyncronziedTestOne {
public static void main(String[] args) {
// 锁对象
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj) {
try {
// 先让线程休眠两秒钟
Thread.sleep(8888);
System.out.println("当前线程" + Thread.currentThread().getName() + "开始执行了......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (obj) {
try {
// 先让线程休眠两秒钟
Thread.sleep(8888);
System.out.println("当前线程" + Thread.currentThread().getName() + "开始执行了......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
t1.start();
System.out.println("当前线程"+Thread.currentThread().getName()+"开始执行了......");
}
}
在上面的代码中,此时有三个线程:main、t1和t2线程,虽然三个线程都有共享数据obj,但是因为Main线程没有使用共享变量object,而t1和t2使用了共享变量object,那么将会导致这两个线程发生线程同步。
其实这里有一个疑问,就是为什么在main线程中定义的变量因为是属于main线程的的,但是为何对t1和t2来说是可以看到的呢?
因为在main线程中创建的对象是存在于堆内存中,这个堆中的变量相对于其他线程来说是可见的,所以其他的线程是可以来进行操作的。
这里主要是因为变量的作用域范围在起作用,因为在同一个作用域范围之内,是可以操作到其他线程的。
这个共享变量还可以定义为在当前类中的成员属性位置上。
同步代码快的作用域范围效率越高,如果越大,那么效率越低;
使用syncronzied关键点就在于区分哪些是线程共享变量,哪些变量不是!
java中的三大变量
成员变量
静态变量
局部变量
而对于上面的三大变量来说,局部变量是永远不可能出现线程安全问题的。
因为对于每个线程来说,所有的线程都有自己的栈空间,也就是自己的工作内存。
每个线程操作自己栈空间的数据对于其他线程来说是没有任何影响的。
比如说:
public class SyncronziedTestThree {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
// 原因在于当前的线程在使用的过程中,在主线程会发生变化,而在线程中不会发生变化,一般来说,这种需要使用临时变量来进行替换!
int finalI = i;
new Thread(() -> {
int tmp = 20;
tmp++;
System.out.println("当前线程的变量i是"+tmp);
// // 这里可以看到无法使用当前线程中的对象
// java: 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量
System.out.println("当前的变量i是:"+ finalI);
}).start();
}
System.out.println("当前线程" + Thread.currentThread().getName() + "开始执行了......");
}
}
变量tmp定义在每个线程栈中,对于每个线程来说,都可以独立的来使用线程来进行操作!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?