• 线程同步Volatile与Synchronized(一)


    volatile

    一、volatile修饰的变量具有内存可见性

    volatile是变量修饰符,其修饰的变量具有内存可见性。

    可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。

    在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存

    实例讲解:

    class FlagThread extends Thread{
        private boolean flag = false;
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
    
            System.out.println("flag : " + flag);
        }
        public boolean isFlag() {
            return flag;
        }
    }
    public class TestVolatile {
        public static void main(String[] args) {
            FlagThread flagThread = new FlagThread();
    
            flagThread.start();
            while (true){
                if (flagThread.isFlag()){
                    System.out.println("------------------------");
                    break;
                }
            }
        }
    }
    

    程序输出:(main线程结束不了)

    问题分析:

    在操作共享数据的时候,系统会将共享数据放置在主存中。

    流程分析:

    步骤一:

    线程1:读取到的主存数据,flag = true;

    main线程:读取到的主存数据,flag = true;

    步骤二:

    线程1:修改自身线程缓存空间的值,令flag = false;并将其更新到主存中,此时主存中的flag = false.

    但是,main线程中的值,仍然是,flag = false,并一直循环下去。

    因此,main线程始终在循环中,无法检测到falg已经变化的值。

    二、volatile禁止指令重排

    volatile可以禁止进行指令重排。

    指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

    程序执行到volatile修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

    volatile的使用举例

    //线程1:
    context = loadContext();   //语句1  context初始化操作
    inited = true;             //语句2
     
    //线程2:
    while(!inited ){
      sleep()
    }
    doSomethingwithconfig(context);
    

    因为指令重排序,有可能语句2会在语句1之前执行,可能导致context还没被初始化,而线程2中,

    // 此处判断为false,则不会进入循环
    while(!inited ){ 
      sleep()
    }
    

    因而,线程2使用未初始化的context去进行操作,导致程序出错。

    这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,这是因为volatile禁止指令重排:程序执行到volatile修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

    synchronized

    三、synchronized

    synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。

    可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中

    原子性表现在:要么不执行,要么执行到底。

    实例讲解:必须使用synchronized而不能使用volatile的场景

    public class Test {
        public volatile int inc = 0;
        public void increase() {
            inc++;
        }   
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }
    

    程序分析:

    结果:例子中用new了10个线程,分别去调用1000次increase()方法,每次运行结果都不一致,都是一个小于10000的数字。

    问题分析:自增操作不是原子操作,volatile 是不能保证原子性的。回到文章一开始的例子,使用volatile修饰int型变量i,多个线程同时进行i++操作。比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了。同理可以解释为什么每次运行结果都是小于10000的数字。

    但是使用synchronized对部分代码进行如下修改,就能保证同一时刻只有一个线程获取锁然后执行同步代码。运行结果必然是10000。

    public  int inc = 0;
    public synchronized void increase() {
            inc++;
    }
    

    内存可见性

    在Java中,我们都知道关键字synchronized可以用于实现线程间的互斥,但我们却常常忘记了它还有另外一个作用,那就是确保变量在内存的可见性 - 即当读写两个线程同时访问同一个变量时,synchronized用于确保写线程更新变量后,读线程再访问该 变量时可以读取到该变量最新的值。

    即当ThreadA释放锁M时,它所写过的变量(比如,x和y,存在它工作内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工作内存会被设置为无效,然后ThreadB会重新从主存中加载它要访问的变量到它的工作内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。通过这样的方式来实现ThreadA到ThreadB的线程间的通信。

    //线程A,B共同访问的代码
    Object lock = new Object();
    int a=0;
    int b=0;
    int c=0;
     
    //线程A,调用如下代码
    synchronized(lock){
        a=1; //1
        b=2;  //2
    } //3
    c=3; //4
     
     
    //线程B,调用如下代码
    synchronized(lock){  //5
        System.out.println(a);  //6
        System.out.println(b);  //7
        System.out.println(c);  //8
    }
    
    

    我们假设线程A先运行,分别给a,b,c三个变量进行赋值(注:变量a,b的赋值是在同步语句块中进行的),然后线程B再运行,分别读取出这三个变量的值并打印出来。那么线程B打印出来的变量a,b,c的值分别是多少?

    输出结果:
    线程B里,打印的a,b肯定是1和2. 但是,访问的到c变量有可能还是0,而不是3.

    四、总结

    (1)从而我们可以看出volatile虽然具有可见性但是并不能保证原子性。

    (2)性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。

    但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。

    参考资料:

    Java并发——线程同步Volatile与Synchronized详解

  • 相关阅读:
    hdu_5791_Two(DP)
    hdu_5783_Divide the Sequence(贪心)
    hdu_5769_Substring(后缀数组)
    hdu_5778_abs(暴力)
    hdu_5776_sum(前缀和维护)
    hdu_5777_domino(贪心)
    [wikioi2069]油画(贪心)
    [bzoj 1503][NOI 2004]郁闷的出纳员(平衡树)
    数据结构练习
    [poj3274]排排站(Hash)
  • 原文地址:https://www.cnblogs.com/ch-forever/p/10207432.html
Copyright © 2020-2023  润新知