并发编程中的原子性,可见性,有序性问题

原子性

首先看到的这个原子性,对于我们肯定都不陌生,因为在接触数据库的四大特性的时候就遇到过(原子性,一致性,隔离性,持久性)。在数据库中,原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。当然此时说的原子性操作也类似,即线程执行一系列操作,这些操作都会被看着一个不可分割的整体,要么全部执行,要么全部不执行。

原子性是指,CPU在执行一个或多个操作的过程具有原子性,它们是一个不可分割的整体,在执行的过程中不会被中断。
举例来说明,几天是个好日子,你的老板看你工作劲头不错,要给你发10000元奖金,而进行转账包含两个操作,老板的银行卡上扣除10000元,你的银行卡上增加10000元,而这两个操作就是一个不可分割的整体,要么两个操作全部执行,要么全部不执行,不能单独出现只有老板银行卡扣除money或者只有你的银行卡增加money的情况。

那我们来深究原子性问题,什么原因导致了原子性问题?
就上面的例子来讲,两个操作是不可分割的,当CPU正在执行老板银行卡扣除money的操作时,此时还没有提交完成,CPU突然切换到你的银行卡增加money操作,这就会出错。

因此,原子性产生的原因还是线程的切换。如果线程正在执行一项操作,发生了线程切换,CPU去执行另一项操作,中断了线程执行的任务,就会产生原子性问题。

回归到更常见的例子,我们在写程序的初始阶段,肯定都写过以下这样的代码:

private int i =0public void add(){
    i++;
}

在单线程情况下,这段代码不会发生的问题,但是在多线程并发的情况下,这段代码可能会发生问题。因为i++,++i等操作并非是原子性操作。这大致上包含三个步骤,1.将变量i从内存中加载到CPU的寄存器中;2.在CPU的寄存器中执行i++或++i的操作;3.将运算的i++的结果写入缓存中。因此在多线程访问时,有可能出现,线程1读取了i的值,线程2也读取了i的值,线程2将i的值进行+1并将i的值写入内存,但是这种情况下,线程1读到的i的值还是原始的那个,因此很容易出现多线程并发上的错误。

再多说一点,对于这种情况,我们可以用加锁和volatile的方法来解决多线程的问题,即:

private volatile int i =0public synchronized void add(){
    i++;
}

但是加锁的方式可能会影响线程执行的效率,因为当线程1拿到锁,线程2无法执行方法,只能等线程1释放锁后再获得CPU的使用权,当线程1执行的时候,线程2处于阻塞状态,所以可能会影响线程的执行效率。

当然在Java中也有一些原子类,他们专门可以保证原子操作,此类位于package java.util.concurrent.atomic包中,这个在后面会详细介绍,大佬感兴趣的话可以先看源码。


因此做个总结,如果线程在执行的过程中发生线程切换,使得线程暂停当前的任务而去执行其他的任务,可能会发生原子性问题。

可见性

可见性指的是,在多线程下,一个线程修改了共享变量,其他线程能立即读取到共享变量的最新值。无论共享变量如何变化,其他线程总是能够及时读取到共享变量的最新的值。

当线程在串行程序中执行或者线程是在单核CPU情况下执行时,不会出现线程之间的可见性问题。因为在单核CPU中,即使有很多线程,但是一个时刻,CPU只能执行一个线程,也就是说只有一个线程来抢到CPU的资源,来对CPU缓存进行读写操作,在这个线程放弃CPU停止执行任务时,其他线程会对同一个CPU缓存进行读写操作,并且会读取CPU缓存中的最新值,所以不会发生线程的可见性问题。

而在多核CPU下恰恰相反,因为多核CPU下,每个CPU核心都有自己各自的缓存,多个线程在读取主内存的共享变量时,会把主内存中的共享变量复制到线程的私有内存中,每个线程在对数据进行读写操作时,都会直接操作自身工作内存的数据。
举例说明,此时线程1和线程2运行在两个不同的CPU核心中,线程1和线程2同时将主内存中的共享变量i复制到自己的CPU缓存中,进行读写操作,但是线程2无法及时读取都线程1***享变量i的值,线程1也无法及时读取到线程2***享变量i的值。所以线程1和线程2对共享变量i存在有可见性问题。

因此综上所述,多个线程在多个CPU上运行,会出现可见性问题,所以造成可见性的根本原因就是CPU的缓存机制。在串行程序和单核CPU上不存在可见性问题。

有序性

有序性,顾名思义,就是有顺序,按顺序执行。在并发编程中亦是如此。有序性是指能够按照编写代码的顺序执行,但是为了提高程序的执行性能和编译性能,计算机和编译器有时候会修改程序的执行顺序。然而,在多线程情况下,编译器对执行顺序的修改可能会造成错误。

