多线程之线程安全(三)

1. 线程安全

这是多线程中最核心的话题
预期能够自增10w次,但是实际上自增的次数为无法确定,每次运行的结果不一样
此为bug,如果不是两个程序并发,能够按要求自增.导致代码中出现了bug,这种情况称为”线程不安全“

由于多线程并发执行多线程并不好驾驭~
正因为如此,有的编程语言中,直接就把线程给干掉了~~(进行了诸多限制)
python 中的线程,就是“假的线程”,很多时候根本无法并发,JS中压根就没线程, 只能通过"定时器’+回调 这样的机制来凌合实现类似于 •并发•的效果
Go中摒奔了线程,但是引入了更高端的“协程:,借助办程来井发编程~(用起来要比 线程 更简单)

下面画的这种请情况,线程一执行三个操作,后再线程二执行,此时操作出来的结果,两线程个自增一次,结果为2;

在这里插入图片描述
在这里插入图片描述

但是,这两个线程不是按照这样的顺序进行的
操作系统调度线程的时候,是以抢占式执行的方式,某个线程什么时候在cpu上执行,什么时候切换出cpu都是完全不确定的
而且另外一方面,两个线程在两个不同的cpu也完全可以并发执行,因此两个线程具体的执行顺序,是完全不可以预期的;
是在线程一之前读到的基础(0)上开始自增的,所以此时count还是1
很多情况都会产生这种问题,如果线程1的save在线程2的load之后,此时当前线程就会出现这种不安全的亲狂,或者说必须是串行执行的时候,才不会出现这种问题
极端情况下,如果每次都触发了线程问题,(都是并行执行),结果刚好是5w,如果每次自增都没有触发线程安全问题,(都是串行执行),结果刚好是10w;

2.产生线程不安全的原因

  1. 线程之间是抢占式执行的(根本原因) 操作系统内核决定
    抢占式执行,导致这两个线程里面操作的先后顺序无法确定,这种随机性,就是导致线程安全问题的根源

  2. 多个线程修改同一变量
    某些场景写法转换规避线程安全问题
    一个线程修改同一个变量,没有线程安全问题,不涉及并发,结果就是确定
    多个线程读取同一变量,没有线程安全文艺,读只是单纯的吧数据放到cpu中,不管怎么度,内存的数据始终不变
    多个线程修改不同的变量,没有线程安全问题,同第一个原因差不多

  3. 原子性
    像++这样的操作,本质上是三个操作,是一个"非原子"的操作
    像=操作,本质是一个操作,认为是一个原子
    像当前 ++操作本身不是原子的,但是可以通过加锁的方式,可以把这个操作变成原子的

  4. 内存可见性
    可见性指的是一个线程对共享变量值的修改,能够及时的被其他线程看到

    线程1:读取变量
    线程2:对变量自增
    在这里插入图片描述

如果只是单线程下,这样的优化,没有任何副作用,这个省略操作是编译器javac和JVM综合配合的效果
一个线程修改,一个线程读取,由于编译器优化,可能把一些中间环节的save和load操作去掉
此时读的线程无法更新值

volatile关键字,用来解决这个问题

  1. 指令重排序(也与编译器相关)
    编译器会自动调整执行指令的顺序,以达到提高执行效率的效果,
    调整的前提是,保证指令的最终效果是不变的
    如果当前的逻辑只是在单线程下运行,编译器判定顺序是否影响结果,就很容易;
    如果当前的逻辑可能再多线程下运行,编译器判定顺序是否影响结果,就可能出错
    (编译器优化,是一个非常智能呢个的,即使代码挫,编译器一顿优化后,代码效率还是很高)

3.解决线程不安全问题

一,最普适的办法就是通过原子性来解决
synchronized
1)原义叫做“同步”,理解成互斥
其功能本质是把并发改串行,牺牲一点速度,保证准确度
1.修饰方法在这里插入图片描述
如果两个线程同时并发的尝试调用这个synchronized修饰的方法
此时一个线程会执行这个方法,另外一个线程会等待,等待第一个线程方法执行完了之后,第二个线程才会继续执行

就相当于“加锁”和“解锁”
进入synchronized的方法,相当于 加锁,
出了synchronized的方法,相当于 解锁,
如果当前程序已经是加锁的状态,其他线程就无法执行这里的逻辑,就只能阻塞等待
2.修饰代码块
在这里插入图片描述

synchronized如果修饰代码块,

需要显示的在()中指定一个要加锁的对象,
如果修饰的是非静态方法,相当于加锁对象就是this

