Java并发:其他(总结性的东西)

来源:互联网 发布:淘宝店铺产品布局 编辑:程序博客网 时间:2024/06/05 02:21

相关文章:
1.原子类 ,锁
http://blog.csdn.net/youyou1543724847/article/details/52735510

2.多线程相关的

3.线程安全的集合
http://blog.csdn.net/youyou1543724847/article/details/52734876

4.ThreadLocal
http://blog.csdn.net/youyou1543724847/article/details/52460852

5.并发:其他(如volatile,原子类,不变类)
http://blog.csdn.net/youyou1543724847/article/details/52735829

并发中的主要问题

并发基础主要解决的是可见性,有序性和原子性的问题,让不可控的进程/线程变得可以预测,可以控制行为。
Java解决可见性/有序性的主要技术是通过Java内存模型来解决的。Java内存模型这个域里面有这些知识点

  1. 可见性问题的根源 – CPU写操作的延迟
    1. 造成写操作延迟的原因主要是高速缓存的存在,理解缓存的原理,局部性原理,高速缓存的原理等
    2. 解决可见性问题的通用方法 – 确定一致性需求。有多种一致性模型:线性一致性,顺序一致性,因果一致性,处理器一致性,弱一致性,释放一致性,进入一致性等等
    3. 底层硬件提供了实现一致性需求的能力 – 内存屏障,比如X86的mfence, sfence, lfence, Lock前缀等等,理解Lock前缀的语义
    4. 底层硬件提供了缓存一致性协议来提供底层同步缓存的能力,注意总线的互斥性,缓存一致性流量等
    5. Java内存模型是语义级的内存模型,主要是屏蔽底层硬件提供的内存模型能力的差异,提供了一系列的Java同步操作语法,制定了Happens-before规则
    6. 理解volatile, synchronized, CAS等操作的底层实现原理
    7. 理解Happens-before规则描述的是可见性的问题
    8. 理解指令重排序的概念,理解有序性

Java解决原子性的问题主要是通过锁/互斥来实现的,锁这个问题域里面有这些知识点

  1. 锁的原理,饥饿,公平,自旋,阻塞,管程,条件队列等等概念
  2. 并发编程的三个重要特性:可见性,有序性和原子性。锁解决的问题域
  3. 线程在各个层面的状态控制,JVM中如何实现线程,操作系统如何实现线程,线程调度
  4. 自旋 VS 阻塞
  5. 多种经典的自旋锁的实现,比如TAS/TTAS/CLH/MCS lock
    1. 读写锁,可重入锁,时限锁的原理和实现
  6. Object.wait(), Object.notify, Condition等条件队列操作的底层原理
  7. sun.misc.Unsafe类提供的同步能力
  8. Java并发包中的AQS同步器的设计和重点实现
  9. Java并发包中的Semaphore, CountDownLatch, CyclicBarrier等同步器的实现
  10. 一些无锁的数据结构的设计思路及实现,比如无锁队列
    12.锁的优化,比如控制锁的粒度,锁分段,识别和解决死锁/活锁等

理解了可见性,有序性,原子性的原理和底层实现之后,需要理解一下并发场景下的一些通用的设计思路,高性能服务器的设计思路
1. 如何安全发布一个对象
2. 线程封闭技术,不可变对象的使用
3. 控制锁的粒度,锁分段,CopyOnWrite等优化
4. 生产者消费模型
5. 线程池的设计和实现
6. 同步操作转异步操作
7. 5种IO模型的理解
8. 高性能服务器的线程模型设计, reactor / proactor
9. 使用高效的网络传输 – NIO的原理,设计和实现,比如epoll / selector / pull, Buffer的使用
10. 多进程监听一个端口 vs 单进程监听一个端口, nginx的惊群问题分析

happends-before模型

(主要原因:由于指令的乱序重排,cache等的因素,使得指令的实际执行顺序并不可预测。但是JAVA虚拟机提供基本的保证,保证这些操作之间的可见性,顺序性,这些基本的保证就是happens-before原则,即JMM)
happends-before:什么是happens-before?
happens-before就是“什么什么一定在什么什么之前运行”,也就是保证顺序性。
因为CPU是可以不按我们写代码的顺序执行内存的存取过程的,也就是指令会乱序或并行运行,
只有上面的happens-before所规定的情况下,才保证顺序性。

1.锁:
如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。(An unlock on a monitor happens-before every subsequent lock on that monitor.)

2.volatile:
线程1写入了volatile变量v(这里和后续的“变量”都指的是对象的字段、类字段和数组元素),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。(A write to a volatile field happens-before every subsequent read of that volatile.)

