深入理解volatile和使用场景

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使用公平锁获取锁时,步骤如下:

  1. ReentrantLock.lock()
  2. FairSync.lock()
  3. AbstractQueuedSynchronizer.acquire()
  4. 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;
        }

使用公平锁解锁时的顺序如下:

  1. ReentrantLock.unlock()。
  2. AbstractQueuedSynchronizer.release(int arg)。
  3. 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变量的原子操作。
 

发布了37 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/new_com/article/details/105104279
今日推荐