让我来康康Java并发编程的套路。(然后在cpp里自己造轮子,wdnmd)
第1章 并发编程现成基础
在Java中有三种创建线程的方式
- Runnable
- 继承Thread类
- java不能多继承,所以这个方法会导致不能继承其他类
- FutureTask
- 方便获取返回值。
- 有点像js的await。
紧接着是一堆线程常用操作。
- wait, sleep, notify, join, yield
- java里的线程中断简直迷惑行为
interrupted()
里用了currentThread()
,始终是对主线程起作用。
守护线程与用户线程
- JVM不等待守护线程退出,gc就属于守护线程
- 用户可通过
setDaemon(true)
将线程设置为守护线程 - tomcat中的服务线程也都默认是守护线程
ThreadLocal
- 线程私有变量
- 具体是每个线程维护一个私有的map,访问时先找到map,再找变量
- 不支持继承
InheritableThreadLocal
- 可被继承
- 像Web server可能需要继承用户的token
第2章 并发编程其他基础知识
线程安全,指多个线程同时读写一个共享资源,并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。
内存可见性问题,书中的意思是L1 cache不同步,但根据我在体系结构和编译原理中所学,我以为问题是寄存器不同步。
- 计算机在操作变量时,总是在寄存器里操作的,这样速度也最快。
- 编译器有时会做一些优化,使得对某一个变量的操作总是在寄存器中进行。如果该变量是一个局部变量,这就是一个非常完美的优化。但有时,编译器根据上下文,也可能将某个变量一直存在寄存器中,直到函数结束再写回主存。
- 在多核cpu中,寄存器是肯定不共享的。L1 cache这一层,应当有cache一致性算法提供保证。(这是我的理解,这些都可能随着计算机发展而变化,具体情况具体分析)
原子性操作
- syncronized,java关键字,被修饰的代码块,执行时需要先获取独占锁。
Unsafe类,java中玩转指针
- 操纵对象属性
- 操纵数组元素
- 线程挂起与恢复、CAS(硬件级别的原子性保证)
ABA问题
代码重排序问题
1
2
3
4
5
6
7
8
9
10thread-1
{
num = 2
ready = true
}
thread-2
{
if ready:
print(num)
}- 对num和ready的赋值会被编译器识别为,无依赖性,有可能会重排汇编指令执行顺序。
- java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
- 屏障的作用
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
- volatile修饰的变量。
- 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
- 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
锁
- 悲观锁,整个操作过程都使用排他锁。
- 乐观锁,通常在本地(对线程来说就是单独的栈,对分布式来说是不同的进程)修改,最后update的时候获取锁。
- 公平锁,拿锁遵循先到先得。可以避免某个线程饥饿。
- 共享锁,通常用于实现读写锁,读锁是共享的。写锁则是独占的。不过在《Linux多线程服务端编程:使用muduo C++网络库》中,作者提到,实践中从来不用读写锁。真正解决问题还是靠减小critical block。
- 可重入锁,指的是同一线程重复获取同一个锁。synchronized的锁是实例级别的,可重入实现,使得可以在synchronized方法中,调用自己的另一个sync方法。
- 自旋锁,获取锁失败时,不suspend,也就是不放弃cpu时间,认为很快可以获得锁。内核代码大量使用自旋锁,因为计算机正常工作时,处于内核态的时间应该不多,而且context switch开销很大。