java中final与volatile-线程安全问题

在线程安全问题中final主要体现在安全发布问题上,在这里先讲一下什么事安全发布,在《java并发编程实践》一书中有讲,不过看起来挺难懂的….

    public class Holder {
        private int n;
        public Holder(int n) { this.n = n; }
        public void assertSanity() {
            if (n != n)
                throw new AssertionError("error");
        }
    }

假设这里有一个线程A执行了下面一段代码

Holder holder = new Holder(10);

同时有另一个线程也在执行下面这段代码

if (holder != null) holder.assertSanity();

那么在某些情况下就会抛出上面的异常,原因就是:
Holder holder = new Holder(10);其实是由三步组成的

  1. 给holder分配内存
  2. 调用构造函数
  3. 将holder指向刚分配的内存

理想中是这个执行顺序,然而事实上这三步并不一定按照这个顺序执行,是为了优化效率而存在的指令重排在作怪,假如一个执行顺序为1 3 2,那么在刚执行完1和3的时候线程切换到B,这时候holder由于指向了内存所以不为空并调用assertSanity函数,该函数中的if语句并不是一步完成:

  1. 取左表达式n的值
  2. 取右表达式n的值
  3. 进行!=表达式运算

那么假设刚执行完第一步的时候B线程挂起并重新回到A线程,A线程继续执行构造函数并将n赋值为10,然后再次跳回B线程,这时候执行第2步,那么就会造成前后取到的n不一样,从而抛出异常。
那么加了final修饰之后会如何呢,JVM做了如下保证

一旦对象引用对其他线程可见,则其final成员也必须正确的赋值了。

就是说一旦你得到了引用,final域的值(即n)都是完成了初始化的,因此不会再抛出上面的异常。另外高并发环境下还是多用final吧!

再来看一下volatile关键字,这个关键字有两层意义,1.保证可见性 2.阻止代码重排。
先看第一条,这个问题我到现在还是有疑问,这一条的意思是说一个线程修改了一个被volatile修饰的变量的值,这新值对其他线程来说是立即可见的。可是在网上找了好久也没有说到底是如何实现立即可见的,先来看java内存模型都定义了哪些操作:

  1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  2. unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

    其中当要修改主内存中的值时,要先复制到工作内存(高速缓存)中,然后修改工作内存,然后复制回工作内存,由于需要store和write两步才能将值写回主内存,所以对于普通变量来说有可能刚执行完store就被切换线程了,也就是说操作完了但是主内存却没变,因此可能出现问题,也就是不可见性,而volatile避免了这种不确定性(注意volatile还有个作用是让所有该变量的缓存无效,即在读这个变量时一定要去主存读),我的理解就是强制将这两步绑定到了一起,也就是store完之后必须马上write,不许干别的
    在happens-before原则中有一条是关于volatile的:
    volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作 也大体让我坚信了这一点,不知道这个理解是否正确。
    然后很经典的一段代码:

//线程1
boolean stop = false;
while(!stop) {
    doSomething();
}

//线程2
stop = true;

网上很多说出现死循环是因为

线程2更改了stop变量的值之后,没来得及写入主存当中,线程2被切换了,线程1由于不知道线程2stop变量的更改,因此死循环。

我是死活没明白为什么会死循环,就算是没来得及写入主存,那总会有重新切回线程2的那时候,然后继续把stop写回主存,也根本不会出现死循环吧。。
我认为正确的是JVM在某种情况下会将线程1的代码优化成如下代码:

boolean stop = false;
if (!stop) {
    while(!true) {
        doSomething();
    }
}

那么这种情况下确实很容易出现死循环,而且这种优化在JVM开启了server模式才会出现,而加了volatile之后就不会出现的原因应该就是阻止了代码重排,也就是阻止了这种优化。下面来说一下volatile阻止代码重排。
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
主要体现在:

i = 0;  // 1
j = 1;  // 2
flag = true;  // volatile修饰
x = 10;  // 3
y = "111";  //4

那么这段代码flag上下的代码不能互相越界,但是1和2,3和4仍然可以交换顺序。
还有个很经典的问题,看代码:

    public volatile int i = 0;
    for (int i = 0; i < 10; ++i) {
        new Thread(() -> {
            for (int j = 0; j < 100000; ++j) i++;
        }).start();
    }

这段代码i最终会小于1e6,原因是i++没有原子性,因为i++由三步组成:

  1. 读取i的值
  2. 计算i+1
  3. 重新赋值给i
    然后一中顺序就是
    1.线程A读取i的值(假设为1)
    2.线程切换到B,线程B取i的值,计算i+1=2,赋值给i=2
    3.线程切换回A,A计算i+1=2,赋值给i=2
    也就是只加了1,那么这里就会有人有疑问了,不是说violate关键字会让i对其他线程可见并且让i的缓存无效吗,那为何第3步的时候线程A计算i+1还是等于2,这里的原因就是,实际上i++的真正操作是这样子的:
    1. 取i的值到栈顶
    2. 将栈顶元素+1
    3. 赋值回i

因此这里的第二步并没有访问i,因此也就看不到i的更新了。
/*******************************/
更新一波,面试的时候面试官对指令重排这一部分产生了质疑,今天做了个实验,发现并没有出现所谓的指令重排,我现在是彻底懵逼了。。。。。上代码:

private static int a = 0;
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000000; i++) {
            ThreadA threadA = new ThreadA();
            ThreadB threadB = new ThreadB();
            threadA.start();
            threadB.start();

            threadA.join();
            threadB.join();
            a = 0;
            flag = false;
        }
    }

    static class ThreadA extends Thread {
        public void run() {
            a = 1;
            flag = true;
        }
    }

    static class ThreadB extends Thread {
        public void run() {
            if (flag) {
                if (a == 0) System.out.println("a == 0!!");
            }
        }
    }

**

如果文章写得有问题,请一定要指正!!

**

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值