那么 对应 Java 内存模型里面的工作内存,在实现上这里是指 L1 或者 L2 缓存或者 CPU 的寄存器.
假如线程 A 和 B 同时去处理一个共享变量,会出现什么情况呢?达内科技的老师告诉你
使用上图 CPU 架构,假设线程 A和 B 使用不同 CPU 进行去修改共享变量 X,假设 X 的初始化为0,并且当前两级 Cache 都为空的情况,具体看下面分析:
假设线程 A 首先获取共享变量 X 的值,由于两级 Cache 都没有命中,所以到主内存加载了 X=0,然后会把 X=0 的值缓存到两级缓存,假设线程 A 修改 X 的值为1,然后写入到两级 Cache,并且刷新到主内存(注:如果没刷新会主内存也会存在内存不可见问题).
这时候线程 A 所在的 CPU 的两级 Cache 内和主内存里面 X 的值都是1;
然后假设线程 B 这时候获取 X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X=1;然后线程 B 修改 X 的值为2;然后存放到线程2所在的一级 Cache 和共享二级 Cache,最后更新主内存值为2;
然后假设线程 A 这次又需要修改 X 的值,获取时候一级缓存命中获取 X=1,到这里问题就出现了,明明线程 B 已经把 X 的值修改为了2,为啥线程 A 获取的还是1呢?
这就是共享变量的内存不可见问题,也就是线程 B 写入的值对线程 A 不可见.
那么对于共享变量内存不可见问题如何解决呢?Java 中首屈一指的 Synchronized 和 Volatile 关键字就可以解决这个问题,下面会有讲解.
Java 中 Synchronized 关键字
Synchronized 块是 Java 提供的一种原子性内置锁,Java 中每个对象都可以当做一个同步锁的功能来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫做监视器锁.
线程在进入 Synchronized 代码块前会自动尝试获取内部锁,如果这时候内部锁没有被其他线程占有,则当前线程就获取到了内部锁,这时候其它企图访问该代码块的线程会被阻塞挂起.
拿到内部锁的线程会在正常退出同步代码块或者异常抛出后或者同步块内调用了该内置锁资源的 wait 系列方法时候释放该内置锁;
内置锁是排它锁,也就是当一个线程获取这个锁后,其它线程必须等待该线程释放锁才能获取该锁.
上一节讲了多线程并发修改共享变量时候会存在内存不可见的问题,究其原因是因为 Java 内存模型中线程操作共享变量时候会从自己的工作内存中获取而不是从主内存获取或者线程写入到本地内存的变量没有被刷新会主内存.

下面讲解下 Synchronized 的一个内存语义,这个内存语义就可以解决共享变量内存不可见性问题.
线程进入 Synchronized 块的语义是会把在 Synchronized 块内使用到的变量从线程的工作内存中清除,在 Synchronized 块内使用该变量时候就不会从线程的工作内存中获取了,而是直接从主内存中获取;
退出 Synchronized 块的内存语义是会把 Synchronized 块内对共享变量的修改刷新到主内存.
对应上面一节讲解的假如线程在 Synchronized 块内获取变量 X 的值,那么线程首先会清空所在的 CPU 的缓存,然后从主内存获取变量 X 的值;
当线程修改了变量的值后会把修改的值刷新回主内存.
其实这也是加锁和释放锁的语义,当获取锁后会清空本地内存中后面将会用到的共享变量,在使用这些共享变量的时候会从主内存进行加载;
在释放锁时候会刷新本地内存中修改的共享变量到主内存.
除了可以解决共享变量内存可见性问题外,Synchronized 经常被用来实现原子性操作,另外注意,Synchronized 关键字会引起线程上下文切换和线程调度的开销.
Java 中 Volatile 关键字
上面介绍了使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太重,因为它会引起线程上下文的切换开销,对于解决内存可见性问题,Java 还提供了一种弱形式的同步,也就是使用了 volatile 关键字.
一旦一个变量被 volatile 修饰了,当线程获取这个变量值的时候会首先清空线程工作内存中该变量的值,然后从主内存获取该变量的值;
当线程写入被 volatile 修饰的变量的值的时候,首先会把修改后的值写入工作内存,然后会刷新到主内存.这就保证了对一个变量的更新对其它线程马上可见.
下面看一个使用 volatile 关键字解决内存不可见性的一个例子,如下代码的共享变量 value 是线程不安全的,因为它没有进行适当同步措施.
public class ThreadNotSafeInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; } }
首先看下使用 synchronized 关键字进行同步方式如下:
public class ThreadSafeInteger { private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; } }
然后看下使用 volatile 进行同步如下:
public class ThreadSafeInteger { private volatile int value; public int get() { return value; } public void set(int value) { this.value = value; } }
这里使用 synchronized 和使用 volatile 是等价的,都解决了共享变量 value 的内存不可见性问题;但是前者是独占锁,同时只能有一个线程调用 get() 方法,其它调用线程会被阻塞;
并且会存在线程上下文切换和线程重新调度的开销;而后者是非阻塞算法,不会造成线程上下文切换的开销.
这里使用 synchronized 和使用 volatile 是等价的,但是并不是所有情况下都是等价的,这是因为 volatile 虽然提供了可见性保证,但是并没有保证操作的原子性.
那么一般什么时候才使用 volatile 关键字修饰变量呢?
以上就是达内科技给大家做的内容详解,更多关于IT知识的学习,请继续关注达内科技