volatile特性和实现原理
volatile 实现了共享变量的线程安全,在多线程操作单个volatile变量时,保证了线程间的可见性。根据happens-before规则,对一个volatile共享变量的写操作,总是happens-before对该volatile共享变量的读操作。
当线程对volatile共享变量进行写操作时
- JMM会把线程对volatile共享变量写操作后,修改的值刷新到主内存中。
- JMM会保证在对volatile共享变量写操作之前的任何一个操作(读或者写操作)禁止重排序。
当其他线程对同一个volatile共享变量进行读操作时
- JMM会把该线程中本地缓存的该volatile共享变量设置为失效,然后,再从主内存中读取该volatile共享变量。
- JMM也会禁止在对volatile共享变量读操作之后的任何一个操作(读或者写操作)禁止重排序。
特性示例代码如下:
public class VolatileFeaturesExample {
private long value;
private boolean flag;
public void writer(long value) {
// 普通变量写 A
this.value = value;
// volatile共享变量写 B
this.flag = true;
}
public long reader() {
// volatile共享变量读 C
if (flag) {
// 普通变量写 D
long tempValue = this.value + 1;
System.out.println(String.format("thread %s get temp value %s", Thread.currentThread().getId(), tempValue));
}
return value;
}
}
在多个线程并发访问的时候,如果线程先执行writer方法,其他线程再执行reader方法。根据volatile特性,操作B happens-before 操作C。由于,操作B是volatile写操作,操作A不会被重排序,所以,操作A happens-before操作B。由于操作C是volatile读操作,所以操作D不会被重排序,操作C happens-before操作D。最终的happens-before规则是A>B>C>D。
volatile原子性操作
假设volatile 变量i为int数值类型,其i++的复合操作不是原子性的,即不是线程安全的。将i++拆分为单个操作时,代码如下:
int tempInt=i;
tempInt=tempInt+1;
i=tempInt;
这个复合操作是先读,修改,后写的综合步骤。按照JMM特性,这个操作的顺序一致性是不会改变的,但是,JMM实现的是在对i变量进行写操作后,其他线程对i的读操作是可见的,属性先写后读的顺序。
这里首先进行的是对i进行读操作,这时,volatile变量i对所有操作的线程都是可见的,并发操作下,多个线程可能同时获取的变量,然后再进行修改时,实际值就与期望值不同了。
对于,对共享变量的值进行读改写的复合操作,可以通过加锁来实现原子性。也可以使用CAS方式实现,CAS同时具有volatile读和volatile写的内存语义,其操作也是线程安全的。参考AtomicInteger的getAndSet方法,代码如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* cas 方式修改值
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
使用场景
- 内存缓存标识
使用volatile 变量做状态标识,实现内存缓存的线程安全。示例代码如下:
public class OverallCache<K, V> {
/**
* 实效时间
*/
private volatile long expires;
/**
* 缓存cacheMap
*/
Map<K, V> cacheMap = ImmutableMap.of();
/**
* 数据加载器
*/
Function<Map<K, V>> loader;
public V get(K key) {
refreshIfNecessary();
return cacheMap.get(key);
}
/**
* 设置失效
*/
private void markExpired() {
// 设置缓存失效,volatile 变量写操作 对其他线程可见
expires = 0;
}
/**
* 刷新缓存
*/
private void refreshIfNecessary() {
long cur = System.currentTimeMillis();
// 读取volatile 变量
if (expires>cur) {
return;
}
// 设置实效时间
expires = cur + cooldown;
// 获取数据
cacheMap = loader.excute();
}
}
volatile expires共享变量表示内存缓存的失效时间。更新缓存时,线程调用markExpired方法,进行volatile expires共享变量写操作,其余线在调用get方法获取缓存时,会先进行volatile expires共享变量读取操作。设置失效方法markExpired总是happens-before get方法,保证了线程间的可见性。
- AQS实现锁
AbstractQueuedSynchronizer,基于volatile 类型的int state变量维护锁的同步状态,包含线程的Node双链表,实现了同步锁的公共基类,基于其子类ReentrantLock分析进行分析。ReentrantLock使用公平锁获取锁时,步骤如下:
- ReentrantLock.lock()
- FairSync.lock()
- AbstractQueuedSynchronizer.acquire()
- FairSync.tryAcquire()
最后在Sync中进行加锁操作,代码如下:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// volatile state变量读取操作
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
使用公平锁解锁时的顺序如下:
- ReentrantLock.unlock()。
- AbstractQueuedSynchronizer.release(int arg)。
- Sync.tryRelease(int releases)
最后在Sync中进行解锁操作,代码如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 对volatile的state变量进行写操作
setState(c);
return free;
}
公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。
非公平锁的原理也差不多,只是是使用CAS的方式,该方式也是线程可见的,使用非公平锁时,会在列表中获取阻塞的线程前,通过CAS方式尝试获取锁,执行一次对于volatile state变量的原子操作。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13625.html