Volatile的学习

2022年3月17日 3点热度 0条评论 来源: 大队长11

首先先介绍三个性质

可见性

可见性代表主内存中变量更新,线程中可以及时获得最新的值。

下面例子证明了线程中可见性的问题

由于发现多次执行都要到主内存中取变量,所以会将变量缓存到线程的工作内存,这样当其他线程更新该变量的时候,该线程无法得知,导致该线程会无限的运行下去。

public class test1 {
    private static int flag = 1;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while (flag == 1){

            }
        },"t1");
        t1.start();
        Thread.sleep(1000);
        flag = 2;
    }
}

疑问

当我们在这个死循环中加入一个synchronized关键字的话就会将更新

猜测:synchronized会使更新当前线程的工作内存

public class test1 {
    private static int flag = 1;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while (flag == 1){
                synchronized ("1"){
                    
                }
            }
        },"t1");
        t1.start();
        Thread.sleep(1000);
        flag = 2;
    }
}

原子性

即多线程中指令执行会出现交错,导致数据读取错误。

比如i++的操作就可以在字节码的层面可以被看成以下操作

9 getstatic #9 <com/zhf/test3/test2.i : I>   获得i
12 iconst_1    将1压入操作数栈
13 isub   将两数相减
14 putstatic #9 <com/zhf/test3/test2.i : I>  将i变量存储

然后在多线程的情况下,会出现以下程序出现非0的结果。

public class test2 {

    private static int i;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int j = 0; j < 400; j++) {
                i++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int j = 0; j < 400; j++) {
                i--;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

设计模式---两阶段停止使用volatile

@Slf4j
public class test3 {

    private Thread monitor;
    private volatile boolean flag = false;

    public static void main(String[] args) {
        test3 test3 = new test3();

        test3.monitor = new Thread(()->{
            while (true){
                Thread thread = Thread.currentThread();
                if (test3.flag){
                    log.debug("正在执行后续");
                    break;
                }
                try {
                    log.debug("线程正在执行");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 当进程在睡眠过程中被Interrupte()打断此时isInterrupted()为false
                    // 从而当异常被抓住后会继续执行
                    // 所以要调用下面方法继续将isInterrupted()置为true
                    // thread.interrupt();
                }
            }
        });

        test3.start();
        try {
            Thread.sleep(5500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        test3.stop();
    }
    public void stop(){
        flag = true;
        monitor.interrupt();
    }

    public void start(){
        monitor.start();
    }
}

设计模式---犹豫模式

具体实现,最常见的就是单例模式。

首先是饿汉模式,这里的多线程安全是由JVM保证的,对象是在类加载的加载阶段创建的。

class SingtonHungry{
    private static Object object = new Object();

    // 饿汉模式
    public synchronized Object getObject() {
        return object;
    }
}

其次就是饿汉模式,最常见的不过就是下面的进行多线程安全的方案。文章后面会对其进行优化。

class SingtonLazy{
    private Object object;

    // 懒汉模式
    // 由于这样的话不管有没有创建出对象都要加锁然后才能取对象,性能太差
    public synchronized Object getObject() {
        if (object == null){
            object = new Object();
            return object;
        }
        return object;
    }
}

有序性

JVM会对指令进行重排序,其和CPU的流水线操作类似,当需要流水线操作的时候,需要进行优化的时候,就会对CPU指令进行重排序优化。

当操作的顺序变了之后,就会出现问题。可能会导致条件的提前触发等等。

Volatile使用

使用域: Volatile只能在类的静态成员变量或者成员变量上。

volatile标识符能够让线程强制去读主存的该变量的值,保证了线程变量的可见性。

volatile标识符能够让线程去顺序执行该变量的操作,保证了执行变量的语句的有序性

  • 在读取该变量时,会为其添加读屏障。在该读屏障之后的代码不会放在读屏障之前执行。

  • 在写该变量时,会为其添加写屏障。在该写屏障之前的代码不会在屏障之后执行。

所以在volatile的修饰下,能够保证变量的可见性和有序性,但并不能保证其的原子性。

class SingtonLazy{
    // 加上volatile的主要目的就是防止在synchronized内的代码指令重排,正常是先构造好对象然后赋对象地址
    // 导致object会被首先赋予了地址,导致其不为null,然而构造方法还没有开始构造
    // 被其他的线程拿走会出现使用出错。
    private static volatile Object object;

    // 懒汉模式
    public static  Object getObject() {
        if (object != null){
            return object;
        }else{
            // 这里可能出现这里的线程还没有为其进行声明对象,但已经由线程进入了等待锁
            // 所以需要在这里来一个为空判断。
            synchronized (SingtonLazy.class){
                // 这里可能会出现指令重排,所以要加上volatile
                if(object == null){
                    object = new Object();
                }
                return object;
            }
        }
    }
}

实现单例的另外一个方式

public class Singleton {
	// 当使用到ObjectHolder才会进行到这个静态内部类的加载,同时才会创建该类
    // 也是属于懒汉式
    private static class ObjectHolder{
        static final Singleton singleton = new Singleton();
    }

}

synchronized补充

首先在synchronized代码块中,它会保证代码块中的可见性,原子性和有序性。

有序性仅仅是表现在synchronized的执行后最后的结果都是一样的,并不会阻止JVM在其内部进行代码的重排序。就比如上个例子来说,在synchronized代码块中最后代码的执行结果都是一样的,但可能由于其优化,导致其他线程出错。

    原文作者:大队长11
    原文地址: https://www.cnblogs.com/duizhangz/p/16243965.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。