一文了解volatile高并发关键字

声明:尊重他人劳动成果,转载请附带原文链接!学习交流,仅供参考!

1、volatile是什么?

1、volatile是一种同步机制,和synchroized、Lock两者之比,更加轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
2、如果一个变量被volatile修饰,那么意味着JVM就知道这个变量可能会被并发修改
3、volatile开销很小,对应就会功能也会很小,虽然volatile是用来同步的保证线程安全的,但是volatile却做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。

2、volatile适用于那些场合?

2.1 不适合场景 a++(递增或者递减,不具有原子性)

public class Demo implements Runnable {
    
    
    volatile int a;
    public static void main(String[] args) throws InterruptedException {
    
    
        Demo demo = new Demo();
        // 两个线程对一个共享变量进行并发修改
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
         System.out.println("a="+demo.a);
    }
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100000; i++) {
    
    
            a++;
        }
    }
}

结果:

a=142019

假如我们先不看结果的话,我们理想的情况应该是a=200000,而结果怎么却少了这么多次?
有人可能会说,有可能是由于程序只执行了142019次吧,为了验证这一说法,这里我利用了 AtomicInteger类对代码做了改变。

public class Demo implements Runnable {
    
    
    AtomicInteger readA = new AtomicInteger();
    volatile int a;
    public static void main(String[] args) throws InterruptedException {
    
    
        Demo demo = new Demo();
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a=" + demo.a);
        System.out.println("readA="+demo.readA.get());
    }
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100000; i++) {
    
    
            a++;
            readA.incrementAndGet();
        }
    }
}

结果:

a=193825
readA=200000

看到结果,程序确实运行了200000次,可是为什么a++却少了这么多次呢?

  因为我们都知道a++,实际上,它包含了三个独立的操作,读取a的值,将值加1,然后将计算结果写入a,这是一个"读取-修改-写入"的操作序列,如果出现在两个线程在没有同步的情况下,同时对一个计数器执行递增操作,就会出现数值偏差。

2.2 适合场景

2.2.1 赋值操作

  如果一个共享变量自始至终只是被各个线程赋值,而没有其他操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证可见性,所以就足以保证线程安全。
例如:两个线程对共享变量a进行赋值

public class Demo {
    
    
    volatile int a;
    public void change1() {
    
    
        a = 1;
        System.out.println(a);
    }
    public void change2() {
    
    
        a = 2;
        System.out.println(a);
    }
    public static void main(String[] args) throws InterruptedException {
    
    
        Demo demo = new Demo();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                demo.change1();
            }
        }).start();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                demo.change2();
            }
        }).start();
    }
}
2.2.2 触发器

  可能大家对触发器不陌生,在MySql里面都是听过的,但这里说的触发器是用作刷新之前变量的触发器。
如下的一个伪代码中, 也说明了volatile作为触发器的场景.
线程A在执行完了一系列的配置操作后, 给volatile修饰的变量initialized赋值了true.
线程B在判断initialized 如果为false则会一直休眠, 直到initialized为true,才会走下面的代码, 并且使用线程初始化的一些配置. 此时 volatile修饰的变量initialized 作为了触发器.
在这里插入图片描述

3、volatile的两点作用?

1、可见性:每个线程读一个volatile变量之前,需要先使相应的本地缓存失效,然后从主内存中去读取变量最新的值,然后拷贝到独自的工作空间,修改完变量的值后,会立即刷入到主内存中。
2、禁止指令重排序优化:虽然重排序有好处,但是禁止指令重排序可以解决单例双重锁乱序问题。

4、volatile总结

volatile 在这方面可以看做是轻量版的synchronized,假如一个共享变量自始至终只是被各个线程进行赋值,而没有其他操作,那么就可以用volatile替代synchronized或者代替原子变量,因为赋值本身具有原子性,而volatile又保证可见性,所以就足以保证线程安全。

volatile属性的属性读写操作(如果只是多个线程对一个共享变量赋值,这条可以忽略)都是无锁操作的,它不能代替synchronized,因为它不具有原子性和互斥性。正因为没有锁,就不用花费时间在加锁和释放锁上,所以说它是低成本的。

volatile只能用在属性,如果用volatile修饰属性,这样编译器就不会对这个属性进行指令重排序。

volatile也具有可见性(任何一个线程对一个共享变量修改了值,其他线程都能立即看到),也提供了happens-before(前一个写操作执行完后,后一个读操作都能全部知道你写操作干了什么)保证。

おすすめ

転載: blog.csdn.net/qq_40805639/article/details/121012056