半个读书笔记,没什么含量。
JMM:
JMM围绕在并发过程中如何处理原子性,可见性和有序性三个特征来建立。
java内存模型JMM与实际硬件模型有比较类似的地方:
内存 => 高速缓存 => cpu
主内存 => 工作内存 => 线程
由于I/O速度与cpu速度差距过大,所以在二者之间加入一层与cpu速度相对接近的高速缓存。cpu将数据从内存读入高速缓存中,计算过后将数据再写入缓存,最后再重新写进内存。由此解决了速度上的矛盾,也带来了缓存一致性的问题。
JMM的模型与之类似,每个java线程都有自己的工作内存,线程私有,其他线程是访问不到的。java线程将变量在主内存中的副本拷贝到工作内存中,线程对变量的操作都在工作内存中进行。
在JMM模型中,主内存与工作内存之间有8种原子性交互操作:
lock(锁定):一个线程独占主内存的一个变量
unlock(解锁):将线程锁定的主内存变量释放出来
read(读取):将变量从主内存 -> 到工作内存中
load(加载):将变量值从工作内存 -> 放入工作副本
use(使用):工作内存变量 -> 放入执行引擎中
assign(赋值):从之型引擎接收到新值 -> 放入工作内存变量副本
store(存储):变量 工作内存 -> 放回主内存中
write(写入):将新值放回主内存变量中
JMM规定这8种操作需要满足一定的规则,简而言之为“先行发生原则”,如果不满足这个规则,即为线程不安全的。
需要注意“先行发生”并不代表“时间上的先发生”,因为有指令重排序等原因的存在
VOLATILE:
volatile变量可以保证可见性:
JMM规定,store和write要同时出现,不能单独出现其中一种操作。而volatile关键字则规定assign和store操作必须要连起来出现。所以意味着一旦变量值被修改,则会被强制写回到主内存当中。
JMM还规定,read和load操作要成对出现,而volatile关键字规定load与use操作必须要连起来出现,这样就保证了线程每次都要从主内存中读取最新的变量值进来。
通过以上的一些规定,volatile保证了可见性,即变量被某线程修改后,其他线程就会看见。
volatile不保证原子性,所以依然会有线程安全问题
典型例子:count++问题
public class FakeVolatile implements Runnable {
static FakeVolatile fakeVolatile = new FakeVolatile();
public static volatile int i = 0;
public static void main(String[] args) throws InterruptedException {
for(int j = 0 ; j < 20 ; j++) {
new Thread(fakeVolatile).start();
}
while(Thread.activeCount() > 1) {
}
// Thread t1 = new Thread(fakeVolatile);
// Thread t2 = new Thread(fakeVolatile);
// Thread t3 = new Thread(fakeVolatile);
// Thread t4 = new Thread(fakeVolatile);
// Thread t5 = new Thread(fakeVolatile);
// t1.start();
// t2.start();
// t3.start();
// t4.start();
// t5.start();
// t1.join();
// t2.join();
// t3.join();
// t4.join();
// t5.join();
System.out.println("i=" + i);
}
@Override
public void run() {
for(int x = 0 ; x < 10000 ; x++)
i++;
}
}
理论上的结果为200000,然而实际上什么结果都有,反正都比20万小。原因就是i++不是原子类操作,所以虽然可以保证volatile读到的变量是主内存最新的,但是在i++的三步操作中,可能就有别的线程已经更新值了,这样就出现了线程不安全的问题。
volatile还能禁止指令的重排序优化
作用:双重校验的单例模式,https://blog.youkuaiyun.com/qq_34785454/article/details/83049079
这篇文章里有提及,通过禁止指令重排序保证单例模式。
volatile编译后通过设置内存屏障来禁止重排序,后面的指令不能重排序到屏障之前的位置。
除了volatile,synchronized和final也能保证可见性。synchronized关键字规定,unlock解锁变量之前,必须先执行store和write操作;而lock上锁之前,则必须把线程工作内存中的副本清空。通过这样强制修改变量写入主内存,读取时也从主内存读取最新值。
final关键字变量在初始化完成后即可保证可见性。