啥叫“加锁的对象”
Java中的任意对象,都可以作为锁对象,但c++,python,go,加锁操作,只能针对指定的对象加锁,
例如c++里面有一个专门std::mutex这个类,这个类创建的对象才叫锁对象
例如
对象:Counter counter = new Counter();
Java里面一个对象的内存布局:
在这里插入图片描述

如果两个线程,尝试对同一个锁对象进行加锁,此时一个线程会先获得锁,另外一个线程会阻塞等待
如果两个线程,尝试对两个不同的锁对象进行加锁,此时不涉及到并行,两个线程都会获得各自的锁,互不冲突,没有锁竞争;

  1. 刷新内存
    synchronized 的工作过程:
  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

在这里插入图片描述

保证内存的可见性,但一旦用了synchronized,此时程序和“高性能”无缘
在这里插入图片描述

保证可见性

3)可重入
在这里插入图片描述

synchronized实现可重入:
synchronized内部记录了当前这个锁是哪个线程持有的
synchronized 修饰普通方法的话,相当于是针对 this 进行加锁.
这个时候如果两个线程井发的调用这个方法,此时是否会触发锁竞争,就看实际的锁对象是否是同一个了
synchronized 修饰静态方法的话,相当于针对 类对象 进行加锁
由于类对象是单例的,两个线程并发调用该方法,一定会触发锁竞争

反射~
反射也是面向对象中的一个基本特性(和封装继承多态…是井列关系)
反射也叫“自省",—个对象能够认清自己•(程序运行时)
(这个对象里包含哪些属性, 每个属性叫啥名字,是啥类型public/private.
包含哪些方法,每个方法叫啥名字,参数列表是啥,public/private.)
这些不是一看代码就知道了?
这些信息来自于 class 文件(java 被编泽生成的二进制字节码),会在JVM 运行的时候加载到内存

4.标准库中(集合类)的线程安全类

这里的集合类,大部分是线程不安全的(不能再多线程环境下并发修改同一个对象)
ArrayList/LinkededList/HashSet…都是不安全的
还有线程安全的
vector jdk早期的集合类,设计不是很科学,一般不建议使用vector,而是使用ArrayList来代替
vector也是一个顺序表(动态数组),能自动扩容
vector使用synchronized来保证线程安全~,给很多方法中都加入了synchronized来修饰,因为大多数情况都不需要在多线程中使用vector,而我们加入了太多synchronized就会对单线程下的操作的效率造成影响(类似HashTable)

5.volatile

1)能保证内存可见性问题
在这里插入图片描述

在快速的循环读入flag的值(频繁的从内存中读数据)
由于这里读得太快,所以编译器进行了优化,并不会每次都从内存中读取数据,读了一次之后,后续都从cpu中来读内存的值;
在这里插入图片描述

而此时线程2的修改是修改了内存的值
此时此刻,线程1感知不到flag内存对应的数据变化,这个也是内存可见性问题(本质上都是编译器优化所带来的问题)

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

  • 代码在写入 volatile 修饰的变量的时候,
    • 改变线程工作内存中volatile变量副本的值
    • 将改变后的副本的值从工作内存刷新到主内存
  • 代码在读取 volatile 修饰的变量的时候,
    • 从主内存中读取volatile变量的最新值到线程的工作内存中
    • 从工作内存中读取volatile变量的副本

直接访问工作内存(实际上是cpu的寄存器或者cpu的缓存,速度非常快,但是可能出现数据不一致的情况)加上volatile,强制读写内存,速度满了,但是数据的准确性变高了
在这里插入图片描述

volatile只能修饰单一的属性,此时代码中针对这个属性的读写操作就一定能保证读写内存;
2)不保证操作的原子性
volatile是与编译器优化密切相关的东西,难以把握
在这里插入图片描述

一般来说,如果某个变量,在一个线程里面读,在另外一个线程里面写,这个时候大概率需要用到volatile;
volatile这里会涉及到JMM(Java memory mode)内存模型
JMM针对计算机的硬件结构,又做了一层抽象,(主要是Java要考虑到跨平台性,要支持不同的计算机)

JMM就把CPU的寄存器,L1,L2,L3cache统称为“工作内存”,
JMM也把真正的内存称为“主内存”
在这里插入图片描述

三个线程,各有自己的工作内存
每个线程都有自己独立的上下文,独立的上下文就是各自的一组寄存器/cache的内容

CPU在和内存交互的时候,经常会把主内存中的内容,拷贝到工作内存中,然后进行操作,再写回到主内存
这个过程中,就会很容易的出现数据不一致的情况,这种情况再编译器开启优化的时候会特别严重
volatile或者synchronized就能够强制保障接下来的操作就是操作内存,在生成的Java字节码中强制插入一些“内存屏障”的指令,这些指令的效果,就是强制同步主内存和工作内存的内容

synchronized和volatile的区别

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值