1、测试没有 volatile关键字的demo
public class VolatileTest1 {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
while(true){
if(threadDemo.isFlag()){
System.out.println("----主线程读到flag为true----");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
flag = true;
System.out.println("子线程修改了值flag=" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
在子线程中将线程的共享变量 flag的值修改成了 true时,但是主线程在条件判断时读到的flag一直是false,所以while循环不会停止跳出,程序不会终止。这是由于内存的可见性导致的。
2、内存可见性(Memory Visibility)
内存可见性(Memory Visibility)其实是指共享变量在不同线程之间的可见性。
内存可见性与Java内存模型有关系
所有的变量都存储在主内存中(操作系统给进程分配的内存空间),而每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本。
注意:线程对共享变量的所有操作都必须在自己的工作内存(working memory,是cache和寄存器的一个抽象,并不是内存中的某个部分)。不同线程之间,当前线程无法直接访问其他线程的工作内存中的变量,线程间变量值得传递需要通过主内存来完成。
缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
解决共享变量的内存可见问题的方式有很多
1、synchronized实现可见性
JMM(Java内存模型)关于synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存中,
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
while(true){
// main线程加锁
synchronized(threadDemo){
if(threadDemo.isFlag()){
System.out.println("----主线程读到flag为true----");
break;
}
}
}
}
2、volatile关键字实现可见性
对于多线程, volatile不具备“互斥性”,不能保证变量状态的“原子性操作”。
使用 volatile 关键字用来确保将变量的更新操作通知到其他线程。
某个线程的工作内存中修改了共享变量的值并会刷新到主内存中,同时其他线程已经读取的共享变量副本就会失效,需要读数据时就会再次去主内存中读取新的共享变量的值,从而达到共享变量内存可见。
// 共享变量用 volatile修饰即可
private volatile boolean flag = false;
synchronized 和 volatile比较
synchronized具备“互斥性”,既能保证可见性,又能保证原子性,volatile不具备“互斥性”,只能保证可见性,不能保证原子性。
volatile不需要加锁,比synchronized更轻量级,不会阻塞线程,效率更高。如果能用 volatile解决问题,应尽量使用volatile,因为它的效率比synchronized更高。
原子性:一次操作,要么全部执行成功,要么全部执行失败。一个很经典的例子就是银行账户转账问题。
1、一个实例demo
public class AtomicTest {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo(0);
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable{
//线程共享变量
private volatile int number;
public AtomicDemo(int number) {
this.number = number;
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",number=" + ++number);
}
}
运行结果会发现可能会在不同的线程中,看到相同的数值,这是由于 volatile关键字保证了操作的内存可见性,但是 volatile不能保证操作的原子性。
自增操作不是原子性操作,它包括读取变量的原始值、进行加1操作、写入工作内存。而且volatile也无法保证对变量的任何操作都是原子性的。
2、解决原子性操作问题--JUC
java.util.concurrent.atomic 原子操作类包里面提供了一组原子变量类。封装了一系列常用的数据类型对应的封装类,
Java.util.concurrent.atomic 中实现的原子操作类可以分成4组:
标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
复合变量类:AtomicMarkableReference,AtomicStampedReference
这些类都保证了两点:
1)类里的变量都用了volatile保证内存是可见的
2)使用了一个算法CAS(Compare And Swap),保证对这些数据的操作具有原子性
public class AtomicTest1 {
public static void main(String[] args) throws InterruptedException {
//线程共享变量
AtomicInteger atomicInteger = new AtomicInteger(0);
AtomicDemo1 atomicDemo = new AtomicDemo1(atomicInteger);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(atomicDemo);
thread.start();
}
}
}
class AtomicDemo1 implements Runnable{
private AtomicInteger atomicInteger = null;
public AtomicDemo1(AtomicInteger atomicInteger) {
this.atomicInteger = atomicInteger;
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ",atomicInteger=" + atomicInteger.incrementAndGet());
}
}
参考文章:
Java并发编程:volatile关键字解析
阿里面试:跟我死磕Synchronized底层实现,我满分回答拿了Offer