java并发编程的艺术

1 java并发编程的艺术

2.1 volatile的应用
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
1.volatile的定义与实现原理
缓存行:缓存中可以分配的最小存储单位
0x01a3de1d: movb 0×0,0×1104800( 0 × 0 , 0 × 1104800 ( 0×0,(%esp);
Lock前缀的指令在多核处理器下会引发了两件事情[:
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。()一个处理器的缓存回写到内存会导致其他处理器的缓存无效。)
3)volatile的内存语义
·可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
·原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原性。
·线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过
主内存向线程B发送消息。当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保
volatile写之前的操作不会被编译器重排序到volatile写之后。
·当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保
volatile读之后的操作不会被编译器重排序到volatile读之前
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序
2,处理器如何实现原子操作:
使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。所谓总线锁就是使用处理器提供的一个
LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该
处理器可以独占共享内存。
使用缓存锁保证原子性
第二个机制是通过缓存锁定来保证原子性。
3,Java如何实现原子操作
(1)使用循环CAS实现原子操作:自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子
方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更
新的long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和。
(2)CAS实现原子操作的三大问题
1)ABA问题(2)循环时间长开销大。3)只能保证一个共享变量的原子操作。
3, 线程之间的通信机制有两种:共享内存和消息传递
同步是指程序中用于控制不同线程间操作发生相对顺序的机制
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享
变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
3.1.3 从源代码到指令序列的重排序
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
的执行顺序
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
去可能是在乱序执行。
3.3.6final域的重排序规则
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用
变量,这两个操作之间不能重排序
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能
重排序
3.6.3读final域的重排序规则
读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final
域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)编译器会在读final
域操作的前面插入一个LoadLoad屏障。
4.3.2 等待/通知机制
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个
过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模
式隔离了“做什么”(what)和“怎么做”(How),在功能层面上实现了解耦,体系结构上具备了良
好的伸缩性
4.3.3 等待/通知的经典范式
等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。
4.3.5 Thread.join()的使用
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才
从thread.join()返回。
4.3.6 ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这
个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个
线程上的一个值
5.1 Lock接口
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,
异常抛出的同时,也会导致锁无故释放。
5.2 队列同步器
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组
件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获
取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步
器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交
互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,
它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
5.2.1 队列同步器的接口
·getState():获取当前同步状态。
·setState(int newState):设置当前同步状态。
·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态
设置的原子性
1.同步队列
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取
同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其
加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再
次尝试获取同步状态。
同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
5.3 重入锁
它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
1.实现重进入
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再
次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到
该锁。
5.5 LockSupport工具
当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应
工作。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功
能,而LockSupport也成为构建同步组件的基础工具。
5.6 Condition接口
Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等
待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点
(lastWaiter)当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部
加入等待队列;
6.3 Java中的阻塞队列
6.3.1 什么是阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞
的插入和移除方法。
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不
满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
6.3.2 Java里的阻塞队列
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证线程公平的访问队列,
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。默认情况下元素采取自然顺序
升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化
PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证
同优先级元素的顺序
·DelayQueue:一个使用优先级队列实现的无界阻塞队列,DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素.
DelayQueue运用在以下应用场景:
·缓存系统的设计:可以用DelayQueue保存缓存元素的有效期
·定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,
·SynchronousQueue:一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,
否则不能继续添加元素。它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。
队列本身并不存储任何元素,非常适合传递性场景。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。(1)transfer方法(2)tryTransfer方法
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列,双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
6.3.3 阻塞队列的实现原理
让生产者和消费者进行高效率的通信呢?
使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生
产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
6.4.1 什么是Fork/Join框架
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干
个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
工作窃取算法的优点:充分利用线程进行并行计算,减少了线程间的竞争
工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时.
ForkJoinTask与一般任务的主要区别在于它
需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执
行任务。如如果不足够小,就必须分割成两个子任务,每个子任务在调用fork方法时,又会进入
compute方法。
7.1 原子更新基本类型类
·AtomicBoolean:原子更新布尔类型。
·AtomicInteger:原子更新整型。
·AtomicLong:原子更新长整型。
7.2 原子更新数组
·AtomicIntegerArray:原子更新整型数组里的元素。
·AtomicLongArray:原子更新长整型数组里的元素。
·AtomicReferenceArray:原子更新引用类型数组里的元素。
7.4 原子更新字段类
·AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
·AtomicLongFieldUpdater:原子更新长整型字段的更新器。
8.1 等待多线程完成的CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作,
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完
成,这里就传入N。
当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法
会阻塞当前线程,直到N变成零。
8.2 同步屏障CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一
组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会
开门,所有被屏障拦截的线程才会继续运行。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数
量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
8.2.2 CyclicBarrier的应用场景
用一个Excel保存了用户所有银行流水,每个Sheet保存一个账户近一年的每笔银行流水,现在需要统计用户的日均银行流水,
8.2.3 CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重
置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数
器,并让线程重新执行一次
CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier
阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。
8.3 控制并发线程数的Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以
保证合理的使用公共资源。
1.应用场景:Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。
8.4 线程间交换数据的Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交
换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过
exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也
执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产
出来的数据传递给对方。
9.1 线程池的实现原理
线程池的处理流程如下:
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作
线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这
个工作队列里。如果工作队列满了,则进入下个流程
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程
来执行任务。如果已经满了,则交给饱和策略来处理这个任务
ThreadPoolExecutor执行execute方法分下面4种情况
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤
需要获取全局锁)
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执
行这一步骤需要获取全局锁)。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用
RejectedExecutionHandler.rejectedExecution()方法。
9.2 线程池的使用
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
milliseconds,runnableTaskQueue, handler);
1)corePoolSize(线程池的基本大小):
2)runnableTaskQueue(任务队列):
3)maximumPoolSize(线程池最大数量)
4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设
置更有意义的名字。
5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,那么必须采取一种策略处理提交的新任务
·AbortPolicy:直接抛出异常。
·CallerRunsPolicy:只用调用者所在线程来运行任务
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
·DiscardPolicy:不处理,丢弃掉。
·keepAliveTime(线程活动保持时间)
·TimeUnit(线程活动保持时间的单位)
9.2.2 向线程池提交任务
execute()和submit()方法。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个
future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方
法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线
程一段时间后立即返回,这时候有可能任务没有执行完。
9.2.3 关闭线程池
shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
10.1.2 Executor框架的结构与成员
1.Executor框架的结构
Executor框架主要由3大部分组成如下:
·任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口。
·任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的
ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口
(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。
·异步计算的结果。包括接口Future和实现Future接口的FutureTask类
2.Executor框架的成员
(1)ThreadPoolExecutor
ThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建3种类型的
ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。
1)FixedThreadPool。下面是Executors提供的,创建使用固定线程数的FixedThreadPool的
API
2)SingleThreadExecutor。下面是Executors提供的,创建使用单个线程的SingleThreadExecutor的API。
3)CachedThreadPool。下面是Executors提供的,创建一个会根据需要创建新线程的
CachedThreadPool的API
(2)ScheduledThreadPoolExecutor
·ScheduledThreadPoolExecutor。包含若干个线程的ScheduledThreadPoolExecutor。
·SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。
(3)Future接口
Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。当我们把Runnable
接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或
ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会向我们
返回一个FutureTask对象
10.2 ThreadPoolExecutor详解
·通过Executor框架的工具类Executors,可以创建3种类型的ThreadPoolExecutor。
·FixedThreadPool。
·SingleThreadExecutor。
·CachedThreadPool。
10.2.1 FixedThreadPool详解
FixedThreadPool被称为可重用固定线程数的线程池。
1)如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。
2)在线程池完成预热之后(当前运行的线程数等于corePoolSize),将任务加入
LinkedBlockingQueue。
3)线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。
FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为
Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中
的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。
4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或
shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)
10.2.2 SingleThreadExecutor详解
SingleThreadExecutor是使用单个worker线程的Executor。
SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工
作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列
对线程池带来的影响与FixedThreadPool相同,
1)如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线
程来执行任务。
2)在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入LinkedBlockingQueue。
3)线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来
执行。
10.2.3 CachedThreadPool详解 CachedThreadPool是一个会根据需要创建新线程的线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但
CachedThreadPool的maximumPool是无界的。这如果主线程提交任务的速度高于
maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,
CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源
1)首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程
正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行
offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方
法执行完成;否则执行下面的步骤2)。
2)当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1)将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
3)在步骤2)中新创建的线程将任务执行完后,会执行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线
程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执
行步骤1)),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于
空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源
10.4 FutureTask详解
FutureTask可以处于下面3种状态:
1)未启动。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一
个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态。
2)已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。
3)已完成。FutureTask.run()方法执行完后正常结束,或被取消(FutureTask.cancel(…)),或
执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态。
10.4.3 FutureTask的实现
当线程E执行run()方法时,会唤醒队列中的第一个线程A。线程A被唤醒后,首先把自己从
队列中删除,然后唤醒它的后继线程B,最后线程A从get()方法返回。线程B、C和D重复A线程
的处理流程。最终,在队列中等待的所有线程都被级联唤醒并从get()方法返回。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值