线程安全问题

缺乏、安全感 2024-03-25 18:16 160阅读 0赞

目录

1.不安全原因

1.1线程调度无序(抢占式)

1.2多个线程修改同一个变量

1.3修改操作不是原子的

1.4内存可见性

1.5指令重排序

2.解决问题

2.1synchronized 关键字

2.1.1 特点

2.1.2 synchronized的过程

2.2使用

2.2volatile 关键字


1.不安全原因

1.1线程调度无序(抢占式)

  1. public static void main(String[] args) throws InterruptedException {
  2. Counter counter=new Counter();
  3. //两个线程,分别对count自增5w次
  4. Thread t1=new Thread(()->{
  5. for (int i = 0; i < 50000; i++) {
  6. counter.add();
  7. }
  8. });
  9. Thread t2=new Thread(()->{
  10. for (int i = 0; i < 50000; i++) {
  11. counter.add();
  12. }
  13. });
  14. t1.start();
  15. t2.start();
  16. t1.join();
  17. t2.join();
  18. System.out.println(counter.get());
  19. }

实际结果和预期结果不相符==>线程不安全

#

原因:与线程调度随机性相关

count++:由3个cpu指令构成

1.load,把内存中的数据读取到cpu寄存器中

2.add,寄存器中值,进行+1运算

3.save,把寄存器中的值写回到内存中

由于多线程调度顺序不确定,实际执行过程中,++操作的指令顺序有很多可能

例如:

第一种:

t1.load t1.add t1.save t2.load t2.add t2.save

执行过程:t1和t2可能在同一个cpu核心,可能在不同的cpu核心

t1=0 t1中++ —>t1=1 内存中count=1

t2=1 t2中++ —>t2=2 内存中count=2

第二种:

执行过程:

t1.load t2.load t2.add t2.save t1.add t1.save

t1=0 t2=0 t2中++ t2=1

内存中count=1 t1中++ t1=1 内存中count=1

两个结果不同

当前调度顺序无序,不知道有多少次是顺序执行,多少次是交错执行,所以每次结果都不一样

1.2多个线程修改同一个变量

一个线程修改同一个变量==>安全

多个线程读取同一个变量==>安全

多个线程修改不同变量==>安全

1.3修改操作不是原子的

不可分割的最小单位是原子

例如:

  1. 像++操作不是原子的,可以拆分成3个操作(load,add,save)

如果操作对应多个cpu指令,大概率不是原子

  1. 如果是直接使用=赋值,就是原子操作

因为不是原子导致两个线程的指令排列存在变数

不是原子导致两个线程的指令排列存在变数,导致执行结果与预期结果不同

1.4内存可见性

内存可见性:多线程环境下,编译器对代码优化,产生了误判,引发了bug

例如:

  1. while(flag==0){
  2. }

while循环,flag==0

load 从内存读取数据到cpu寄存器,cmp比较寄存器值是否为0

load时间开销高于cmp,因为load开销大,load结果一样,所以编译器就把load优化掉了,只有第一次执行load真正执行了(编译器优化的手段)

编译器优化,在程序结果变前提下,通过加减语句,变换语句,列操作,让整个程序执行的效率大大提升在程序结果不变前提下:单线程下判定是非常准确,多线程就不一定了

1.5指令重排序

指令重排序,是编译器优化的策略

调整代码执行顺序,让程序更高效

调整代码执行顺序,单线程不会有影响,但是多线程就不一定了

#

2.解决问题

2.1synchronized 关键字

2.1.1 特点

某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到

同一个对象 synchronized 就会 阻塞等待

进入 synchronized 修饰的代码块 , 相当于 加锁

退出 synchronized 修饰的代码块 , 相当于 解锁

例如:

  1. public void add(){
  2. synchronized (this){
  3. count++;
  4. }
  5. }

这里的this是锁对象

如果两个线程,针对同一个对象加锁,就会出现锁竞争(一个先拿到锁,另一个阻塞等待)

如果两个线程,针对不同对象加锁,不会有影响

()里的锁对象,可以写作任意一个Object对象(基本数据类型不行),此处的this,相当于counter

  1. Thread t1=new Thread(()->{
  2. for (int i = 0; i < 50000; i++) {
  3. counter.add();
  4. }
  5. });
  6. Thread t2=new Thread(()->{
  7. for (int i = 0; i < 50000; i++) {
  8. counter.add();
  9. }
  10. });

这两个线程在竞争同一个锁对象,就会产生锁竞争,(t1拿到锁,t2阻塞)

运行结果:100000

此时,线程就安全了

就count++为例:

两个线程t1,t2原本有3个指令,加上synchronized后,加上lock和unlock.

如果t1先执行,保证,t2的load一定在t1的save之后,此时就线程安全

加锁:本质是吧并发的变成串行的

join和加锁的区别:

join:两个线程完整的进行串行

加锁:两个线程的某个小部分串行,大部分并行

上述代码,线程的步骤:

1.创建i

2.判定i<5000

3.调用add

4.count++

5.add返回

6.i++

只有4是串行,12356是并发,在保证线程安全的前提,同时代价更快,更好利用多核cpu

因为t1还没有执行完count++,此时t2也执行到count++,t2就会阻塞等待,此时t2暂时还不能执行5 6,但是1 2 3都是并发执行.t1,t2都执行完4,5 6 t1 t2依然可以并发执行

加锁可能会阻塞,对效率有影响

比不加锁准确,比串行快

2.1.2 synchronized的过程

  1. 获得互斥锁

  2. 从主内存拷贝变量的最新副本到工作的内存

  3. 执行代码

  4. 将更改后的共享变量的值刷新到主内存

  5. 释放互斥锁

2.2使用

  1. 直接修饰普通方法:

锁的 SynchronizedDemo 对象

  1. public synchronized void methond() {
  2. }

2) 修饰静态方法:

锁的 SynchronizedDemo 类的对象

  1. public synchronized static void method() {
  2. }

3) 修饰代码块: 明确指定锁哪个对象

锁当前对象

  1. public void method() {
  2. synchronized (this) {
  3. }

锁类对象

  1. public void method() {
  2. synchronized (SynchronizedDemo.class) {
  3. }

2.2volatile 关键字

volatile 修饰的变量, 能够保证 “内存可见性”

  1. volatile public static int flag=0;

每次都从内存读取flag变量的值,编译器不进行优化

volatile 不保证原子性

volatile使用的场景:一个线程读,一个线程写

发表评论

表情:
评论列表 (有 0 条评论,160人围观)

还没有评论,来说两句吧...

相关阅读

    相关 线安全问题

    我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就...

    相关 线安全问题

    一、线程安全 VS 线程不安全? 线程安全指的是代码若是串行执行和并发执行的结果完全一致,就称为该代码是线程安全的。 若多个线程串行执行(单线程执行)的结果和并发执行的

    相关 线安全问题

    定义 > 首先大家需要思考一下何为线程安全性呢??? 《Java并发编程实战》书中给出定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替

    相关 线安全问题

    线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污