【Java】【并发编程】详解并发三大特性

举报
huahua.Dr 发表于 2021/09/24 23:35:56 2021/09/24
【摘要】 并发三大特性:原子性、有序性、可见性。

什么是并发三大特性

在并发编程中,去解决线程安全的问题,一般可以从两大核心和三大特性作为切入点来思考怎么去解决,两大核心就是JMM内存模型和happens-before规则,三大特性主要是原子性、可见性和有序性。并发关键字synchronized和volatile都涉及到了三大特性,说明了三大特性的重要性。从这两个关键字分析这三大特性。

一、原子性

什么是原子性:就是指一个操作是不可中断的,要么执行成功要么执行失败,在多线程中,一个线程正在执行就不会被其他线程干扰。分析如下例子:

int a = 10; //1

a++; //2

int b=a; //3

a = a+1; //4

上面这四个语句中只有第1个语句是原子操作,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,java内存模型中定义了8中操作都是原子的,不可再分的。

  • lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
  • unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存复制传输到线程的工作内存中,以便后面的load动作使用;
  • load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值复制放入工作内存中的变量副本
  • use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值同步传送给主内存中以便随后的write操作使用;
  • write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值同步放入主内存的变量中。

上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。那么如何理解这些指令了?比如,把一个变量从主内存中复制到工作内存中就需要执行read,load操作,将工作内存同步到主内存中就需要执行store,write操作。注意的是:java内存模型只是要求上述两个操作是顺序执行的并不是连续执行的。也就是说read和load之间可以插入其他指令,store和writer可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序:read a,read b, load b,load a

由原子性变量操作read,load,use,assign,store,write,可以大致认为基本数据类型的访问读写具备原子性例外就是long和double的非原子性协定

synchronized

上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用,反应到java代码中就是synchronized关键字,也就是说synchronized满足原子性

volatile

我们先来看这样一个例子:

public class VolatileExample {
    private static volatile int counter = 0;

public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++)
                        counter++;
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

开启10个线程,每个线程都自加10000次,如果不出现线程安全的问题最终的结果应该就是:10*10000 = 100000;可是运行多次都是小于100000的结果,问题在于 volatile并不能保证原子性,在前面说过counter++这并不是一个原子操作,包含了三个步骤:1.读取变量counter的值;2.对counter加一;3.将新值赋值给变量counter。如果线程A读取counter到工作内存后,其他线程对这个值已经做了自增操作后,那么线程A的这个值自然而然就是一个过期的值,因此,总结果必然会是小于100000的。

如果让volatile保证原子性,必须符合以下两条规则:

  • 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
  • 变量不需要与其他的状态变量共同参与不变约束

二、可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。同样的在volatile分析中,会通过在指令中添加内存屏障指令,以实现内存可见性。因此, volatile具有可见性

三、有序性

synchronized

synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序性

volatile

在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的volatile包含禁止指令重排序的语义,其具有有序性

四、总结

synchronized: 具有原子性,有序性和可见性volatile:具有有序性和可见性

 

 

 

 

 

 

 

 

 

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。