Java多线程专题-volatile的使用

什么是volatile

Volatile关键字的作用是保证被修饰的变量在多个线程之间的可见性。并不保证线程安全

先来看下面这个例子

class ThreadVolatileDemo extends Thread {
    public boolean flag = true;
    @Override
    public void run() {
        System.out.println("开始执行子线程....");
        while (flag) {
        }
        System.out.println("线程停止");
    }
    public void setRuning(boolean flag) {
        this.flag = flag;
    }

}

public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
        threadVolatileDemo.start();
        Thread.sleep(3000);
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已经设置成false");
        Thread.sleep(1000);
        System.out.println(threadVolatileDemo.flag);
    }
}

上面代码中setRuning(false)并不能使线程停止,原因是线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。解决办法使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值。

当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
volatile变量不会被缓存在期存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

--- 摘自 《java编发编程实战》

 变量的可见性是我们期待的结果,但是多线程操作共享变量时,却并非如此。由于处理器将变量存放至缓存行中进行读取,这里的数据对于其他处理器不可见,所以其他处理器上的线程就有可能读取到旧的数据,即失效数据。为了避免读取到失效数据,应该在多线程共享数据时,使用同步机制来保证变量的可见性。

即时线程读取了失效数据,这个数据也是其他线程在某个时间节点修改的结果,是有据可循的,被称为最低安全性。Java内存模型中要求变量的读取和写入必须是原子性,但是对于double和long变量的读写操作会被分解为两个32位操作,当这种读写操作在不同线程中执行时,可能造成数据混乱。所以对于long和double的共享变量必须使用volatile保护起来。

当操作共享变量的所有线程在同一个锁上进行同步操作时,也能保证变量的可见性。所以,加锁的含义不仅仅局限于互斥行为,还包含内存可见性,但必须在同一把锁上。 

Volatile非原子性

注意: Volatile非原子性,volatile也不能保证线程安全,也不能保证数据的原子性。

public class VolatileNoAtomic extends Thread {
    private static volatile int count;

    // private static AtomicInteger count = new AtomicInteger(0);
    private static void addCount() {
        for (int i = 0; i < 1000; i++) {
            count++;
            // count.incrementAndGet();
        }
        System.out.println(count);
    }

    public void run() {
        addCount();
    }

    public static void main(String[] args) {

        VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
        for (int i = 0; i < 10; i++) {
            arr[i] = new VolatileNoAtomic();
        }

        for (int i = 0; i < 10; i++) {
            arr[i].start();
        }
    }
}

运行结果如下:

结果发现 数据不同步,因为Volatile不用具备原子性。

使用AtomicInteger原子类

volatile只能保证变量的修改对其他线程可见,不能保证原子性,AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减。

public class VolatileNoAtomic extends Thread {
    static int count = 0;
    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            //等同于i++
            atomicInteger.incrementAndGet();
        }
        System.out.println(count);
    }

    public static void main(String[] args) {
        // 初始化10个线程
        VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
        for (int i = 0; i < 10; i++) {
            // 创建
            volatileNoAtomic[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < volatileNoAtomic.length; i++) {
            volatileNoAtomic[i].start();
        }
    }
}

volatile与synchronized区别

仅靠volatile不能保证线程的安全性。(原子性)

  1. volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
  2. volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
  3. synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

线程安全性

  1. 线程安全性包括两个方面,可见性。原子性。
  2. 从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。

 

 

 

©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页