下面举例来说明会出现有序性的情况。

在创建单例对象时,使用到了双重检测机制,在并发情况下,可能会出现问题。以下是创建单例对象的方法:

private static SingleInstance instance; public static SingleInstance getInstance(){ if(instance == null){ synchronized(SingleInstance.class){ if(instance ==null){
instance =new SingleInstance();
        }
    }
  } return instance;
}

假设现在有线程1和线程2,同时执行getInstance()方法获取instance的对象实例,当进行if判断时,instance均为null,此时由于方法加了synchronized锁,只能有一个线程获取锁,另一个线程阻塞,假如线程1获取了锁,创建了对象,执行完成后释放锁,线程2获取锁,发现此时instance不为null,因此不会再创建对象了。

我们在前面也说了new一个对象包括三个步骤:1.为对象分配内存空间;2.初始化对象;3.将对象的引用指向内存空间。

在正常情况下程序是按照顺序执行的,但是如果CPU对对象进行重排序,把第三个步骤排到了第二个步骤的前面,在并发情况下可能就会发生错误。

分析:假设线程1和线程2都进入到了if判断阶段,如果线程1获取了锁,进入到了代码块里,在new对象时,JVM会在堆中为对象找到一块存储空间,并且线程1会将instance的引用指向该内存空间,但是此时并没有为对象进行初始化,因此还是空对象,当线程切换到线程2时,首先会拿到锁,进入到代码块中去,由于instance是空对象,在使用instance时,就可能会出错。

综上,出现此错误的原因就是,编译器修改了创建对象的执行顺序,导致在多线程并发情况下,程序出现了错误。因此出现有序性的根本原因就是编译器修改了程序的执行顺序。

#Java##程序员#
全部评论

相关推荐

好像有点准
我推的MK:感觉这个表格呢好像有用又好像没用,真有offer了不管加班多么严重也得受着,没offer管他加班什么样也只能看看,反正轮不到我选
点赞 评论 收藏
分享
03-15 14:55
已编辑
门头沟学院 golang
bg:双非学院本 ACM银 go选手timeline:3.1号开始暑期投递3.7号第二家公司离职顽岩科技 ai服务中台方向 笔试➕两轮面试,二面挂(钱真的好多😭)厦门纳克希科技 搞AI的,一面OC猎豹移动 搞AIGC方向 一面OC北京七牛云 搞AI接口方向 一面OC上海古德猫宁 搞AIGC方向 二面OC上海简文 面试撞了直接拒深圳图灵 搞AIGC方向一面后无消息懒得问了,面试官当场反馈不错其他小厂没记,通过率80%,小厂杀手😂北京字节 具体业务不方便透露也是AIGC后端方向2.28约面 (不知道怎么捞的我,我也没在别的地方投过字节简历哇)3.6一面 一小时 半小时拷打简历(主要是AIGC部分)剩余半小时两个看代码猜结果(经典go问题)➕合并二叉树(秒a,但是造case造了10分钟哈哈)一天后约二面3.12 二面,让我挑简历上两个亮点说,主要说的docker容器生命周期管理和raft协议使用二分法优化新任leader上任后与follower同步时间。跟面试官有共鸣,面试官还问我docker底层cpu隔离原理和是否知道虚拟显存。之后一道easy算法,(o1空间解决 给定字符串含有{和}是否合法)秒a,之后进阶版如何用10台机加快构建,想五分钟后a出来。面试官以为45分钟面试时间,留了18分钟让我跟他随便聊,后面考了linux top和free的部分数据说什么意思(专业对口了只能说,但是当时没答很好)。因为当时手里有7牛云offer,跟面试官说能否快点面试,马上另外一家时间到了。10分钟后约hr面3.13,上午hr面,下午走完流程offer到手3.14腾讯技术运营约面,想直接拒😂感受: 因为有AIGC经验所以特别受AI初创公司青睐,AIGC后端感觉竞争很小(指今年),全是简历拷打,基本没有人问我八股(八股吟唱被打断.jpeg),学的东西比较广的同时也能纵向深挖学习,也运气比较好了哈哈可能出于性格原因,没有走主流Java路线,也没有去主动跟着课写项目,项目都是自己研究和写的哈哈
烤点老白薯:你根本不是典型学院本的那种人,贵了你这能力
查看7道真题和解析
点赞 评论 收藏
分享
03-19 10:07
已编辑
广东药科大学 golang
Yki_:你倒是进一个面啊
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务