volatile关键字的使用

  关键字volatile关键字的作用是使变量在多个线程间可见。也就是volatile只能保证可见性,不能保证原子性。所以volatile不具备同步性。

1.一个死循环问题 

   如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在取得程序运行的结果上并没有什么太大的区别。如果一旦出现"多继承"的情况,则用实现Runnable接口的方式来处理多线程问题。

package cn.qlq.thread.five;

public class Demo1 {
    public static void main(String[] args) {
        Sync1 sync1 = new Sync1();
        sync1.print();
        System.out.println("停止循环");
        sync1.setContinue(false);
    }
}

class Sync1 {
    private boolean isContinue = true;

    public boolean isContinue() {
        return isContinue;
    }

    public void setContinue(boolean isContinue) {
        this.isContinue = isContinue;
    }

    public void print() {
        int i = 0;
        while (isContinue) {
            System.out.println(++i);
        }
    }
}

 

 结果:

...

440970
440971
440972
440973

 .......

 

解释:因为main线程一直在死循环,所以不会执行sync1.print();下面的两行代码。解决的办法就是使用多线程技术。

 

2.解决同步死循环

  多线程解决死循环问题。

package cn.qlq.thread.five;

public class Demo2 {
    public static void main(String[] args) {
        final Sync2 sync2 = new Sync2();
        new Thread(new Runnable() {
            @Override
            public void run() {
                sync2.print();
            }
        }).start();
        System.out.println("停止打印");
        sync2.setContinue(false);
    }
}

class Sync2 {
    private boolean isContinue = true;

    public boolean isContinue() {
        return isContinue;
    }

    public void setContinue(boolean isContinue) {
        this.isContinue = isContinue;
    }

    public void print() {
        int i = 0;
        while (isContinue) {
            System.out.println(++i);
        }
    }
}

 

结果:

停止打印
1
2
3

 

3.volatile关键字解决异步死循环

先看一个异步死循环的例子:

package cn.qlq.thread.five;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Sync3 sync3 = new Sync3();
        sync3.start();
        Thread.sleep(100);
        sync3.setContinue(false);
        System.out.println("已经设置为false");
    }
}

class Sync3 extends Thread {
    private boolean isContinue = true;

    public void setContinue(boolean isContinue) {
        this.isContinue = isContinue;
    }

    @Override
    public void run() {
        System.out.println("进入run了");
        while (isContinue == true) {
        }
        System.out.println("退出run了");
    }
}

 结果:(线程不会终止,而且不会打印线程被停止,也就是一直在while循环)

 

 

 原因解释:

  在启动Sync3线程的时候,变量private boolean isContinue = true;;存在于公共堆栈以及线程的私有堆栈中。在JVM中设置为-server模式是为了提高运行效率,线程一直在私有堆栈中取得的isContinue的值是true,虽然执行了sync3.setContinue(false);代码虽然被执行,更新的却是公共堆栈的isContinue的值为false,所以就是一直是死循环的状态。内存结构如下:

  这个问题其实就是私有堆栈中的值和公共堆栈的值不同步的原因造成的。

  

  每个线程在运行过程中都有自己的工作内存,那么线程sync3 在运行的时候,会将isContinue变量的值拷贝一份放在自己的工作内存当中。  

  那么当主线程更改了isContinue变量的值之后,但是还没来得及写入主存当中,主线程转去做其他事情了,那么线程sync3 由于不知道主线程对isContinue变量的更改,因此还会一直循环下去。

 

解决这种问题其实就是加volatile关键字了,它的作用就是当线程访问isContinue这个变量的时候,强制性的从公共堆栈中获取。

    private volatile boolean isContinue = true;

 运行结果:

 

   通过使用volatile关键字,强制的从公共内存中读取变量的值,内存结构如图:

使用volatile关键字增加了实例变量在多个线程之间的可见性。但是volatile最致命的缺点是不支持原子性。

下面是volatile和synchronized关键字的比较:

