【Java EE 初阶】如何保证线程安全

文章介绍了线程的概念,强调了线程安全的重要性,通过例子展示了线程不安全的原因,如非原子性操作和内存可见性问题。接着,讨论了Java内存模型JVM以及其对线程安全的影响,并提出Synchronized关键字作为解决线程不安全的手段,解释了其工作原理和注意事项。
摘要由CSDN通过智能技术生成

目录

 

1.线程是什么?

2.线程安全(重点)

1.概念:

1.举例:用两个线程分别对同一个变量做五万次自增,观察答案是否符合预期

那么是哪些原因造成了这种线程不安全的现象呢?我们一起来分析一下:

1.多个线程修改了同一个变量

2.线程在CPU调度上是抢占式执行的

3.没有保证线程的原子性

 4.内存可见性

JAVA内存模型JVM是什么?

为什么要用JVM呢?

 5.代码的有序性

我们来总结一下造成线程不安全的原因主要有哪些? 

3.解决线程不安全问题(Synchronized关键字-监视器锁)

核心代码展示:对count自增的方法上锁

结果展示:

4.Synchronized关键字的注意事项

在JAVA虚拟机中,对象在内存中的结构可以划分为4部分区域

5.Synchronized的使用


 

1.线程是什么?

Thread是Java中的类,PCB是系统中真正的线程

调用start()方法才会真正的去系统中申请一个线程

线程的状态有哪些? 

d41fca42e3e7489ba7a0b99610e551f8.png

2.线程安全(重点)

1.概念:

如果多线程环境下代码运行的结果与单线程环境下相同,则说明这个线程是安全的

1.举例:用两个线程分别对同一个变量做五万次自增,观察答案是否符合预期

5ceb8680f07a47728236b788ea575f40.png

可以看见结果并不是我们所想的100000

那么是哪些原因造成了这种线程不安全的现象呢?我们一起来分析一下:

1.多个线程修改了同一个变量

2.线程在CPU调度上是抢占式执行的

3.没有保证线程的原子性

原子性就是一段代码,要么全部执行成功,要么全部执行失败

我们知道,代码最终都会编译成CPU可以执行的指令,那么count++这一条指令其实对应着三步操作:

1.从内存把数据读到CPU

2.进行数据更新

3.把数据写回CPU

那么为什么不保证原子性就会出错呢?首先这和线程的抢占式执行密切相关

我们来举个例子:

分别有两个线程对count这个变量进行自增操作t1和t2,count初始值为0

假设t2先从内存把数据读到CPU,此时CPU被t1抢占了过去,t1开始从内存把数据读到CPU并对count进行了自增操作:count++,此时t2又把CPU抢占了回来,也对count进行了自增操作,count++,此时又回到t1,将数据写回CPU,count = 1,然后t2也进行了数据的写回,count= 1;现在我们可以发现问题了,由于没有保证原子性,导致多个线程之间进行了重复的自增操作,覆盖掉别的线程修改后的值,所以最终的结果不对

图解如下:

9108dca3deb5480389d37aefb027435a.png

 4.内存可见性

在多线程环境下,某一个线程修改了共享的变量,其他线程不能及时地接收到最新的值

JAVA内存模型JVM是什么?

99cfac92f5474ccbbbc608e91c326167.png

1.主内存:指的是硬件的内存条,进程在启动的时候会申请一些资源,包括内存资源,用来保存所有的变量

2.工作内存:指的是线程独有的内存空间,它们之间不能相互访问,起到了线程之间内存隔离的作用

3.JVM规定,一个线程在修改某个变量的时候,必须把这个变量从主内存加载到自己的工作内存,修改完成后在返回主内存

为什么要用JVM呢?

Java是一个跨平台语言,而JVM把不同的计算设备和操作系统对内存的管理做了一个统一的封装

 5.代码的有序性

是指在编译过程中,JVM调用本地接口,CPU执行指令的过程中,指令的有序性

3eb28da366b0425fb8009f12740941a2.png

 这种通过打乱顺序,把不相关的代码或指令重新排列,从而提高程序效率的方式,在程序中叫做指令重排序

在单线程中重排序之后的结果百分百正确,但在多线程环境下就未必了

我们来总结一下造成线程不安全的原因主要有哪些? 

f08d9ab219284248948289b514036d13.png

3.解决线程不安全问题(Synchronized关键字-监视器锁)

对当前执行的代码加锁

当某一个线程要执行这个方法时,就先获取锁,获取到锁之后再去执行代码,其他线程也需要执行这个方法时,也需要获取锁,但是已经有线程持有锁时,他就需要等待,等到上一个线程释放这把锁(注意分辨锁的释放与CPU调度的区别,只有锁被释放之后,其他线程才能竞争锁,否则一直需要等待,且CPU的调用调离与释放锁没有任何关系)之后才有可能竞争到CPU的调度

因为线程时抢占式执行,CPU对调度是随机的,并不是先阻塞等待的线程就一定会先拿到锁

我们发现上了锁之后的代码变成了单线程,这也是不可避免的

所以在获取数据时,我们用多线程来提高效率

在修改数据时,用Synchronized上锁来保证安全

核心代码展示:对count自增的方法上锁

public synchronized void increase() {
        count++;
    }

结果展示:

c6e944af48db4912a2434172b544cb77.png

可以看见上锁之后结果正确了,说明Synchronized解决了原子性的问题,并且通过把并行操作变成了串行操作,也保证了内存的可见性

但是Synchronized并不能保证有序性

4.Synchronized关键字的注意事项

1.从并行到串行:首先要保证正确,然后才考虑效率

2.加锁与CPU调度:锁的释放与CPU调度的区别,只有锁被释放之后,其他线程才能竞争锁,否则一直需要等待,且CPU的调用调离与释放锁没有任何关系

3.加锁的范围:比如加在for循环外就与串行是一模一样的,但加在外面两个for循环之间就是并发执行的,这样写要比两个for循环分别执行要快很多,这个要跟及实际情况考虑,加锁的范围越大称锁的粒度越大,反之越小

4.给代码块加锁:Synchronized可以用来修饰代码块,此时需要传入一个参数,用来表明锁的对象

5.锁对象:竞争的锁都必须是针对同一个对象的,可以自定义一个单独的对象表示锁(可以是JAVA中的任何对象),也可以使用this,多线程并不关心锁的对象是哪个,只关心他们是否竞争同一把锁,也就是是否是同一个对象,只有同一个对象才会产生锁的竞争

在JAVA虚拟机中,对象在内存中的结构可以划分为4部分区域

  • mark word 与 类型指针(_class)  这两部分统一称为对象头 主要描述了当前是哪个线程获取到的资源,记录的是线程对象信息,当线程释放所资源的时候就会把线程对象信息清除掉,其他的线程就可以继续获得锁资源
  • 实例数据 保存的就是类中的属性
  • 对其填充 每一个类对象占用的字节数必须是八字节的整数倍

5.Synchronized的使用

6b8be3966b7e498686174e6e9c20af7b.png

 989469be810641a8a640f19b329e0705.png

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值