3.线程
Thread termination rule Join:某个线程B调用了A.join(),则所有的A的Action都happends-before B中join()之后的代码。(Each action in a thread happens-before every subsequent action in that thread.)
Any action in a thread happens-before any other thread detects that thread has terminated, either by successfully return from Thread.join or by Thread.isAlive returning false

单个线程:单个线程的动作A对之后的动作都是可见的(Each action in a thread happens-before every subsequent action in that thread.)

线程调用:A call to Thread.start on a thread happens-before every action in the started thread

中断法则:一个线程调用另一个线程的interrupt一定发生在另一线程发现中断。

3.传递规则
If A happens-before B, and B happens-before C, then A happens-before C.

4.终结法则
一个对象的构造函数结束一定发生在对象的finalizer之前

Java内存模型中只是列出了8种比较基本的hb规则,在Java语言层面,又衍生了许多其他happens-before规则,如ReentrantLock的unlock与lock操作,又如AbstractQueuedSynchronizer的release与acquire,setState与getState等等。

接下来用hb规则分析两个实际的可见性例子:例子一
看个CopyOnWriteArrayList的例子,代码中的list对象是CopyOnWriteArrayList类型,a是个静态变量,初始值为0

假设有以下代码与执行线程:
这里写图片描述
那么,线程2中b的值会是1吗?来分析下。假设执行轨迹为以下所示:
这里写图片描述

p1,p2是同一个线程中的,p3,p4是同一个线程中的,所以有hb(p1,p2),hb(p3,p4),要使得p1中的赋值操作对p4可见,那么只需要有hb(p1,p4),前面说过,hb关系具有传递性,那么若有hb(p2,p3)就能得到hb(p1,p4),p2,p3是不是存在hb关系?
Actions in a thread prior to placing an object into any concurrent collection happen-before actions subsequent to the access or removal of that element from the collection in another thread。
p2是放入一个元素到并发集合中,p3是从并发集合中取,符合上述描述,因此有hb(p2,p3).也就是说,在这样一种执行轨迹下,可以保证线程2中的b的值是1.如果是下面这样的执行轨迹呢?
这里写图片描述

依然有hb(p1,p2),hb(p3,p4),但是没有了hb(p2,p3),得不到hb(p1,p4),虽然线程1给a赋值操作在执行顺序上是先于线程2读取a的,但jmm不保证最后b的值是1.这不是说一定不是1,只是不能保证。如果程序里没有采取手段(如加锁等)排除类似这样的执行轨迹,那么是无法保证b取到1的。像这样的程序,就是没有正确同步的,存在着数据争用(data race)。

既然提到了CopyOnWriteArrayList,那么顺便看下其set实现吧:
这里写图片描述

有意思的地方是else里的setArray(elements)调用,看看setArray做了什么:

    final Object[] getArray() {        return array;    }    /**     * Sets the array.     */    final void setArray(Object[] a) {        array = a;    }

一个简单的赋值,array是volatile类型。elements是从getArray()方法取过来的,getArray()实现如下:

也很简单,直接返回array。取得array,又重新赋值给array,有甚意义?setArray(elements)上有条简单的注释,但可能不是太容易明白。正如前文提到的那条javadoc上的规定,放入一个元素到并发集合与从并发集合中取元素之间要有hb关系。set是放入,get是取(取还有其他方法),怎么才能使得set与get之间有hb关系,set方法的最后有unlock操作,如果get里有对这个锁的lock操作,那么就好满足了,但是get并没有加锁:

    public E get(int index) {        return get(getArray(), index);    }

但是get里调用了getArray,getArray里有读volatile的操作,只需要set走任意代码路径都能遇到写volatile操作就能满足条件了,这里主要就是if…else…分支,if里有个setArray操作,如果只是从单线程角度来说,else里的setArray(elements)是没有必要的,但是为了使得走else这个代码路径时也有写volatile变量操作,就需要加一个setArray(elements)调用。

接下来用hb规则分析两个实际的可见性例子:例子二
FutureTask
提交任务给线程池,我们可以通过FutureTask来获取线程的运行结果。绝大部分时候,将结果写入FutureTask的线程和读取结果的不会是同一个线程。写入结果的代码如下:
这里写图片描述
获取结果的代码如下:

volatile的关键字

在Java最初的版本中,就有一个叫volatile的关键字,它是一种简单的同步的处理机制,因为被volatile修饰的变量遵循以下规则:
•变量的值在使用之前总会从主内存中再读取出来。
•对变量值的修改总会在完成之后写回到主内存中。
  使用volatile关键字可以在多线程环境下预防编译器不正确的优化假设(编译器可能会将在一个线程中值不会发生改变的变量优化成常量),但只有修改时不依赖当前状态(读取时的值)的变量才应该声明为volatile变量。

0 0
原创粉丝点击