(1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized性能要好,并且volatile只能修饰变量,而synchronized可以同步方法以及代码块。随着JDK心版本的发布,synchronized关键字在执行效率上大幅提升;

(2)关键字volatile不会发生阻塞,synchronized会出现阻塞。

(3)volatile关键字能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以间接保证数据的可见性,因为它将私有内存和公共内存中的数据做同步。

(4)关键字volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。

  线程包括可见性、原子性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。

 

对于上面更深一步的认识:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量isContinue的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量isContinue的缓存行无效,所以线程1再次读取变量isContinue的值时会去主存读取。

  那么在线程2修改isContinue值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量isContinue的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。

 

并发编程中的三个问题是:

  原子性问题--即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

  可见性问题--当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  有序性问题--程序执行的顺序按照代码的先后顺序执行

 

4. volatile非原子的特性(并不能解决线程非安全问题)

  关键字volatile虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。

package cn.qlq.thread.five;

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable sync4 = new Sync4();
        for (int i = 0; i < 50; i++) {
            new Thread(sync4, "" + i).start();
        }
    }
}

class Sync4 implements Runnable {
    volatile private int count;

    public void run() {
        System.out.println((count++) + "\tthreadName->" + Thread.currentThread().getName());
    }
}

结果:(如下线程非安全问题)

 

 解决办法:(run方法前面加同步)

class Sync4 implements Runnable {
    volatile private int count;

    public synchronized void run() {
        System.out.println((count++) + "\tthreadName->" + Thread.currentThread().getName());
    }
}

  在本实例中加了synchronized关键字,变量count也就没有必要加volatile关键字了。

  关键字volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这一就保证了同步数据的可见性。但是在这里需要注意的是:如果修改实例变量中的数据,比如i++,也就是i=i+1,则这样的操作并不是原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:

  (1)从内存中读取i的值,

  (2)计算i的值

  (3)将i的值写回内存

  假如第二部的时候,另外一个线程也在修改i的值,这个时候就会出现脏读数据。解决的办法就是用synchronized同步。所以说volatile本身并不处理数据的原子性,而是强制将数据的读写及时影响到主内存。

  用图来演示一下volatile线程非安全的原因,变量在内存中工作的过程如图:

      

  由上得出结论:

(1)read和load阶段,从主存复制变量到当前线程工作内存

(2)use和assign阶段:执行代码,改变共享变量值

(3)store和write阶段,用工作内存数据刷新主存对应变量的值。

  在多线程环境中,use和asign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了线程非安全的问题。

  对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到工作内存的值是最新的,例如线程1和线程2在进行read和load操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是volatile关键字解决的是变量读写时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

 

5.使用原子类进行i++操作

   除了在i++时加同步机制之外,还可以使用AtomicInteger类进行实现。

  原子操作是不能分隔的整体,没有其他线程能够中断或者检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全。

例如:原子类实现i++不用加同步

package cn.qlq.thread.five;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable sync4 = new Sync4();
        for (int i = 0; i < 50; i++) {
            new Thread(sync4, "" + i).start();
        }
    }
}

class Sync4 implements Runnable {
    volatile private AtomicInteger count = new AtomicInteger(0);

    public void run() {
        System.out.println(count.getAndIncrement() + "\tthreadName->" + Thread.currentThread().getName());
    }
}

 

6. 原子类也并不完全安全

   原子类在具有逻辑性的情况下输出结果也具有随机性。

package cn.qlq.thread.five;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Sync5 sync4 = new Sync5();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(sync4);
        }
        for (int i = 0; i < 5; i++) {
            threads[i].start();
        }
    }
}

class Sync5 implements Runnable {
    private AtomicInteger count = new AtomicInteger(0);

    public void run() {
        System.out.println("\tthreadName->" + Thread.currentThread().getName() + ",加了100后的值是" + count.addAndGet(100));
        count.addAndGet(1);
    }
}

结果:

threadName->Thread-0,加了100后的值是100
threadName->Thread-2,加了100后的值是300
threadName->Thread-1,加了100后的值是200
threadName->Thread-3,加了100后的值是403
threadName->Thread-4,加了100后的值是503

从结果看出还是发生了线程非安全的问题。因为虽然addAndGet()方法是同步的,但是方法和方法的调用顺序却不是原子的。解决的办法仍然是加同步:

    public synchronized void run() {
        System.out.println("\tthreadName->" + Thread.currentThread().getName() + ",加了100后的值是" + count.addAndGet(100));
        count.addAndGet(1);
    }

结果:

threadName->Thread-0,加了100后的值是100
threadName->Thread-1,加了100后的值是201
threadName->Thread-2,加了100后的值是302
threadName->Thread-4,加了100后的值是403
threadName->Thread-3,加了100后的值是504

 

