高并发程序设计这本书从浅入深,首先从并发的基础概念(同步异步、并发并行等等)开始,让读者对于并发有一个比较浅显的认识,然后讲述了并行程序的基础——线程,这里主要讲了一下线程的相关状态和相关的操作,然后提了几个可能会因为线程而出问题的错误。接下来讲了一下Java中比较常用的并发包里的类,包括锁、线程池、以及一些并发容器。第四章介绍了一些有关锁的概念以及如何通过优化锁来提高性能,比如锁粗化、所分离等等,另外,JVM本身也对锁进行了一系列的优化,这个下面会详细讲一下。除此之外,这一章还详细讲解了无锁的相关概念和原理,着重通过原子类来说明CAS操作。第五章讨论了一些并行模式以及并行算法。
走进并行世界
并行相关概念
并发和并行:并发是指程序在同一个时间段内顺序发生。并行是指程序在同一时间点发生。
同步和异步:同步是指程序执行是必须按照顺序执行,必须是一段代码返回结果之后才能执行下一段代码。异步调用不必等到结果返回,结果返回时会通知调用线程。
临界区:多个线程访问的共享资源,但是每次只能有一个线程访问,不然可能会出错。
阻塞和非阻塞:阻塞意味着一个线程的执行可能会影响其他线程的执行,比如它获得了所之后其他线程就只能等待它释放锁。非阻塞则是多个线程之间不会互相影响。
死锁、饥饿和活锁:死锁可能是由于多个线程顺序推进不当造成的,他们互相持有对方所需要的资源并且等待对方释放自己需要的资源。饥饿是指线程由于优先级的存在,可能有些低优先级的线程一直都没有时间片执行。活锁是指资源在线程之间相互跳动而导致线程都不能执行。
并发级别
阻塞: 通过加锁的方式来对临界资源进行操作。
无饥饿:采用公平锁的方式,所有线程都有机会执行。
无障碍:所有线程都可以修改临界区资源,一旦发生冲突双方都立即回滚。
无锁:线程不断尝试修改临界变量,发生冲突后会尝试重试。通过CAS操作进行修改。
JMM模型
原子性:操作是一步完成的,不可中断。在多线程的情况下,这种操作不会受其他线程影响。
可见性:由于每个线程都有自己的一个缓存空间,每次取数据和放数据不一定都会在主内存中进行。可见性是指一个线程修改了一个变量,其他线程可以立刻感知这个线程对变量的修改,Java中可以通过volatile关键字来保证变量在线程之间可见。
有序性:并发时由于指令重排序的存在可能会导致程序在多线程之间无序,也就是会引起线程间语义不一致。指令重排序的存在是因为流水线技术可以提高CPU的性能,而重排指令可以保证流水更顺畅。但是有些指令是不可以重排的,这也就是Happen-before规则。主要包括程序顺序规则(一个线程内语义一致),volatile规则(写先于读),线程中断(中断先于被中断)等等。
Java并行的基础
线程相关
线程是轻量级进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。线程的状态包括:
New:初始状态,当一个线程被创建时是这个状态。
Runnable:初始态线程调用start方法后,线程会变成Runnable状态。
Terminated:终止状态,当线程执行完毕之后进入此状态。
Blocked:线程被阻塞时进入阻塞态。
Waiting:无限期等待,runnable调用wait或sleep (不带参数)就会进入此状态。
Timed_waiting:有限期等待,wait和sleep带参数。
线程的方法
start:调用start方法线程从初始态变为运行态,内部自动调用run方法。
stop:直接终止线程,并且会立即释放锁,这样有可能破坏正在修改的对象,然后让别的线程读到这个被破坏的对象。
interrupt:中断线程。
join:等待线程执行完,主线程内调用a.join方法,即主线程要等a线程执行完。
yield:当前线程让出CPU,但是有可能接下来仍然会获得CPU。
volatile、synchronized 和JMM模型
volatile关键字保证了变量的可见性和有序性。可见性通过每次线程修改变量后将变量值刷新到主内存中,每次读变量都从主内存中去读来保证。有序性通过禁止指令重排序保证。synchronized关键字则可以保证原子性、可见性和有序性。原子性通过加锁来实现。
程序中并发可能引起的错误
并发下的ArrayList:多线程下扩容可能会引起数组越界等问题。
并发下的HashMap:扩容可能会导致死锁。
错误的加锁:锁的对象要是同一个才有意义。
JDK并发包
同步控制
可重入锁的优点:
可以响应中断。也就是在等待锁的过程中,可以取消对锁的请求。
锁申请等待超时:线程请求锁一段时间后没有获得锁就会放弃等待。
公平锁:所有线程按照先后顺序依次执行,不会出现由于优先级导致的饥饿现象。
ReadWriteLock:读写锁,适合读多写少的情况,读读不互斥,读写互斥,写写互斥。
CountDownLatch:控制线程等待,通过一个计数器设置线程数。数值为0时主线程方能执行。
CyclicBarrier:循环栅栏,计数器可以重复使用,当计数器完成一次计数时,所有线程一起执行。
线程池
为每一个任务创建一个线程执行,任务执行完成后销毁线程,当任务比较小并且数量比较多的时候系统开销会比较大,因此引入了线程池的概念。系统维护一定数量的线程,每当有新的任务进来时就从线程池中拿出空闲线程并执行任务,当没有空闲线程时会把任务放在任务队列中。
线程池核心参数:
corePoolSize:核心线程数量。
maximumPoolSize:指定了线程池中的最大线程数量。
keepAliveTime:线程数量大于核心线程数量后,多于的空闲线程的存活时间。
unit:keepAliveTime的单位,秒分时等等。
workQueue:任务队列,用于存储提交但是尚未被执行的任务。
handler:拒绝策略。包括1.直接抛出异常,阻止系统正常工作。2.默默的丢弃无法处理的任务。3.丢弃最老的请求,也就是任务队列中即将被执行的任务,然后尝试再次提交当前任务。4.直接在调用者线程里面运行当前被丢弃的任务,会影响系统性能。
常见的工厂方法创建的线程池:
newFixedThreadPool:设置一个核心线程数和最大线程数一样的线程池。当有一个新的任务提交时,若有空闲线程则执行之,否则的话把它放在任务队列中,任务队列满了就考虑拒绝策略了。
newSingleThreadExecutor:返回一个只有一个线程的线程池。
newCachedThreadPool:返回一个线程数目可变的线程池。
并发容器简单介绍
ConcurrentHashMap:一个并发的HashMap,线程安全。
CopyOnWriteArrayList:适合读多写少的情况,底层是一个数组,适合高并发下读,当要修改数组是会复制一个数组并且替换原来的数组。
ConcurrentLinkedQueue:高效的并发队列,底层使用链表实现。
BlockingQueue:通过put和take方法来存取内容。
SkipList:多层链表,链表中的数据是经过排序的。
锁的优化及注意事项
提高性能的方法
减少持锁时间:也就是减少其他线程的等待时间。
减少锁粒度:只在必要的地方加锁。
读写分离锁:读多写少的情况下可以使用分离锁替代独占锁。
锁粗化:将多个相邻的锁连在一起,减少线程不断请求、释放锁的时间。
JVM层面的锁优化
偏向锁:如果一个线程获取了锁,那么这个锁就进入偏向模式,每次线程请求时不用再做同步操作。
轻量级锁:偏向锁存在竞争的情况下,锁就会膨胀为轻量级锁。
自旋锁:当前线程暂时无法获得锁,会自旋几个时钟周期,若还没有获得锁,就挂起该线程。
锁消除:利用逃逸分析技术,消除不可能存在资源共享的锁。节约请求锁的时间。
无锁
无锁假设对资源的请求是不会出现冲突的,因此不需要等待,所有线程都可以在不停顿的情况下持续执行。当遇到冲突时,使用CAS(比较并交换)的技术来鉴别冲突,一旦发生冲突,就一直重试一直到没有冲突为止。CAS的方法是这样的,包含三个参数,V代表要更新的变量,E代表要更新变量的预期值,N代表新值,当V和E相等时才可以使用N替换。
AtomicInteger:无锁的安全整数,原子包下的类。
AtomicReference:无锁的对象引用,可以对普通的对象引用进行包装。
AtomicStampedeReference:带有时间戳的对象引用,通过保存状态信息可以解决对象两次更改之后值一样的情况。
并行模式及并行算法
单例模式
保证对象全局唯一性的一种方式。
不变模式
一个对象一旦被创建,它的内部状态一定是不可以被改变的。使用final关键字。
生产者消费者模式
生产者线程用于提交用户请求,消费者线程用于处理用户请求,生产者和消费者之间通过共享缓冲区来进行通信。
Future模式
本质上属于异步调用。当一个线程实现了Callable接口并实现call方法时是可以返回真是数据的,future可以用于接受这个数值,并且通过get方法获得它。
并行流水线
将任务分成多个部分,可以充分利用多核CPU的优势。
NIO
通过selector将线程分发到多个channel上,每个channel对应着一个Buffer,从buffer中获得数据之后会通知selector,然后返回给线程。
AIO
异步非阻塞。异步意味这IO操作完成之后再给线程发送通知,非阻塞意味着各个线程之间完全不会相互影响。