多线程---volatile关键字

在多线程中,volatile关键字是很重要的一个知识点,在多线程共享资源的时候,每个线程数据对外都是不可见的,这就容易出现”脏读”现象,其实就是线程私有堆栈中的数据和公共堆栈中的数据不同步造成的.解决这样的问题,就要使用volatile关键字了。

  • 内存结构
    image_1b4ik6j497iaodp1oi066g1hsu13.png-19.5kB
    有这个结构图,就能很清晰的知道为毛会造成数据的不同步了。每个线程都会有各自的线程栈,执行运算的时候,是从公共堆栈读取数据到线程堆栈中,线程操作的是线程堆栈的数据,结束后,再从线程堆栈回刷到公共堆栈,所以这种肯定会引起数据的不同步。

那valotitle关键字有什么用,他是强制线程从公共堆栈中读取变量的值,以保证读取的是最新的,增加了实例变量的可见性。
image_1b4ik5h0q1stj1g41s643evcgfm.png-28.9kB
来个demo验证下

public class MyThread extends Thread {
    private boolean isRun= true;

    @Override
    public void run() {
        System.out.println("run start");
        while(isRun){

        }
        System.out.println("run end");
    }

    public void setRun(boolean isRun) {
        this.isRun = isRun;
    }

}

    public static void main(String[] args) throws Exception {
        MyThread r = new MyThread();
        r.start();
        Thread.sleep(2000);
        r.setRun(false);

    }

打印结果:
image_1b4iknqbc1h35nd21fd4rhpr8n1g.png-11.5kB
线程一直处于运转状态,run end无法执行到;
修改一下假如volatitle关键字

public class MyThread extends Thread {
    volatile    private boolean isRun= true;

    @Override
    public void run() {
        System.out.println("run start");
        while(isRun){

        }
        System.out.println("run end");
    }

    public void setRun(boolean isRun) {
        this.isRun = isRun;
    }

}

结果:
image_1b4iks8orjomogbe3si201obn2d.png-7.1kB
完全正确

但是volatile只解决了线程的可见性,并不能保证原子性

  • 非原子性
    来个demo验证:
public class MyThread extends Thread {
    volatile public static int count;
    @Override
    public void run() {
        add();
    }

    private void add() {
        for(int i=0;i<100;i++){
            count++;
        }
        System.out.println("count="+count);
    }
}


public static void main(String[] args) throws Exception {
        MyThread[] rs = new MyThread[100];
        for(int i=0;i<rs.length;i++){
            rs[i] = new MyThread();
        }

        for(MyThread r:rs){
            r.start();
        }

    }

打印结果:
image_1b4ilia3blqfms8173kkbk1on22q.png-9.6kB
不等10000,那怎么改?只需要在add方法加synchronized锁,将add变为静态方法即可

    synchronized private static void add() {
        for(int i=0;i<100;i++){
            count++;
        }
        System.out.println("count="+count);
    }

结果:
image_1b4iltsve1o9jdunj651qg2t3837.png-6.2kB

关键字volatilt提示线程每次从共享内存中读取变量,而不是从私有内存中读取变量,保证了同步数据的可见性。但是这里需要注意的而是,修改实例变量中的数据,如i++,这样的操作其实不是一个原子操作,也就是非线程安全,表达式i++的操作分解步骤如下:
1)从内存中取i的值
2)计算i的值
3)将i的值写到内存中。
来看一下变量在内存中的工作过程:
image_1b4im999hqoe1qbj9ab1i3k1p0p3k.png-66.2kB
在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和工作内存的变量不同步,所以计算出来的结果和预期不一样,也就是非线程安全问题。
对于volatile修饰的变量,JVM只是保证从主内存加载到工作内存中的值是最新的,例如线程1和2在进行read和load的操作中,发现主内存中的count的值都是5,那么都会加载这个最新的值,也就是说volatile关键字解决的是变量读时的可见性问题,但不保证原子性,对于多个线程访问同一个实例变量还是需要加同步锁。

  • 原子类进行操作
    原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量,一个原子类型(Atomicxxx)就是一个原子操作可用的类型,他可以在没有锁的情况下做到线程安全。
public class MyThread extends Thread {
    private AtomicInteger atomic = new AtomicInteger(0);

    @Override
    public void run() {
        add();
    }

     private  void add() {
        for(int i=0;i<100;i++){
            System.out.println("count="+atomic.incrementAndGet());
        }

    }
}

public static void main(String[] args) throws Exception {
        MyThread r = new MyThread();
        Thread[] rs = new Thread[100];
        for(int i=0;i<rs.length;i++){
            rs[i] = new Thread(r);
        }

        for(Thread m:rs){
            m.start();
        }

    }

image_1b4inf5tfsb21stna21l7rc7d41.png-9kB
成功累加到10000

  • 原子类也并不完全安全
public class MyThread extends Thread {
    public static AtomicInteger atomic = new AtomicInteger(0);

    @Override
    public void run() {
        add();
    }

     private  void add() {
            System.out.println("count="+atomic.addAndGet(100));
            atomic.addAndGet(1);
    }

}

public static void main(String[] args) throws Exception {
        MyThread r = new MyThread();
        Thread[] rs = new Thread[5];
        for(int i=0;i<rs.length;i++){
            rs[i] = new Thread(r);
        }

        for(Thread m:rs){
            m.start();
        }

        Thread.sleep(2000);
        System.out.println(MyThread.atomic.get());

    }

结果的一种可能:
image_1b4inroaieu814gdo0716sub2f4e.png-4kB
虽然最终结果是对的,但是打印明显不符合预期,+1这个明显没有打印出来。虽然add方法是原子操作,但是调用方法却不是原子的,解决这种问题,必须用同步。
改进后的

public class MyThread extends Thread {
    public static AtomicInteger atomic = new AtomicInteger(0);

    @Override
    public void run() {
        add();
    }

    synchronized private  void add() {
            System.out.println("count="+atomic.addAndGet(100));
            atomic.addAndGet(1);
    }

}

打印结果:
image_1b4io1pv61fm5gn1tpa1gjh1me34r.png-6.5kB

猜你喜欢

转载自blog.csdn.net/baidu_17508977/article/details/53815687