第二章
自旋锁-原理
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。由此我们可以看出,自旋锁是一种比较低级的保护数据结构或代码片段的原始方式,这种锁可能存在两个问题:
1、死锁。试图递归地获得自旋锁必然会引起死锁:递归程序的持有实例在第二个实例循环,以试图获得相同自旋锁时,不会释放此自旋锁。
在递归程序中使用自旋锁应遵守下列策略:
递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。此外如果一个进程已经将资源锁定,那么,即使其它申请这个资源的进程不停地疯狂"自旋",也无法获得资源,从而进入死循环。
2、过多占用cpu资源。如果不加限制,由于申请者一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不会睡眠,会持续的尝试,单cpu的时候自旋锁会让其它process动不了. 因此,一般自旋锁实现会有一个参数限定最多持续尝试次数. 超出后, 自旋锁放弃当前time slice. 等下一次机会
由此可见,自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作
原子操作:(1)总线锁,使用处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器可以独占共享内存,(缺点,总线开销大);(2)缓存锁
Java如何实现原子操作:(1)循环CAS(2)锁机制实现
第三章
在共享内存的并发模型里,通过写读内存中的公共状态进行隐式通信;在消息传递的并发模型里,通过发消息来显式通信
java线程之间的通信由java内存模型(JMM)控制,通过控制主内存与每个线程的本地内存之间的交互(像是一个中转站)。
编译器优化的重排序,指令并行的重排序,内存系统的重排序。
Happens-before来阐述操作之间的内存可见性。
遵守as-if-serial语义(不管怎么重排序,单线程程序的执行结果不能被改变),编译器和处理器不会对存在数据依赖关系的操作做重排序;Happens-before保证多线程程序的执行结果不被改变,都是为了不改变结果的前提下,尽可能地提高并行度。
顺序一致性模型与JMM的区别
1.顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行。(比如正确同步的多线程程序在临界区内的重排序)
2.顺序一致性模型保证所有线程能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
3.JMM不保证对64位long型和double型变量的写操作具有原子性,而顺序一致性模型能保证对所有的内存读/写操作都具有原子性
内存屏障(LoadLoad,StoreStore,LoadStore,StoreLoad)有两个作用:
1、阻止屏障两侧的指令重排序;
2、强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
Volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性
旧的JMM中有允许volatile变量与普通变量重排序,后来严格限制,以确保volatile的写/读和锁的释放,获取具有相同的内存语义
对于final 域,编译器和处理器要遵守两个重排序规则:
在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。
Final域的重排列规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。
JMM的内存可见性保证
对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,Null,False),JMM保证线程读操作读取到的值不会无中生有(Out Of Thin Air)的冒出来。64位数据的非原子性写“发生”在对象被多个线程使用的过程中(写共享变量)。
第四章
线程状态:NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED
生产者-消费者:消费者无限循环检查变量是否满足预期。睡眠一段时间,再检查,防止过快的“无效”,问题:1)难以确保及时性;2)难以降低开销。JAVA引出,等待/通知机制(notify(),notifyAll(),wait())。
Notify()将等待队列中的一个等待线程移到同步队列中。从waiting方法返回的前提是获得调用锁的对象
线程池:消除了频繁创建喝消亡线程的系统资源开销;面对过量任务的提交能够平缓劣化。
第五章
Synchronized将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。
(先获得A锁,再获得B锁,当B锁获得后,释放A锁同时获得C锁,当锁C获得后,再释放B锁同时获得锁D,以此类推)
独占式:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会加入到队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态是,同步器调用tryRelease(int arg)释放同步状态,然后唤醒头节点的后继节点。
第六章
多线程会导致hashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
第八章
并发工具类:
等待多线程完成的CountDownLatch,需要等待所有线程完成操作,这里可以使用join方法,检查join线程是否存活,直到中止之后才会调用this.notifyAll();还可以使用CountDownLatch构造函数,new CountDownLatch(n),n可以是n个线程也可以是线程的n个操作,每执行一次,便countDown(),即n--,直到n为0。
同步屏障CyclicBarrier,一个线程到达屏障时被阻塞,直到所有线程到达屏障才会继续运行。每个线程调用await()方法,告知已到达。构造函数CyclicBarrier(int parties),参数即线程数量。
上述两个区别:CountDownLatch计数器只能使用一次,CyclicBarrier可以reset,中途有错误便可重新开始计算。
Semaphore信号量,控制同时访问特定资源的线程数量,用于做流量控制
第九章
核心线程池是否已满,工作队列是否已满,线程池的线程是否都在工作。
线程池的创建:
- corePoolSize
- RunnableTaskQueue,任务队列
- maximumPoolSize
- ThreadFactory,用于创建线程的工程
- RejectedExecutionHandler,线程池已满执行饱和策略(直接抛出异常(默认),丢弃队列里最近的一个任务等等)
关闭线程池:
Shutdown、shutdownNow,原理:逐个调用线程的interrupt 中断线程,区别:shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回