Java并发--三大性质

一、多线程的三大性质

原子性;可见性、有序性

二、原子性

原子性介绍

原子性是指:一个操作时不可能中断的,要么全部执行成功要么全部执行失败,有着同生共死的感觉。即使在多线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

先看看哪些是原子操作,哪些不是原子操作:

int a=10;  //1

a++;  //2

int b=a;  //3

a=a+1;  //4

上面这四个语句中只有第1个语句是原子操作,将10赋值给线程工作内存的变量a,而语句2 a++,实际上包含了三个操作:读取变量a的值;对进行加1的操作,将计算后的值在赋值给变量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和write也可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序:read a,read b,load b,load a

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

 三、synchronized和volatile的原子性

synchronized

上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和unlock两条原子操作。

如果我们需要大范围的原子操作就可以使用lock和unlock原子操作。尽管JVM没有吧lock和unlock开放给我们,但JVM以更高层次的指令monitorenter和monitorexit开放给我们使用,反映到Java代码中就是---synchronized关键字,也就是说synchronized满足原子性.

volatile

package passtra;

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

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

开启10个线程,每个线程都自加10000次,如果不出现线程安全的问题最终的结果应该是10*10000,可是运行多次都是小于100000。

问题在于volatile并不能保证原子性,count++并不是一个院子操作,包含了三个步骤:1、读取变量count的值;2、对count加1;3、将新值赋给变量count。如果线程A读取count到工作内存中,其他线程对这个值已经做了自增操作后,那么线程A的值自然而然就是一个过期的值,因此,总结过必然会是小于100000的。

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

运算结果并不依赖变量的当前的值,或者能够确保只有一个线程修改变量的值;

变量不需要与其他的状态变量共同参与不变约束。

四、synchronized和volatile的有序性

synchronized

synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。

因此,synchronized语义就要求线程在访问读写操作共享变量时只能串行执行,因此synchronized具有有序性。

volatile

在JMM 中,为了性能优化,编译器和处理器会进行指令重排序,也就是说Java程序天然的有序性,可以总结为:如果在本线程内观察,所有的操作都是有序的,如果在一个线程观察另一个线程,所有的操作都是无序的。

在单例模式的实现上有一种双重检验锁定的方式DCL(Double-checked Locking)

 

public class Singleton{
    
    private static volatile Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getsingleton(){
        
        if(singleton==null){
            synchronized (Singleton.class) {
                if(singleton==null){
                    singleton=new Singleton();
                }
            }
        }
        return singleton;
    }
}

 

这里为什么要加volatile?先分析下不加volatile的情况,有问题的语句是这条:singleton=new Singleton();这条语句实际上包含了三个操作:1、分配对象的内存空间;2、初始化对象;3、设置singleton指向刚分配的内存地址。但由于存在重排序的问题,可能有以下的执行顺序:如果2和3进行了重排序的话,线程B进行判断if(singleton==null)时就会出现true,而实际撒花姑娘这个singleto并没有初始化成功,显而易见对B线程来说之后的操作就会是错的。

而用volatile修饰的话就可以禁止2和3操重排序,从而避免这种情况

volatile包含禁止指令重排序的语义,其具有有序性。

 五、synchronized和volatile的可见性

可见性是指:当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

synchronized:当线程获取锁时或从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。所以,synchronized具有可见性

volatile:同样在volatile分析中,会通过在指令中天机lock指令,一实现内存可见性,因此,volatile具有可见性

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

volatile具有有序性和可见性

 

posted @ 2020-08-19 14:07  mark-xiang  阅读(276)  评论(0编辑  收藏  举报