架构师成长之路

Java并发深入理解

2016-04-13  本文已影响251人  一一道长一一

1.ThreadLocal和volatile的理解:

ThreadLocal会为每个线程中创建一个变量的副本,每一个线程持有一个ThreadLocalMap对象,ThreadLocalMap的key为ThreadLocal对象,value为ThreadLocal的变量值。各个线程管理自己的副本数据,互相之间不会有交集。

数据结构:当前线程.ThreadLocalMap<ThreadLocal,value>

常用的使用场景:Session、数据库连接Connection(每个线程拥有自己的Connection而不互相干扰)

volatile的两个特性:

1)可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

2)原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile内存语义理解:

1)线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。

2)线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。

3)线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

为实现上述内存语义,以下是基于保守策略的JMM内存屏障插入策略:

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

注:内存屏障是一组处理器指令,用于实现对内存操作的顺序限制,这里用来保证volatile变量可以按照写-读的顺序执行。(编译器为优化代码会重排读写操作,内存屏障确保不会进行重排)。

2.生产-消费模式:

要实现生产消费模式,生产者和消费者必须可以互相调用,一般是将二者的实例放到同一个类中,二者的实例在持有中间类。

生产者在没有数据时进行生产,生产后通知消费者进行消费,还有数据时等待;消费者在有数据时进行消费,消费完之后通知生产者进行生产,没有数据时等待。

关键在于wait和notify的理解,wait和notify在调用时对谁进行操作,就要同步谁,即synchronize(谁)。

3.BlockingQueue

实现主要用于生产者-使用者队列,BlockingQueue实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。

LinkedBlockingQueue(无界的队列),ArrayBlockingQueue(有界的队列),SynchronousQueue(一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有,优势是轻量级,适用于线程间传递信号,JDK文档原话是:它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步)

ArrayBlockingQueue中,容量满了之后使用put添加元素会导致阻塞,直到有容量的时候继续执行,take执行与之相反的操作;使用add添加元素则会导致异常,remove执行与之相反的操作;使用offer添加元素则可以指定等待可用容量的时长,如果没有可用容量,返回false,否则返回true,poll方法执行与之相反的操作。put和take是多线程中最常用的组合。

4.PipedReader和PipedWriter

通过输入输出在线程间进行通信通常很有用。PipedReader的构造器中需要一个PipedWriter实例。

PipedReader和普通I/O之间的区别是:PipedReader是可以中断的,而普通I/O是不可interrupt的

5.CountDownLatch(锁存储器)和CyclicBarrier

CountDownLatch被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。CountDownLatch被设计为只触发一次,计数不能被重置。当每个任务完成时,都会在这个所存储器上调用countDown(),等待问题解决的任务在这个锁存储器上调用await(),将它们自己拦住,直到锁存储器计数结束。

CyclicBarrier适用于一组任务并行执行,然后在进行下一个步骤之前等待,直到所有的任务都完成,才执行下一个动作。任务都调用CyclicBarrier的await()方法等待同组其他任务完成,CyclicBarrier的构造方法接收一个或两个参数,第一个为前置任务数量,第二个为要执行的后置任务。

二者不同地方在于,CountDownLatch只能执行一次,而CyclicBarrier可以重复执行。

6.DelayQueue 延迟队列

延迟队列是Delayed元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素

DelayQueue中放置实现了Delayed接口的对象,Delayed接口必须实现compareTo()和getDelay()方法,compare方法决定了DelayQueue.take()出来的Delayed对象的顺序,getDelay()方法必须返回延迟时间减去当前时间的差值,也就是说每次take()都会调用getDelay方法来判断是否到了延迟时间,如果getDelay返回的是一个固定的值,将永远不能take出来。

7.PriorityBlockingQueue 优先级队列

优先级队列允许用户根据自己实现的Compare接口来对对象进行排序,所以优先级队列中的对象必须实现Compare接口或者使用第三方Cmpare接口进行排序,根据使用哪个构造方法来决定。与PriorityQueue不同的是,该对象是线程安全的,是可以被阻塞的。

8.ScheduledThreadPoolException 定时执行器

scheduleAtFixedRate方法与scheduleWithFixedDelay方法的区别:scheduleAtFixedRate是以固定频率执行任务(如果任务执行的时间大于频率时长,前一个任务执行完后立即执行下一个任务,否则,按照固定频率执行);scheduleWithFixedDelay是按照固定延迟时间执行,即前一个任务执行完后(不管用多长时间),都要等待固定的一段时间再执行下一个任务。

