并发编程理论

一、内存模型:


读取和写入:

 

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

进程和线程之由来

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

二、并发编程中三大原则:


1.原子性


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


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


3.有序性

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

 

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

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






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


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


  2)通过缓存一致性协议

三.Java内存模型JMM(Java Memory Model)


Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。举个简单的例子:在java中,执行下面这个语句:
i  = 10;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

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

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。




@see <<Java并发编程的艺术》》,https://www.cnblogs.com/dolphin0520/p/3920373.html#undefined

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

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


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


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


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


死锁

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

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

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

CAS


什么是CAS?


CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS的实现原理


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


Java语言CAS底层如何实现?

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

什么是unsafe呢?
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问题。

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

AQS

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

它是用来构建锁或者其他同步组件的基础框架,其实质就是一个抽象类。它使用一个整型的volatile变量(命名为state)来维护同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
 

其结构如下图所示

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; //同步状态

  方法

    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是否被当前线程所独占

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

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

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

AQS源码详解(待续)

常见问题:


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

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

同步容器

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

  1)Vector、Stack、HashTable

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

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

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

2.同步容器并不能保证绝度的安全

并发容器

为什么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

原理待续

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

阻塞队列

产生背景:

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

几种主要的阻塞队列

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

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

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

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

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

参考资料:

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

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

猜你喜欢

转载自blog.csdn.net/sinat_34814635/article/details/80943749