7.  synchronized代码块具有volatile同步的功能

  关键字synchronized可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。synchronized 可以保证可见性(和volatile 一样),不具备volatile 内存屏障的功能。

一个异步死循环的例子:(与上面3中代码一样)

package cn.qlq.thread.five;

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Sync6 sync3 = new Sync6();
        sync3.start();
        Thread.sleep(100);
        sync3.setContinue(false);
        System.out.println("已经设置为false");
    }
}

class Sync6 extends Thread {
    private boolean isContinue = true;

    public void setContinue(boolean isContinue) {
        this.isContinue = isContinue;
    }

    @Override
    public void run() {
        System.out.println("进入run了");
        while (isContinue == true) {

        }
        System.out.println("线程被停止了");
    }
}

结果:

解决办法:上面3已经测试isContinue加了volatile关键字可以实现私有内存及时更新到主内存。下面测试synchronized同步的功能:

package cn.qlq.thread.five;

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Sync6 sync3 = new Sync6();
        sync3.start();
        Thread.sleep(100);
        sync3.setContinue(false);
        System.out.println("已经设置为false");
    }
}

class Sync6 extends Thread {
    private boolean isContinue = true;

    public void setContinue(boolean isContinue) {
        this.isContinue = isContinue;
    }

    @Override
    public void run() {
        System.out.println("进入run了");
        while (isContinue == true) {
            // 同步代码块
            synchronized ("xx") {
            }
        }
        System.out.println("线程被停止了");
    }
}

 结果:(解决了线程死循环。线程死亡)

 

8. volatile的使用场景

1. 状态的标记符:---例如实现两个线程交替打印一个信息

package cn.qlq.thread.five;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * volatile用作线程标记
 * 
 * @author Administrator
 *
 */
public class Demo7 {
    private volatile static boolean isThreadA;
    private static final Logger LOGGER = LoggerFactory.getLogger(Demo7.class);

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (isThreadA) {
                        LOGGER.info("ThreadName->{}", Thread.currentThread().getName());
                        isThreadA = false;
                    }
                }
            }
        }, "threadA").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (!isThreadA) {
                        LOGGER.info("ThreadName->{}", Thread.currentThread().getName());
                        isThreadA = true;
                    }
                }
            }
        }, "threadB").start();
    }
}

结果:

09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadA
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadB
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadA
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadB
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadA
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadB
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadA
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadB
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadA
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadB
09:35:02 [cn.qlq.thread.five.Demo7]-[INFO] ThreadName->threadA

  有兴趣的可以去掉volatile关键字的声明,则只会打印一次threadB

 

补充:为了复习wait/notify与volatile的知识点。创建20个线程,实现交替打印☆和★符号,也就是实现:下面的效果

    ☆★☆★☆★☆★☆★☆★☆★☆★☆★☆★

package cn.qlq.thread.six;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 交替打印特殊符号
 * 奇数线程打印☆,偶数线程打印★
 * 
 * @author Administrator
 *
 */
public class Demo11 {
    private static final Logger LOGGER = LoggerFactory.getLogger(Demo11.class);

    public volatile static boolean isOddThread = true; // 标记是不是奇数线程

    public static void main(String[] args) throws InterruptedException {
        final Object lock = new Object();
        
        //10个奇数线程打印☆
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        synchronized (lock) {
                            while (!isOddThread) {
                                lock.wait();
                            }
                            LOGGER.info("ThreadName->{}, print ->{}",Thread.currentThread().getName(),"☆");
                            isOddThread  = false;
                            lock.notifyAll();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "odd" + i).start();
        }
        
        //10个偶数线程打印★
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        synchronized (lock) {
                            while (isOddThread) {
                                lock.wait();
                            }
                            LOGGER.info("ThreadName->{}, print ->{}",Thread.currentThread().getName(),"★");
                            isOddThread  = true;
                            lock.notifyAll();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "even" + i).start();
        }

    }
}

 

2.单例模式的双重检查(double-check)

package cn.qlq.thread.five;

/**
 * volatile用作单例的双重检查
 * 
 * @author Administrator
 *
 */
public class Singleton {

    private volatile static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        // first check
        if (instance == null) {
            synchronized (Singleton.class) {
                // second check
                if (instance == null) {
                    instance = new Singleton(); // new
                }
            }
        }
        return instance;
    }

} 

