并发编程理论

 

一、内存模型:

计算机内存模型:

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

 

缓存一致性问题:
如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下2种解决方法:


  1)通过在总线加LOCK#锁的方式


  2)通过缓存一致性协议

 

进程和线程之由来

请参考:Java多线程基础:进程和线程之由来https://www.cnblogs.com/dolphin0520/p/3910667.html

 

并发编程中三大原则


1.原子性


原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。


2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。


3.有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

 

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

 

Java内存模型JMM(Java Memory Model)

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。

Java内存模型结构如图:

并发编程理论_第1张图片


1.主内存(Main Memory)

主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。


2.工作内存(Working Memory)

工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

happens-before


happens-before定义:
JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。
因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happensbefore关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。

happens-before原则:

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
·程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
·传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
-join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

@see <

 

指令重排序:
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

注意:
1.处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行
2.指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
@see https://www.cnblogs.com/dolphin0520/p/3920373.html#undefined

 

内存屏障
内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。

内存屏障会提供3个功能:


  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;


  2)它会强制将对缓存的修改操作立即写入主存;


  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile关键字

1.volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:  

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(即保证了可见性)

2)禁止进行指令重排序。(即保证了有序性)
volatile关键字禁止指令重排序有两层意思:
  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。


注意:volatile关键字无法保证原子性

volatile的原理和实现机制

下面这段话摘自《深入理解Java虚拟机》:

 

 “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

  1. 对变量的写操作不依赖于当前值   
  2. 该变量没有包含在具有其他变量的不变式中

 事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

  下面列举几个Java中使用volatile的几个场景。

1.状态标记量(一般用在全局变量中)

volatile boolean flag = false;
 
while(!flag){
    doSomething();
}
 
public void setFlag() {
    flag = true;
}

此案例是原子操作,故能正常使用


更多volatile技术资料请参考:码农翻身http://chuansong.me/n/2109010751015

 

ThreadLocal类

基础概念

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

同步机制的比较
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。
前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

原理分析

threadLocals属性:Thread类有一个核心属性threadLocals,这个threadLocals就是用来存储当前线程变量和对应副本值的映射关系的。其中键值为当前ThreadLocal变量(该变量与当前线程有关但不是当前线程值),value为变量副本(即T类型的变量的值)。源码如下:

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

注意:是Thread类有一个threadLocals变量,而不是ThreadLocal类有threadLocals变量(之前理解搞反了)

 

ThreadLocalMap:

