在线程安全问题中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 3 2,那么在刚执行完1和3的时候线程切换到B,这时候holder由于指向了内存所以不为空并调用assertSanity函数,该函数中的if语句并不是一步完成:
那么假设刚执行完第一步的时候B线程挂起并重新回到A线程,A线程继续执行构造函数并将n赋值为10,然后再次跳回B线程,这时候执行第2步,那么就会造成前后取到的n不一样,从而抛出异常。
那么加了final修饰之后会如何呢,JVM做了如下保证
一旦对象引用对其他线程可见,则其final成员也必须正确的赋值了。
就是说一旦你得到了引用,final域的值(即n)都是完成了初始化的,因此不会再抛出上面的异常。另外高并发环境下还是多用final吧!
再来看一下volatile关键字,这个关键字有两层意义,1.保证可见性 2.阻止代码重排。
先看第一条,这个问题我到现在还是有疑问,这一条的意思是说一个线程修改了一个被volatile修饰的变量的值,这新值对其他线程来说是立即可见的。可是在网上找了好久也没有说到底是如何实现立即可见的,先来看java内存模型都定义了哪些操作:
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由于不知道线程2对stop变量的更改,因此死循环。
我是死活没明白为什么会死循环,就算是没来得及写入主存,那总会有重新切回线程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++由三步组成:
因此这里的第二步并没有访问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!!");
}
}
}
**
**