如果不用volatile修饰,可能会返回一个未完全初始化的示例 ,问题出在第二层检查的 : new Singleton(); //new
  一般来讲,当初始化一个对象的时候,会经历内存分配、初始化、返回对象在堆上的引用等一系列操作,这种方式产生的对象是一个完整的对象,可以正常使用。但是JAVA的无序写入可能会造成顺序的颠倒,即内存分配、返回对象引用、初始化的顺序,这种情况下对应到 //new 就是singleton已经不是null,而是指向了堆上的一个对象,但是该对象却还没有完成初始化动作。当后续的线程发现singleton不是null而直接使用的时候,就会出现意料之外的问题。

  volatile关键字修饰变量来解决无序写入产生的问题,因为volatile关键字的一个重要作用是禁止指令重排序,即保证不会出现内存分配、返回对象引用、初始化这样的顺序,从而使得双重检测真正发挥作用。

 

总结:

   关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或者某一个代码块。它包含两个特性:互斥性和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。

 

  实际上关键字volatile修饰变量的作用就是保证变量在修改之后在多个线程之间可以立即可见: 它会强制将线程对缓存的修改操作立即写入主存; 而且线程中使用变量的时候从主存中读取变量。这样就保证了变量在线程之间的可见性。

 

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

  volatile关键字禁止指令重排序有两层意思:

  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

 

补充: 利用IDEA的jclasslib 插件查看new 对象的字节码

package com.xm.ggn.test;

public class PlainTest2 {

    public static Object obj = null;

    public static void main(String[] args) throws Exception {
        obj = new Object();
    }

}

对于上面代码,查看到的字节码信息如下:

 解释如下:

 0 new #2 <java/lang/Object>     // 在堆中创建一个空对象,并将其引用压入栈顶
 3 dup    // 复制栈顶数据并且复制值压入栈顶(栈顶有两个引用)
 4 invokespecial #1 <java/lang/Object.<init> : ()V>    // 需要this指针,执行类的默认构造方法,初始化普通属性
 7 putstatic #3 <com/xm/ggn/test/PlainTest2.obj : Ljava/lang/Object;>    // 为指定的静态变量赋值,堆中已经初始化完成的对象的指针pop出来赋值给instance
10 return

  乱序执行可能会变成 0 -》3-》7-》4, 导致的结果就是有的对象拿到的是没有初始化完成的对象。

  这个插件也可以看到常量池中的一些信息以及魔数等信息

 

补充:volatile 如何解决运行期的指令重排

CPU写内存的方式:

(1)同步:CPU把数值写到store buffer, 然后写入内存

(2)异步:CPU把数值写入store buffer, 等CPU空闲了再把BUFFER的数值回写到内存

  因为异步写的存在,才设计了内存屏障。加了内存屏障,读写操作只能按序执行,CPU无法乱序执行。最终是为了保证这个异步刷回内存的时间更短。

CPU提供的屏障:

1》fence簇:mfence、lfence、sfence

2》lock 指令:会锁住地址总线

  这里需要注意指令重排是发生在运行期,编译期间加volatile 和 不加volatile 产生的字节码指令相同

(1) happens-before 原则

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。换句话说,操作1 happens-before 操作2,那么操作1的结果是对操作2可见的。

(2) 内存屏障

》编译阶段: 编译屏障,保证操作的是内存

》运行阶段:内存屏障,发生在CPU内部。 实际volatile 是c++ 层面的volatile,禁止编译器优化无效代码, 使用的是lock

(3)as-if-serial    是个语义,规范

  CPU为了提高效率,乱序执行,但是不管如何乱序不能影响执行结果。在单线程环境下不能改变程序执行的结果;存在数据依赖关系代码(指令)片段的不允许重排序。

补充:关于内存的几个名词

操作系统层面:操作系统内存、本地内存、native memory、物理内存

JVM层面:堆、方法区、栈、本地方法栈、PC   (可以理解为物理内存的一小块区域,JVM启动的时候向操作系统申请的内存)

Java内存模型层面:主内存(可以理解为堆+方法区)、工作内存(可以理解为虚拟机栈)  

 

posted @ 2018-12-10 18:50  QiaoZhi  阅读(551)  评论(0编辑  收藏  举报