9.Semaphore 计数信号量

从概念上讲,信号量维护了一个许可集(初始化时要指定大小)。在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动(它只是一个计数器)。

通常,应该将用于控制资源访问的信号量初始化为公平的(第二个参数为true),以确保所有线程都可访问资源。为其他的种类的同步控制使用信号量时,非公平排序(第二个参数为false)的吞吐量优势通常要比公平考虑更为重要。

适用场景:连接池、对象池的设计

10.Exchanger 两个线程交换数据的栅栏

当一个线程到达exchange调用点时,如果它的伙伴线程此前已经调用了此方法,那么它的伙伴会被调度唤醒并与之进行对象交换,然后各自返回。如果它的伙伴还没到达交换点,那么当前线程将会被挂起,直至伙伴线程到达——完成交换正常返回;或者当前线程被中断——抛出中断异常;又或者是等候超时——抛出超时异常。

11.多线程性能调优

尽量使用synchronize关键字,因为这种方法代码可读性特别高;在影响到性能的时候使用Lock对象,Lock对象在大量并发的情况下性能比较稳定;只有在性能方面的需求特别高时,考虑使用Atomic,Atomic性能最好,但是使用场景特别有限。

12.免锁容器

免锁容器的通用策略:对容器的修改可以与读取操作同时发生,只要读取者只能看到修改完的结果即可,修改是在容器数据结构的某个部分的一个单独的副本上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。

CopyOnWriteArrayList原理:在写的时候是先将底层源数组复制到新数组中,然后在新数组中写,写完后更新源数组。而读的话只是在源数组上读。也就是,读和写是分离的。由于写的时候每次都要将源数组复制到一个新组数中,所以写的效率不高。CopyOnWriteArrayList适合用于读操作远远大于写操作的情景中。

CopyOnWriteArraySet使用CopyOnWriteArrayList实现其免锁行为。

ConcurrentHashMap原理:使用锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。它的get方法里将要使用的共享变量都定义成volatile,保证了高效和统一。

获取ConcurrentHashMap的Size的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小(即锁定所有的Segment的put,remove和clean方法,很低效)。

所以,要尽量避免使用ConcurrentHashMap的size方法。

ConcurrentLinkedQueue由head节点和tair节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。(tair尾节点并不一定总是指向最后一个节点,这样做是为了使用volatile变量的读操作来减少volatile的写操作,同样,并不是每次出队时都更新head节点) 

入队过程主要做二件事情。第一是定位出尾节点,第二是使用CAS算法能将入队节点设置成尾节点的next节点,如不成功则重试。

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

13.乐观锁和悲观锁

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

14.CAS 算法

CAS,即 Compare And Swap,乐观锁用到的机制就是CAS算法。

CAS有三个操作数,内存值V,旧的预期值A,要修改的新值B,当且仅当预期值A和内存值V相等时,将内存值V修改为B,否则什么都不做。

15.ReadWriteLock 读取锁

读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。只有写操作特别少二读操作特别多的时候才使用ReadWriteLock。

16.内存一致性属性

Java Language Specification 第 17 章定义了内存操作(如共享变量的读写)的happen-before关系。只有写入操作happen-before读取操作时,才保证一个线程写入的结果对另一个线程的读取是可视的。synchronized和volatile构造happen-before关系,Thread.start()和Thread.join()方法形成happen-before关系。尤其是:

线程中的每个操作happen-before稍后按程序顺序传入的该线程中的每个操作。

一个解除锁监视器的(synchronized阻塞或方法退出)happen-before相同监视器的每个后续锁(synchronized阻塞或方法进入)。并且因为happen-before关系是可传递的,所以解除锁定之前的线程的所有操作happen-before锁定该监视器的任何线程后续的所有操作。

写入volatile字段happen-before每个后续读取相同字段。volatile字段的读取和写入与进入和退出监视器具有相似的内存一致性效果,但需要互斥锁。

在线程上调用starthappen-before已启动的线程中的任何线程。

线程中的所有操作happen-before从该线程上的join成功返回的任何其他线程。

17.JMM是如何处理编译器和处理器对指令的重排序问题的?

对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序

对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序

总之一句话,JMM是通过禁止特定类型的编译器重排序和处理器重排序来为程序员提供一致的内存可见性保证。

上一篇下一篇

猜你喜欢

热点阅读