AtomicInteger和volatile

AtomicInteger中的方法线程安全,它拥有一个volatile修饰的int类型的value值,我们通过AtoicInteger对象对value进行操作是线程安全的,以getAndIncrement()方法为例说明它是如何实现的,我们先看下源码

public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

可以看到它使用了unsafe对象的getAndAddInt方法,我们知道java项目是运行在jvm上的,无法直接访问操作系统底层,unsafe对象提供了很多native方法,可以对内存进行直接操作,unsafe对象不能直接获取,在项目中如果相使用的话只能通过反射获取。我们点进去unsafe的getAndAddInt方法看一下。var2就是我们刚才传进来的valueOffset 代表了value值在内存中的地址偏移量,var4是1,var1就是AtomicInteger对象,unsafe中的方法具有原子性,它的执行不会被打断,它会先获取var1对象对应的value在内存中的值var5,然后使用cas方法将value值进行+1,并放入valueoffset对应的内存地址中,这个过程中可能有多个线程使用同一个原value值对其进行cas操作,但是只会有一个线程成功,而失败线程并不会陷入阻塞状态可以进行重试,可以看到它使用了do while,如果cas执行失败就重复执行这一过程,直到成功。

unsafe的compareAndSwapInt具有原子性且,变量由volatile修饰具有可见性,因此实现了线程安全(通过unsafe类内的方法,利用底层硬件提供的原子性指令保证操作的原子性,用的最多的就是cas操作)

由volatile关键字修饰的变量拥有的可见性和部分顺序性

1.可见性

当一个共享变量被volatile修饰时,它会保证修改的值除了存储进自己工作内存中,还会立即被更新到主存,并将其他线程中该变量的引用置为无效,当有其他线程需要使用该变量时,它会先检查该变量是否还有效,如果无效则去内存中读取新值。即一个线程修改了某个volatile变量的值,这新值对其他线程来说是立即可见的

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

如上面的代码,线程1先执行,线程2修改stop为true后,线程1可执行其他操作,一般线程将变量存储进内部缓存后会将其再存入主存,但如果发生了某些特殊情况。线程2将stop变为true放入了内部缓存后并没有立即将其存入主存,此时线程2又恰好挂掉了,那么线程1就陷入了无限等待之中。而如果stop被volatile修饰的话,当线程2修改了stop值后,就会立即把它刷入主存之中,线程1使用stop时会感知到它不可用,就会再从主存中获取它,而不会出现刚才说的情况

2.禁止指令重排序

一条代码语句会被转化为字节码指令执行时,但由于编译器和处理器的存在,会在不改变执行结果的情况下对指令的执行执行顺序进行重排,优化程序的运行效率,但在多线程情况下有可能出现错误。

(1)volatile关键字能够保证,当执行到它修饰的变量读写操作时(假设 volatile修饰变量a,执行 a=1),a=1前面的操作肯定已经完成,且可以对a=1其后的操作可见

(2)并且a=1之前的操作不能在它之后执行,a=1之后的操作也不能在它之前执行

说的有点绕,看个例子

x = 2;        //语句1
y = 0;        //语句2
volatile flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。因为flag由volatile修饰,所以,1、2语句顺序可以交换但它肯定在3之前执行完毕,并且对4、5可见,4、5的执行顺序可以交换,但是必须在3之后执行。

并且重排序时有关联的语句相对顺序是不会变的,也就是说假如flag没有被volatile修饰,1肯定出现在4前面。2肯定在5前面,而3号语句可以在任意位置。这样才可以保证,单线程情况下,虽然重排序,但是最终结果不会发生改变。大家看到这里,可能觉得我刚才说的“指令重排序可能会导致多线程情况下出错”这句话说的不对,看下这个例子

//线程1
context = initialize()//初始化 语句1
isComplete = true  //语句2



//线程2
while(!isComplete){
  sleep(1s)
}
context.doSomthing()

线程1执行语句1和2,它们可能发生重排序,在单线程情况下,并没有什么影响,但是在多线程情况下,如果语句2在1之前执行,虽然它变成了true但是实际的初始化并没有完成,线程2使用context执行操作时就会出错。假如使用volatile修饰isComplete后,会保证初始化操作在isComplete=true之前完成,这样就可以确保线程2不会出错。

实现原理:

当变量被volatile修饰后,进行编译后会多出一个lock指令,引入了内存屏障,有3方面作用:

(1)当该变量发生改变后,会将当前线程工作内存中的数据立刻回写到系统主内存;

(2)使在其他线程里缓存了该数据的内存地址无效

(3)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

volatile可以用在懒汉双检查单例模式之中

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

这里为什么使用volatile修饰mySingleton呢,主要是使用了其禁止指令重排序的功能。其实 mySingleton = new Singleton()的过程可以分为三步

(1)在堆中开辟对象存储空间

(2)在该存储空间实例化对象、初始化

(3)mySingleton指向为Singleton分配的内存空间

因为1、3都与内存空间相关,所以它们的相对顺序是一定的,所以重排序后可能为1-2-3,或1-3-2,后面这种情况时,当3执行完毕后mySingleton就已经不指向null了,这时候如果又有其他线程来获取单例对象,那么就获取了该引用,但是Singleton对象还没有进行实例化,如果调用了其中的方法,很有可能的就报错了。现在在mySingleton上加了volatile,那么就可以保证在执行3时1和2肯定已经完成,那么就不会出错了。

下面给一下饿汉模式

public class Singleton {
    private static Singleton mySingleton = new Singleton();
    private Singleton(){}
    public static Singleton getMySingleton(){
        return mySingleton;
    }
}
//饿汉模式初始化就创建了对象, 每次调用都返回同一个对象。

//饿汉模式是线程安全的。

volatile与synchronized的区别

1.volatile不会导致线程阻塞,synchronized会导致线程阻塞
2.volatile修饰的变量操作具有可见性但是没有原子性,而synchronnized修饰的方法或代码块中的变量操作即有可见性,又具有原子性,同时又保障多线程访问的顺序性,synchronized的可见性是通过monitorEnter和monitorExit实现的,当 线程获取synchronized中monitor使用权的时候,会执行montorEnter方法,它会使线程重新从主存获取同步方法/代码块 中变量的值,当执行完毕执行montorExit释放锁时,又会将变量值刷入主存之中。synchronized原子性体现在执行synchronized修饰代码段的线程是不可终止的
3.volatile仅可以修饰变量,但是synchronized可以修饰对象、方法
4.volatile标记的变量不会被指令重排序,而synchronized修饰的变量会被编译器优化

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值