学习《java并发编程的艺术》Chapter3

volatile内存语义:

    通过lock前缀对多处理器发出指令。当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量的值刷新到主内存;当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,然后线程将从主内存中读取共享变量。

volatile内存语义的实现:

    内存屏障的插入可以禁止编译器的重排序,且JMM采取保守策略,保证任意处理器平台的任意程序都可以得到正确的volatile语义。策略如下:

        1)在每个volatile写操作的前后分别加上StoreStore屏障和StoreLoad屏障。StoreStore屏障将volatile写之前的普通写刷新到主内存,StoreLoad屏障避免当前的volatile写与后面可能的volatile读/写操作重排序。也就是说,编译器不会对volatile写和volatile写前面的任意内存操作重排序。

        2)在每个volatile读操作的后面依次加上LoadLoad屏障和LoadStore屏障。LoadLoad屏障用来禁止volatile读与后面的普通读重排序,LoadStore屏障用来禁止volatile写与后面的普通写重排序。也就是说,编译器不会对volatile读和volatile读后面的任意内存操作重排序。

 

锁的内存语义:

    当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中;当线程获取锁时,JMM会把该线程对应的本地内存置为无效,然后线程将从主内存中读取共享变量(和volatile内存语义几乎一样发现了没~)。

锁内存语义的实现:

    以ReentrantLock为例,ReentrantLock有两个内部类的锁,为公平锁和非公平锁。以公平锁为例。

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            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;
        }
    }

    由公平锁源码可知,加锁方法首先是通过getState()方法读取volatile变量state的值(这里使用了volatile读的内存语义,即编译器不会对volatile读和volatile读后面的内存操作重排序),然后再通过state的值判断这个锁是否被占用,在判断时,调用了一个AQS抽象类下的compareAndSetState()方法,也就是unsafe类下的compareAndSwapInt()方法,该方法简称以原子操作的方式更新int类型的state的值,该类方法简称CAS。JDK文档中对该类方法的说明为:如果当前值等于预期值,则以原子方式将同步状态设置为给定的更新值。直译该方法,也就是原子操作类型的比较交换,具有volatile读和volatile写的内存语义,编译器不能对CAS和CAS前后的任意内存操作重排序。

    而解锁的话,依旧是首先读这个volatile变量state,如果是可重入锁的话,state的值可以是0-n;如果是非重入锁的话,state的值是0或1。一般来说,执行一次tryRelease()方法就是释放一次锁,也就是state的值减1,只有当state的值重新为0的时候,才代表锁能够被释放。然后在解锁方法的最后来写新的volatile state变量(这里使用了volatile写的内存语义,即编译器不会对volatile写和volatile写前面的内存操作重排序),并使后续获取锁的线程读取该volatile变量后立即变得对获取锁的线程可见。

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);
            }
            setState(c);
            return free;
        }

至于为什么能具有volatile读/写的内存语义,如下可以看到,这是一个native方法,是通过C++实现的。大致就是说,如果程序是在多处理器上运行,该native方法会为指令加上lock前缀,提供内存屏障效果;反之省略。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

concurrent包的实现

    如果仔细分析concurrent包的源代码,会发现一个通用化的实现模式。以AtomicInteger为例:

        1)声明共享变量为volatile;

private volatile int value;

        2)使用CAS的原子条件更新来实现线程之间的同步,随便举AtomicInteger类中的几个修改值的方法可以看到,都是通过调用unsafe类中的CAS方法来对volatile变量进行更新;

public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }

        3)配合volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

基于volatile的双重检查锁定

    有时候需要采用延迟初始化来降低初始化类和创建对象的开销,只有在使用该对象时才进行初始化。原理是通过static关键字的特性,在加载类时,就初始化static修饰的成员变量,然后在调用双重检查锁定方法的时候,才实例化一个对象并让初始化的引用指向该对象,也就是完成对象的初始化。

    在这里,volatile关键字的作用是防止重排序,避免方法返回一个并没有完成初始化的对象。

public class Test {

    private volatile static Child1 instance;

    public static Child1 getInstance() {
        if (instance == null) {
            synchronized (Test.class) {
                if (instance == null) {
                    instance = new Child1();
                }
            }
        }
        return instance;
    }
}

谈一谈volatile关键字的一个弊端

    先说一说CPU缓存一致性MESI协议。要解决缓存不一致问题,只有通过总线lock和缓存一致性协议两种方式,但通过锁住总线是一种效率比较低下的方式,所以出现了缓存一致性协议。缓存一致性协议中最出名的就是MESI协议,该协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

    那么问题来了:CPU和内存的交互,是要通过三级缓存的,而交互的最小单元,就是缓存行。通常一个缓存行为64字节,哪怕读取4字节数据,也会连续读取该数据之后的60字节。也就是说,如果通过volatile读读取了一个缓存行,且只有前4个字节才是真正需要读取的对象,就会造成性能损耗上的问题。因为最小操作单元是一个缓存行,所以volatile关键字锁住了一整个缓存行的对象,其它线程就都无法再对多余的60个字节的对象做操作。解决办法也很简单,就是通过填充空白字节使对象膨胀,使无关的共享变量放在不同的缓存行中,也就是牺牲空间换时间。

final域的内存语义:

    对于final域,编译器和处理器要遵守两个重排序规则:

        1)在构造函数内写入一个final域,和随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序(通过在构造函数return之前,插入一个StoreStore屏障,禁止处理器把final域的写重排序到构造函数之外,以保证能够正确的读取到final变量初始化之后的值);

        2)初次读一个包含final域的对象的引用,和随后初次读这个final域,这两个操作之间不能重排序(通过在读final域之前插入一个LoadLoad屏障,禁止处理器把final域的读重排序到读对象引用之前,以保证在读一个对象的final域之前,一定会先读包含这个final域的对象的引用)。


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值