/**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }


    

ThreadLocal是如何为每个线程创建变量的副本的:

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

注意:

1.在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

 


get()方法源码分析:

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

     /**
     * 获取当前ThreadLocalMap
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }


     /**
     *   初始化threadLocals
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }



    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }






     /**
     * 初始值为空,若想不为空,则需客户端重写
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * 

This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; }

我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。


重写逻辑可以参考如下:

ThreadLocal longLocal = new ThreadLocal(){
        protected Long initialValue() {
            return Thread.currentThread().getId();
        };
    };
    ThreadLocal stringLocal = new ThreadLocal(){;
        protected String initialValue() {
            return Thread.currentThread().getName();
        };
    };

set方法

set方法主要就是初始化threadLocals,不同的线程会new出不同的ThreadLocalMap

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以看到,set(T value)方法为每个Thread对象都创建了一个ThreadLocalMap,并且将value放入ThreadLocalMap中,ThreadLocalMap作为Thread对象的成员变量保存。

 

ThreadLocal的remove方法

 /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }



/**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

 

 

 

 

threadLocal是如何保证线程之间的值,互相独立的?

ThreadLocal对象每次调用set方法,都是设置的是当前线程的threadLocals变量的值。调用get方法时也是获取当前线程的threadLocals变量。即ThreadLocalMap不一样,且在set/get方法时都按照当前线程设置和获取的。比如线程1的threadLocals变量和线程2threadLocals变量肯定是不同的。

 

ThreadLocal与内存泄漏

ThreadLocal的内部是ThreadLocalMap。ThreadLocalMap内部是由一个Entry类组成。Entry类的构造函数为 Entry(弱引用的ThreadLocal对象, Object value对象)。因为Entry的key是一个弱引用的ThreadLocal对象,所以在 垃圾回收 之后,将会清除此Entry对象的key。那么, ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。这些 value 被Entry对象引用,所以value所占内存不会被释放。
 

下图可以更直接的表示。其中虚线表示弱引用

并发编程理论_第2张图片

 

@see  ThreadLocal内存泄露

 https://blog.csdn.net/yanluandai1985/article/details/82590336

https://blog.csdn.net/puppylpg/article/details/80433271

https://blog.csdn.net/u013182960/article/details/81027774


使用场景(多用于全局变量):

1.日志统计:计算接口调用耗时时,缓存当前的开始时间,和请求id

2.读写分离时,缓存读写标志。

3.设置数据库连接

private static ThreadLocal connectionHolder
= new ThreadLocal() {
public Connection initialValue() {
    return DriverManager.getConnection(DB_URL);
}
};
 
public static Connection getConnection() {
return connectionHolder.get();
}

4.Session管理

private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

 

思考

1.ThreadLocal是否会造成内存溢出?

我认为很难造成内存溢出,因为ThreadLocalMap中的Entry继承了WeakReferenc。

2. ThreadLocal与内存泄漏
关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题,其实就是要看对内存泄漏的准确定义是什么。


认为ThreadLocal会引起内存泄漏的说法是因为如果一个ThreadLocal对象被回收了,我们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。(如果线程没有被销毁的话)


认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。

当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。

 

@see 14.Java并发编程:深入剖析ThreadLocalhttp://www.cnblogs.com/dolphin0520/p/3920407.html

@see ThreadLocal源码解读http://www.cnblogs.com/micrari/p/6790229.html
死锁

根据操作系统中的定义:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁的四个必要条件:
互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。
请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。
非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。

解决死锁理论

只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。 

有效避免死锁的办法(自己总结的)
1.为获取到锁的线程设置超时时间

2.避免一个线程同时获取多个锁。
3.避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
4.于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

CAS

什么是CAS?
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。它提供了一种代替同步锁的方式,相比同步锁减少了内核态与用户态之间来回切换的性能开销
 


CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。否则会重新获取内存地址V的当前值,并重新计算想要修改的新值。这个重新尝试的过程被称为自旋

CAS的基本概念及实现原理请参考:漫画:什么是 CAS 机制

 

基于CAS所实现的类有:Atomic相关类。

CAS的底层实现

Java语言CAS底层如何实现?

 

首先看一看AtomicInteger当中常用的自增方法 incrementAndGet:

 

private volatile int value;


public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
    }
}

public final int get() {
return value;
}

这段代码是一个无限循环,也就是CAS的自旋。循环体当中做了三件事:
1.获取当前值。
2.当前值+1,计算出目标值。
3.进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤。

这里需要注意的重点是 get 方法,这个方法的作用是获取变量的当前值。如何保证获得的当前值是内存中的最新值呢?很简单,用volatile关键字来保证。

CAS的底层实现则是compareAndSet方法。

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

Java利用unsafe类提供了原子性操作方法。

什么是unsafe呢?

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

 

具体原理请参考:漫画:什么是CAS机制?(进阶篇)https://blog.csdn.net/bjweimengshu/article/details/79000506

 

CAS的缺点:

1.CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3.ABA问题

这是CAS机制最大的问题所在。

什么是ABA问题?

当一个值从A更新成B,又更新成A,普通CAS机制会误判通过检测。

怎么解决?

利用版本号比较可以有效解决ABA问题。

实现原理如下:

在Compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。如果期望值A和地址V中的实际值相等但是版本号却不同,则更新失败。

具体原理请参考:  漫画:什么是CAS机制?(进阶篇)

AQS

AQS是抽象队列同步器AbstractQueuedSynchronizer的简称。

它是用来构建锁或者其他同步组件的基础框架,其实质就是一个抽象类。

它使用一个整型的volatile变量(命名为state)来维护同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
 

其结构如下图所示

 

并发编程理论_第3张图片

 

volatile变量的读写与CAS是concurrent包得以实现的基础。CAS表示如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值,此操作具有volatile读和写的内存语义。
AQS通过volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

 

 高层类   Lock  同步器  阻塞队列  Executor  并发容器
  基础类 AQS  非阻塞数据结构  原子变量类
  volatile变量的读/写  CAS

concurrent包的实现结构如上图所示,AQS、非阻塞数据结构和原子变量类等基础类都是基于volatile变量的读/写和CAS实现,而像Lock、同步器、阻塞队列、Executor和并发容器等高层类又是基于基础类实现。

 

 

AQS的域和方法

  

即类的关键属性

private transient volatile Node head; //同步队列的head节点
private transient volatile Node tail; //同步队列的tail节点
private volatile int state; //同步状态

state为0表示可以获取锁,大于0表示不能获取锁。

  方法

AQS提供的可以修改同步状态的3个方法:

protected final int getState();  //获取同步状态
protected final void setState(int newState);  //设置同步状态
protected final boolean compareAndSetState(int expect, int update);  //CAS设置同步状态

资源共享方式

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

AQS提供的获取和释放锁的自定义方法

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。下面为AQS提供的可以重写的方法。

protected boolean tryAcquire(int arg) //独占式获取同步状态,此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。返回值语义:true代表获取成功,false代表获取失败。
protected boolean tryRelease(int arg) //独占式释放同步状态

protected int tryAcquireShared(int arg) //共享式获取同步状态,返回值语义:负数代表获取失败、0代表获取成功但没有剩余资源、正数代表获取成功,还有剩余资源。
protected boolean tryReleaseShared(int arg) //共享式释放同步状态

protected boolean isHeldExclusively() //AQS是否被当前线程所独占

基于AQS的类

独占式加锁和解锁流程

1.ReentrantLock

ReentrantLock有个静态内部类Sync,Sync继承了AbstractQueuedSynchronizer。以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

共享式加锁和解锁流程 

2.CountDownLatch

同样CountDownLatch有个静态内部类Sync,Sync继承了AbstractQueuedSynchronizer。再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

  一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

 

3.Semaphore是信号量加锁解锁流程:

自己理解:初始化一个n值,每当一个线程获取到了资源,就cas减一,当其他线程发现n的值为0,就进入等待队列。当线程释放时,就将n值cas加1,并唤醒被阻塞的线程,被阻塞的线程在重新去获取资源

 

@see 深入理解Semaphore  https://blog.csdn.net/qq_19431333/article/details/70212663

 

AQS源码详解

 

Node类的变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

0状态:值为0,代表初始化状态。
 

独占锁

核心方法:

获取锁

acquire(int)
源码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

函数流程如下:

1.tryAcquire()尝试直接去获取资源,如果成功则直接返回;
2.addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3.acquireQueued()使线程在等待队列中获取资源。每个线程自旋的寻找上个节点是否为头节点,如果不是则被LockSupport.park(this),直到上一个节点释放锁被unpark()或者被interrupt()),然后继续去尝试获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

 

并发编程理论_第4张图片

 

至此,acquire()的流程终于算是告一段落了。这也就是ReentrantLock.lock()的流程,不信你去看其lock()源码吧,整个函数就是一条acquire(1)!!!

 

 

acquireQueued(Node, int)


源码如下:

 

进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。


shouldParkAfterFailedAcquire(Node, Node)


整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,即寻找前驱节点是正常的,并将其状态设置为SIGNAL。


parkAndCheckInterrupt()


park()会让当前线程进入waiting状态,即该方法被阻塞了,直到唤醒才会正常返回。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。


释放锁

release(int)

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到头结点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒等待队列里的下一个线程
        return true;
    }
    return false;
}


它调用tryRelease()来释放资源。有一点需要注意的是,它是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()的时候要明确这一点!!

unparkSuccessor(Node)
此方法用于唤醒等待队列中下一个线程。下面是源码:

private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}


这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!!And then, DO what you WANT!
 

 

 

思考

1.同步队列里的节点状态如何?

所有节点都会被park,但只有最靠近头节点的线程会被unPark。故AQS是悲观锁。

2.同步队列的节点被中断了,节点会如何?

节点如果被中断了,他将不会响应,只是标记它被中断了,在获取资源后再进行自我中断。而他的next节点,在中断节点释放锁后,被唤醒,并移除中断节点。
 

 

 

共享锁

acquireShared(int)

这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:

1.tryAcquireShared()尝试获取资源,成功则直接返回;
2.失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

doAcquireShared(int)


有木有觉得跟acquireQueued()很相似?对,其实流程并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外,其实都一样,不知道Doug Lea是怎么想的。

跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。

小结

其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)。


releaseShared() 

此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。
 

 

常见问题:

@see  《Java并发编程的艺术》


6.Java并发编程--AQS https://www.cnblogs.com/zaizhoumo/p/7749820.html

7.Java并发之AQS详解https://www.cnblogs.com/waterystone/p/4920797.html


使用Java包下的工具进行并发编程

为了方便编写出线程安全的程序,Java里面提供了一些线程安全类和并发工具,比如:同步容器、并发容器、阻塞队列、Synchronizer(比如CountDownLatch)。

 

同步容器

同步容器类
在Java中,同步容器主要包括2类:

  1)Vector、Stack、HashTable

  2)Collections类中提供的静态工厂方法创建的类

使用工具类Collections将非线程安全容器包装成线程安全容器。如Collections.synchronizedMap(Map m)将原始Map包装为线程安全的SynchronizedMap,但是实际上最终操作时,仍然是在被包装的原始map上进行,只是SynchronizedMap的所有方法都加上了synchronized锁控制。

同步容器的缺陷
1.性能问题

2.同步容器并不能保证绝对的安全(使用不当的情况下)

使用不当的情况下的案例请参考: Java并发编程:同步容器https://www.cnblogs.com/dolphin0520/p/3933404.html

以下摘自:《core java》

并发编程理论_第5张图片

 

ConcurrentModificationException

产生原因:
在循环中,调用list.remove()方法导致modCount和expectedModCount的值不一致。注意,像使用for-each进行迭代实际上也会出现这种问题。

ConcurrentHashMap中的迭代器

思考:ConcurrentHashMap的迭代器是强一致性的迭代器还是弱一致性的迭代器?

答:ConcurrentHashMap在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。

ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。
 

 

并发容器

为什么JUC需要提供并发容器?
java collection framework提供了丰富的容器,有map、list、set、queue、deque。但是其存在一个不足:多数容器类都是非线程安全的,即使部分容器是线程安全的,由于使用sychronized进行锁控制,导致读/写均需进行锁操作,性能很低。

并发容器

java cocurrent包提供了很多并发容器,在提供并发控制的前提下,通过优化,提升性能。如利用cas机制,CopyOnWrite容器.

cas机制上文已经说了,CopyOnWrite容器请参考:Java并发编程:并发容器之CopyOnWriteArrayList(转载)https://www.cnblogs.com/dolphin0520/p/3938914.html

常见的并发容器:

1.ConcurrentHashMap


2.ConcurrentLinkedQueue

入队:
并发下的安全问题:

如果有一个线程正在入队,那么它必须先获取尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新获取尾节点。


concurrentLinkedQueue解决并发安全问题:

从源代码角度来看整个入队过程主要做二件事情。第一是定位出尾节点,第二是使用CAS算法能将入队节点设置成尾节点的next节点,如不成功则重试(即重新获取尾节点)。

出队:

首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,则只需要重新获取头节点。如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。

@see 《Java并发编程的艺术》

3.CopyOnWriteArrayList
CopyOnWriteArrayList提供高效地读取操作,使用在读多写少的场景。CopyOnWriteArrayList读取操作不用加锁,但写的时候会加锁,且是安全的;写操作时,先copy一份原有数据数组,再对复制数据进行写入操作,最后将复制数据替换原有数据,从而保证写操作不影响读操作。

CopyOnWrite的缺点
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

 1. 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

  针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

 2. 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

阻塞队列

产生背景:

使用非阻塞队列的时候有一个很大问题就是:它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。但是有了阻塞队列就不一样了,它会对当前线程产生阻塞,比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。

几种主要的阻塞队列

自从Java 1.5之后,在java.util.concurrent包下提供了若干个阻塞队列,主要有以下几个:

  ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。

  LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。

  PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。

  DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

Synchronizer(同步器)

在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程如:

并发编程理论_第6张图片


juc提供的同步器常用的有以下三种:CountDownLatch、CyclicBarrier和Semaphore

1.CountDownLatch(计数器):CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

2.CyclicBarrier(回环栅栏):通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

3.Semaphore(信号量):Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

下面对上面说的三个辅助类进行一个总结:

  1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。CyclicBarrier可以重用表示可以恢复初始时设定的计数器,而CountDownLatch无法重用时因为计数器已经变成0了,无法恢复到初始时设定的值

  2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

原理请参考:Java并发编程:CountDownLatch、CyclicBarrier和Semaphore  https://www.cnblogs.com/dolphin0520/p/3920397.html

Fork/Join

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干
个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join的运行流程图如下:

并发编程理论_第7张图片
使用场景(待续)

参考资料:

1.《Java并发编程的艺术》
2.博客:Java并发编程:volatile关键字解析 https://www.cnblogs.com/dolphin0520/p/3920373.html#undefined
3.码农翻身 :http://chuansong.me/n/2109010751015,

漫画:什么是 volatile 关键字?https://blog.csdn.net/bjweimengshu/article/details/78769623

4.漫画:什么是 CAS 机制https://blog.csdn.net/WantFlyDaCheng/article/details/81603361

5.漫画:什么是CAS机制?(进阶篇)https://blog.csdn.net/bjweimengshu/article/details/79000506

6.Java并发编程--AQS https://www.cnblogs.com/zaizhoumo/p/7749820.html

7.Java并发之AQS详解https://www.cnblogs.com/waterystone/p/4920797.html

8.Java多线程基础:进程和线程之由来https://www.cnblogs.com/dolphin0520/p/3910667.html

9.死锁产生的原因和解锁的方法https://www.cnblogs.com/Jessy/p/3540724.html

10.Java并发编程:同步容器https://www.cnblogs.com/dolphin0520/p/3933404.html

11.Java并发编程:阻塞队列https://www.cnblogs.com/dolphin0520/p/3932906.html

12.java并发编程——并发容器https://www.cnblogs.com/daoqidelv/p/6753162.html

13.Java并发编程:并发容器之CopyOnWriteArrayList(转载)https://www.cnblogs.com/dolphin0520/p/3938914.html

14.Java并发编程:深入剖析ThreadLocalhttp://www.cnblogs.com/dolphin0520/p/3920407.html

14.《core java》

 

你可能感兴趣的:(java并发)