java多线程与并发(四)——java内存模型

《Java并发编程的艺术》第三章学习笔记

一、内存模型的基础

并发编程中的两个关键问题:
1、线程之间如何通信(线程之间交换信息的机制有两种:共享内存和消息传递
在共享内存的并发模型中,线程之间共享程序的公共状态,通过写-读进行隐式通信;在消息传递中,必须通过发送消息显式通信。
2、线程之间如何同步(同步:指在程序中控制不同线程间操作发生相对顺序的机制)
共享内存中,同步显式进行;消息传递中,隐式进行。

在Java中的并发采用的是共享内存模型,整个通信对程序员完全可见。存在内存可见性问题
1.1实例域、静态域和数组元素存储在堆内存中,在线程间共享
1.2局部变量、方法定义参数和异常处理器参数不会在线程间共享,不存在内存可见性问题,也不受内存模型影响
1.3每一个线程有自己抽象的私有的本地内存(缓存、写缓冲区、寄存器以及其他硬件和编译器优化),其中有该线程的读/写共享变量的副本。
1.4编译器和处理器会对指令做重排序,重排序分为3种:
1)编译器优化的重排序。(不改变单线程语义的前提下,可以重排序指令的执行顺序)
2)指令级并行的重排序。(不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序)
3)内存系统的重排序。(因为处理器使用缓存和读/写缓冲区)

以上重排序可能会导致内存可见性问题
java内存模型(JMM)是语言级的内存模型。

java插入内存屏障禁止处理器重排序。

1.5常见的处理器有:sparc-TSO和X86(较强的内存模型,只允许对写/读操作做重排序)、IA64、PowerPC
1.6 happens-before:在JMM中,如果一个操作执行的结果对另一个操作可见,这两个操作必须存在happens-before关系。(可以是一个线程内,也可以是不同线程间)
JMM中一个happens-before规则对应一个或多个编译器和处理器的重排序规则。

二、内存模型中的顺序一致性

2.1重排序遵守的规则:
1)数据依赖性:编译器和处理器不会对存在数据依赖性(只考虑单个处理器中和单个线程中的操作)的操作重排序
2)as-if-serial语义:不管怎么重排序,执行结果不变
3)程序顺序规则:happens-before
重排序会破坏多线程程序的语义。
2.2顺序一致性内存模型:理想化的内存模型
顺序一致性内存模型有两大特性:
1)一个线程的所有操作必须按照程序的顺序执行
2)每个操作必须原子执行且立刻对所有线程可见(不论是否同步)(JMM中没有这个保证)
JMM对正确同步的多线程程序的内存一致性做了如下保证:
如果程序是正确同步的,程序具有顺序一致性,即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。

三、同步原语(synchronized、volatile和final)

1、volatile的内存语义

volatile变量具有下列特性:
1)可见性:对一个volatile变量的读,总是能看到(任意线程)对于volatile变量最后的写入。
2)原子性:对任意单个volatile变量的读/写具有原子性,但volatile++这种复合操作不具有原子性。
volatile变量从JDK5开始可以实现线程之间的通信。
volatile变量的写与锁的释放有相同的内存语义;
volatile变量的读与锁的获取有相同的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。消除将从主内存中读取共享变量。
线程A写volatile变量,线程B读volatile变量,实际上就是线程A通过主内存向线程B发送消息。
volatile内存语义的实现:JMM采取保守策略,基于保守策略的JMM内存屏障插入策略。即在volatile变量写之前插入StoreStore屏障,禁止上面的普通写与volatile写重排序;
在volatile变量写之后插入StoreLoad屏障,避免与后面的volatile变量读/写操作重排序;
在volatile变量读后面插入LoadLoad屏障,禁止下面的volatile变量读与上面的volatile变量读重排序;
在volatile变量读之后插入LoadStore屏障,禁止下面的普通写操作与他重排序。
与锁的区别:
1)volatile变量只对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。
2)在功能上,锁比volatile更强大;
3)在可伸缩性和执行性能上,volatile更有优势。
用volatile替换锁序谨慎!!!

2、锁(synchronized)的内存语义:

synchronized是Java并发编程中最重要的同步机制。
作用:让临界区互斥执行;让释放锁的线程向获取同一个锁的线程发送消息。
锁的释放/获取原理同volatile的写/读。
锁内存语义的实现:
以ReentrantLock为例:

public class ReentrantLockExample {
    int a=0;
    ReentrantLock lock=new ReentrantLock();
    public void writer(){
        lock.lock();
        try{
            a++;
        }finally {
            lock.unlock();
        }
    }
    public void reader(){
        lock.lock();
        try {
            int i=a;
        }finally {
            lock.unlock();
        }
    }
}

调用lock()方法获取锁,调用unlock()方法释放锁。
ReentrantLock分为公平锁和非公平锁,其实现的内存语义:
1)公平锁和非公平锁释放时,最后都要写一个volatile变量state;
2)公平锁获取时,首先回去读volatile变量
3)非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

/**
*使用公平锁的加锁,lock()的轨迹如下:
*1)ReentrantLock:lock()
*2)FairSync:lock()
*3)AbstractQueuedSynchronized:acquire(int arg)
*4)ReentrantLock:tryAcquire(int acquires) 
*/
 static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        .....
}
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//获取锁的开始,首先读volatile变量state
            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;
        }
  /**
  *公平锁解锁方法unlock()轨迹如下:
  *1)ReentrantLock:unlock()
  *2)AbstractQueuedSynchronized:release(int arg)
  *3)Sync:tryRelease(int release).
  */
  public void unlock() {
        sync.release(1);
    }
 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);//释放锁的最后,写volatile变量state
            return free;
        }

而非公平锁的释放与公平锁完全相同,非公平锁的获取如下:

/**
*非公平锁的获取如下:
*1)ReentrantLock:lock()
*2)NonfairSync:lock()
*3)AbstractQueuedSynchronized:compareAndSetState(int except,int update).
*/
protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
/**以上方法以原子操作的方式更新state变量*/

java中把compareAndSet()方法调用简称为CAS,同时具有volatile的读和写的内存语义。
因此,锁的释放-获取的内存语义的实现至少有以下两种方式:
1)利用volatile变量的写-读所具有的内存语义;
2)利用CAS所附带的volatile读和volatile写的内存语义。

3、final语义:

1)在构造函数内对一个final域的写入,与随后把这个被构造函数对象的引用赋值给一个引用变量,这两个操作不能重排序
2)初次读一个final域的对象的引用,与随后初次读这个final域,这两个不能重排序

四、双重检查锁定与延迟初始化

在多线程中,延迟初始化来降低初始化类和创建对象的开销。
双重检查锁定是常见的延迟初始化技术,但是错误的用法。(P67页说明)

未完,待更。。。。

猜你喜欢

转载自blog.csdn.net/u010843421/article/details/80905625