当前位置:   article > 正文

入坑JAVA多线程并发(七)volatile到底是什么_java内存模型简介及其避免入坑提示

java内存模型简介及其避免入坑提示

多线程中有三个特性:

1、原子性:不可分割的一个操作,类似于数据库的事物。
如下代码:
> int a,b;
a = 0;      //1
b = a;      //2
a = a+1;     //3
a++;         //4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上面4个语句中只有1是原子性操作,语句2:先取得a的值,再赋给b,语句3:取得a的值,再加1,最好赋给a;语句4:取得a的值,加1,再赋给a。只有简单的读取和赋值是属于原子性的操作。

2、可见性:保证每个线程访问的变量都是最新的,

  java提供volatile来保证可见性,在多线程中,多个线程访问同一个变量,当其中一个变量修改了变量的值,之后其他线程都能知道这个修改,确保取得最新的值,而不是工作内存中的副本。如不理解java内存模式,请参考入坑JAVA多线程并发(三)同步锁synchronized

3、有序性

  代码的执行顺序并不是完全按照代码的先后位置,在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
  java内存模式会根据happens-before保证从排序不会影响到单线程中代码最后的结果。
  java提供volatile来保证一定的有序性。
  如下:

int a = 3;
int b = 4
int c = a + b;

  happens-before会保证语句3会在1和2的后面执行,因为c=a+b依赖a和b的值,但是语句1和语句2并没有任何依赖关系,可能先执行了语句2再执行语句1。
  而volatile就是保证代码的有序性,如果变量a被volatile修饰,那么语句1一定会在2和3之前执行,如果b被volatile修饰,则可以保证语句2一定会在它上面所有的代码执行完才开始执行,而且在语句2执行完之前不会执行语句2后面的语句。

实例
理论说完了,直接上代码:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1("线程1");
        thread1.start();
        Thread.sleep(100);
        thread1.setFlag(false);
    }
}

class Thread1 extends Thread{

    private boolean flag = true;

    public Thread1(String name){
        super(name);
    }

    public void setFlag(boolean flag1){
        flag = flag1;
    }
    @Override
    public void run() {
        while(flag){
            //System.out.println("flag="+flag);
        };
        System.out.println("跳出死循环");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

没有任何输出,虽然flag的值在100ms之后被修改了,但是并没有同步到线程1的工作内存中,导致在线程1中的flag的值一直是true,所以还在死循环中,
这里如果把run方法的注释代码打开可以退出循环,这是因为在访问flag的时候会刷新工作内存的值,检测到flag值为false,退出循环。
如果把flag的值用volatile修饰,就不会出现上面的情况,
修改如下:

class Thread1 extends Thread{

    private volatile boolean flag = true;

    public Thread1(String name){
        super(name);
    }

    public void setFlag(boolean flag1){
        flag = flag1;
    }
    @Override
    public void run() {
        while(flag){

        };
        System.out.println("跳出死循环");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在main线程修改flag的值后会立刻退出循环。

volatile不能保证原子性

volatile使用起来比synchronized简单很多,但是volatile并不能替代synchronized,因为volatile不能保证原子性,
如下:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread1 thread1 = new Thread1("线程1");
        Thread1 thread2 = new Thread1("线程2");
        Thread1 thread3 = new Thread1("线程3");
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(1000);
        System.out.println(Thread1.num);

    }
}

class Thread1 extends Thread{

    public static volatile int num = 0;

    public Thread1(String name){
        super(name);
    }

    @Override
    public   void run() {
        for(int i = 0;i<1000;i++){
            num++;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

正常情况下应该输出3000;但是运行多次输出都是在1000-2000之间,正如上面对于原子性的介绍,num++这个语句不是原子性的,因为是volatile修饰的属性,所以步骤是:
1、从主内存获取num的值
2、对num进行加1操作,赋值给num
3、把numd值同步到主内存
一个线程在步骤1或者2时,有其它线程对num也进行修改,就导致num最终的值小于3000。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/44883
推荐阅读
相关标签
  

闽ICP备14008679号